Patchwork D7058: rust-dirstate-status: add first Rust implementation of `dirstate.status`

login
register
mail settings
Submitter phabricator
Date Oct. 11, 2019, 3:44 p.m.
Message ID <differential-rev-PHID-DREV-qrihup5gv62cehkok7pg-req@mercurial-scm.org>
Download mbox | patch
Permalink /patch/42219/
State Superseded
Headers show

Comments

phabricator - Oct. 11, 2019, 3:44 p.m.
Alphare created this revision.
Herald added subscribers: mercurial-devel, kevincox, durin42.
Herald added a reviewer: hg-reviewers.

REVISION SUMMARY
  Note: This patch also added the rayon crate as a Cargo dependency. It will
  help us immensely in making Rust code parallel and easy to maintain. It is
  a stable, well-known, and supported crate maintained by people on the Rust
  team.
  
  The current `dirstate.status` method has grown over the years through bug
  reports and new features to the point where it got too big and too complex.
  
  This series does not yet improve the logic, but adds a Rust fast-path to speed
  up certain cases.
  
  Cases where the fast-path is not executed:
  
  - for commands that need ignore support (`status`, for example)
  - if subrepos are found (should not be hard to add, but winter is coming)
  - any other matcher than an `alwaysmatcher`, like patterns, etc.
  - with extensions like `sparserevlog` and `fsmonitor`
  
  There is a caveat to all of this: the current Rust `dirstatemap` implementation
  is dog slow, its performance needs to be addressed.
  This will be done in a future series, immediately after this one, with the goal
  of getting Rust to be at least to the speed of the Python + C implementation
  in all cases before the 5.2 freeze. At worse, we gate dirstatemap to only be used
  in those cases.
  
  The next step after this is to rethink the logic to be closer to
  Jane Street's Valentin Gatien-Baron's Rust fast-path which does a lot less
  work when possible.

REPOSITORY
  rHG Mercurial

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

AFFECTED FILES
  rust/Cargo.lock
  rust/hg-core/Cargo.toml
  rust/hg-core/src/dirstate.rs
  rust/hg-core/src/dirstate/status.rs
  rust/hg-core/src/lib.rs
  rust/hg-core/src/utils/files.rs

CHANGE DETAILS




To: Alphare, #hg-reviewers
Cc: durin42, kevincox, mercurial-devel
phabricator - Oct. 12, 2019, 12:22 a.m.
martinvonz added a comment.


  I've slowly worked my way through `walk_explicit()` so far, but I had a few comments there, so I'll let you take a look at those first while I take a break.

INLINE COMMENTS

> status.rs:22
> +/// TODO subrepos
> +pub fn walk_explicit<P: AsRef<Path>>(
> +    files: Vec<HgPathBuf>,

Does this need to be public?

> status.rs:22-28
> +pub fn walk_explicit<P: AsRef<Path>>(
> +    files: Vec<HgPathBuf>,
> +    dmap: &mut DirstateMap,
> +    root_dir: P,
> +) -> std::io::Result<HashMap<HgPathBuf, Option<HgMetadata>>>
> +where
> +    P: Sync,

I'm completely new to Rust (just finished going through the tutorial), so please excuse naive questions...

What's the advantage of splitting up the definition of `P` like this? Why not constrain it as `AsRef<Path> + Sync` either on line 22 or line 28?

> status.rs:33
> +    // Sentinel value to prevent subrepo walks.
> +    results.insert(HgPathBuf::from_bytes(b".hg"), None);
> +

Looks like this will end up getting returned. Do we want that? It seems surprising to me.

> status.rs:36-38
> +        let mut v = Vec::new();
> +        v.push(HgPathBuf::new());
> +        v

Isn't this the same as `vec!(HgPathBuf::new())`? Do we dislike the `vec!` macro? Or macros in general?

> status.rs:36-38
> +        let mut v = Vec::new();
> +        v.push(HgPathBuf::new());
> +        v

What is this case about? If no files are specified, we want to return stat information for the (repo) root directory? That seems surprising. Does the caller depend on that? Does it have to?

> status.rs:40
> +    } else {
> +        files.to_vec()
> +    };

It already is a vector, so no need to call `.to_vec()` here?

> status.rs:43
> +
> +    let stats_res: std::io::Result<
> +        Vec<Option<(&HgPathBuf, &HgPathBuf, std::io::Result<Metadata>)>>,

Please add a comment explaining what the type is. What does the outer `Result` mean? What does the `Option` mean? What are the two `HgPathBuf`s? What does the inner `Result` mean?

> status.rs:44
> +    let stats_res: std::io::Result<
> +        Vec<Option<(&HgPathBuf, &HgPathBuf, std::io::Result<Metadata>)>>,
> +    > = files

Do we want to keep the trailing comma?

> status.rs:54-55
> +            }
> +            let target_filename =
> +                root_dir.as_ref().join(hg_path_to_path_buf(filename)?);
> +

Do you think it would be easier to read and reason about this whole function if we did up to here in one step and the `symlink_metadata()` in a separate step after? By "step", I mean either in a separate `.map()` or in a separate `par_iter()`. I think it might be easier to follow if any errors from `hg_path_to_path_buf()` were checked earlier.

> status.rs:55
> +            let target_filename =
> +                root_dir.as_ref().join(hg_path_to_path_buf(filename)?);
> +

It doesn't matter yet, but should this be `normalized` instead of `filename`?

> status.rs:63
> +        })
> +        .collect();
> +

https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect talks about using a "turbofish". I don't know how specific you *want* to be about the type here (do you want all the details on line 44? If not, you can do `.collect::<std::io::Result<Vec<_>>>();` here instead.

> status.rs:65
> +
> +    for res in stats_res? {
> +        match res {

The tutorial seemed to put the `?` where the expression was defined, i.e. at the end of line 63 here.  Seems generally better to me so the variable gets a simpler type and so it's only evaluated once even if the variable is used in multiple places. (Maybe changing it requires the turbofish?)

> status.rs:66
> +    for res in stats_res? {
> +        match res {
> +            Some((normalized, _, Ok(stat))) => {

Are we going to use the second value (the unnormalized filename) later? Otherwise I'd prefer to remove it from the tuple. Actually, I'd prefer to do that now anyway unless you already have patches that start using it.

> status.rs:78-80
> +                    if dmap.contains_key(normalized) {
> +                        results.insert(normalized.to_owned(), None);
> +                    }

This looks just like the `is_dir()` case. Should we merge them (i.e. just check `if stat.is_file() { ... } else { ... }`)?

> status.rs:83
> +            }
> +            None => {}
> +            Some((nf, _, Err(_))) => {

Are we going to do more here later? If not, we can just drop the `Option`-wrapping in `stats_res` (perhaps by adding a `.filter()` step to the iteration).

> status.rs:84
> +            None => {}
> +            Some((nf, _, Err(_))) => {
> +                if dmap.contains_key(nf) {

Are we going to care about the specific error type/message soon? Otherwise, could we replace it by an `Option`?

> status.rs:84
> +            None => {}
> +            Some((nf, _, Err(_))) => {
> +                if dmap.contains_key(nf) {

Call it `normalized` here too for consistency (and `normalized` is clearer than `nf`)

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7058/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

To: Alphare, #hg-reviewers
Cc: martinvonz, durin42, kevincox, mercurial-devel
phabricator - Oct. 14, 2019, 10:05 a.m.
Alphare added a comment.
Alphare marked 3 inline comments as done.


  @martinvonz First of all, thanks a lot for starting the Rust reviews, your feedback is very helpful.
  
  For most of your comments, I tried cutting out the parts of the implementation that were not needed yet but did not go far enough. I'll send a new version in a few minutes.

INLINE COMMENTS

> martinvonz wrote in status.rs:22
> Does this need to be public?

It does not, I used to export it to Python to test it.

> martinvonz wrote in status.rs:22-28
> I'm completely new to Rust (just finished going through the tutorial), so please excuse naive questions...
> 
> What's the advantage of splitting up the definition of `P` like this? Why not constrain it as `AsRef<Path> + Sync` either on line 22 or line 28?

I added the where clause well after defining the signature, it's indeed better to do either one of the two syntax.

> martinvonz wrote in status.rs:33
> Looks like this will end up getting returned. Do we want that? It seems surprising to me.

It gets removed by the caller. I tried to replicate Python code to get close to feature-parity, but the more I dive into this, the less convinced I am that the Python implementation should really be trusted. I just removed it completely since it has no real effect on the current code.

> martinvonz wrote in status.rs:36-38
> Isn't this the same as `vec!(HgPathBuf::new())`? Do we dislike the `vec!` macro? Or macros in general?

It's the same and much cleaner, I just forgot about using this macro since most other collections don't have one.

> martinvonz wrote in status.rs:36-38
> What is this case about? If no files are specified, we want to return stat information for the (repo) root directory? That seems surprising. Does the caller depend on that? Does it have to?

I removed this block of code for the same reason as the sentinel above, it's not useful yet, and it might never be.

> martinvonz wrote in status.rs:43
> Please add a comment explaining what the type is. What does the outer `Result` mean? What does the `Option` mean? What are the two `HgPathBuf`s? What does the inner `Result` mean?

Although the inner type should be explained (which I did in my next patch), most of the time `Result` is easily identifiable by looking at the different `?` within the associated scope, which - I think - makes it not really worth it to comment.

> martinvonz wrote in status.rs:44
> Do we want to keep the trailing comma?

No. You would think that `rustfmt` should remove those, but apparently not.

> martinvonz wrote in status.rs:54-55
> Do you think it would be easier to read and reason about this whole function if we did up to here in one step and the `symlink_metadata()` in a separate step after? By "step", I mean either in a separate `.map()` or in a separate `par_iter()`. I think it might be easier to follow if any errors from `hg_path_to_path_buf()` were checked earlier.

For performance reasons I don't want to do any more loops than I need to. I'm already resorting to a sequential `for` loop right after because I haven't found an easy way of collecting into 3 different collections in parallel. 
Overall, I think that the lack of comments is the real issue, this is not too complex.

> martinvonz wrote in status.rs:55
> It doesn't matter yet, but should this be `normalized` instead of `filename`?

Good catch!

> martinvonz wrote in status.rs:63
> https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.collect talks about using a "turbofish". I don't know how specific you *want* to be about the type here (do you want all the details on line 44? If not, you can do `.collect::<std::io::Result<Vec<_>>>();` here instead.

I figured that having the full type written out was more helpful to understand the context. I still do, but I need comments as well. You're right that collecting into `Result<Vec<_>>` is cleaner and compiles just as well however. :)

I like to avoid the turbofish when possible because I think that it's harder to read that a normal variable type declaration.

> martinvonz wrote in status.rs:65
> The tutorial seemed to put the `?` where the expression was defined, i.e. at the end of line 63 here.  Seems generally better to me so the variable gets a simpler type and so it's only evaluated once even if the variable is used in multiple places. (Maybe changing it requires the turbofish?)

It is indeed preferable to put the `?` where the expression is defined, however in certain cases such as this one, the type inference is not good enough. Here, we're collecting into a `Result`; changing it to `collect?` into a `Vec` causes the compiler to ask us for type annotations.

I could also have a line right after to do `let stats_res = stats_res?;` but I don't think it's really useful since we're only using it once.

> martinvonz wrote in status.rs:83
> Are we going to do more here later? If not, we can just drop the `Option`-wrapping in `stats_res` (perhaps by adding a `.filter()` step to the iteration).

Collecting into a `Result` prevents us from doing a `filter_map` during `collect`. Implementing a `ParallelIterator` adapter that would do a `filter_map` with `Result` handling is possible, but would require more work.

In this specific case, as I eluded to above, I just removed the sentinel value so the question is not relevant anymore.

> martinvonz wrote in status.rs:84
> Are we going to care about the specific error type/message soon? Otherwise, could we replace it by an `Option`?

The `symlink_metadata` returns a `Result`, we don't care about the error here. It's the Python equivalent of a `try except` or `try finally` fallback.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7058/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

To: Alphare, #hg-reviewers
Cc: martinvonz, durin42, kevincox, mercurial-devel
phabricator - Oct. 15, 2019, 9:23 a.m.
Alphare added a comment.


  In D7058#103954 <https://phab.mercurial-scm.org/D7058#103954>, @yuja wrote:
  
  > Just quickly scanned. Not reviewed the core logic.
  >
  >> +/// Get name in the case stored in the filesystem
  >> +/// The name should be relative to root, and be normcase-ed for efficiency.
  >> +///
  >> +/// Note that this function is unnecessary, and should not be
  >> +//  called, for case-sensitive filesystems (simply because it's expensive).
  >> +/// The root should be normcase-ed, too.
  >> +pub fn filesystem_path<P: AsRef<HgPath>>(root: &HgPath, path: P) -> HgPathBuf {
  >
  > Unused?
  
  Indeed, it was for a future patch, removed.
  
  > I think `path: impl AsRef<HgPath>` syntax is easier to follow unless we need
  > a type variable.
  
  I changed to `impl Trait` in all relevant places in this patch.
  
  >> +    // TODO path for case-insensitive filesystems, for now this is transparent
  >> +    root.to_owned().join(path.as_ref())
  >> +}
  >
  > `filesystems_path()` sounds like returning a `std::path::PathBuf`, but
  > `HgPath` should be a repo-relative path.
  >
  >> +/// Returns `true` if path refers to an existing path.
  >> +/// Returns `true` for broken symbolic links.
  >> +/// Equivalent to `exists()` on platforms lacking `lstat`.
  >> +pub fn lexists<P: AsRef<Path>>(path: P) -> bool {
  >
  > Unused?
  
  Same as above, removed.
  
  >> +    if !path.as_ref().exists() {
  >> +        return read_link(path).is_ok();
  >> +    }
  >> +    true
  >> +}
  >
  > Maybe you want `fs::symlink_metadata()`?
  > I don't know which is faster, but the behavior should be closer to `lstat`.
  > https://doc.rust-lang.org/std/fs/fn.symlink_metadata.html
  >
  >> +impl HgMetadata {
  >> +    #[cfg(unix)]
  >> +    pub fn from_metadata(metadata: Metadata) -> Self {
  >> +        use std::os::linux::fs::MetadataExt;
  >
  > `unix != linux`. Maybe you want `std::os::unix::fs::MetadataExt`.
  
  Thanks. I also caught a typo in the name of the non-unix impl. It's now split it in two impls.
  
  >> +pub fn status<P: AsRef<Path>>(
  >> +    mut dmap: &mut DirstateMap,
  >> +    root_dir: P,
  >> +    files: Vec<HgPathBuf>,
  >> +    list_clean: bool,
  >> +    last_normal_time: i64,
  >> +    check_exec: bool,
  >> +) -> std::io::Result<(Vec<HgPathBuf>, StatusResult)>
  >> +where
  >> +    P: Sync,
  >
  > `P: AsRef<path> + Sync`.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7058/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

To: Alphare, #hg-reviewers
Cc: yuja, martinvonz, durin42, kevincox, mercurial-devel
phabricator - Oct. 15, 2019, 1:12 p.m.
This revision now requires changes to proceed.
kevincox added inline comments.
kevincox requested changes to this revision.

INLINE COMMENTS

> Alphare wrote in status.rs:83
> Collecting into a `Result` prevents us from doing a `filter_map` during `collect`. Implementing a `ParallelIterator` adapter that would do a `filter_map` with `Result` handling is possible, but would require more work.
> 
> In this specific case, as I eluded to above, I just removed the sentinel value so the question is not relevant anymore.

I don't follow. can't this `.filter_map(...)` be replaced with a `.filter(...).map(...)`? I think this would make the code much more clear.

> status.rs:23
> +fn walk_explicit(
> +    files: Vec<HgPathBuf>,
> +    dmap: &mut DirstateMap,

This doesn't appear to be consumed by the function so you should take a `&[HgPathBuf]`. Or since I don't think you use the path either probably a `&[HgPath]`.

> status.rs:134
> +    results: HashMap<HgPathBuf, Option<HgMetadata>>,
> +) -> ((Vec<HgPathBuf>, StatusResult)) {
> +    let mut lookup = vec![];

Why are there two pairs of parens?

> status.rs:168
> +                _ => {}
> +            },
> +            Some(HgMetadata {

This seems like a very complicated way to write this. Why not separate the `match state` and do it first. Or put it in an `else` block? Furthermore why do you have two separate `match state`? Why not handle them all in one `match`?

> status.rs:240
> +    let mut results =
> +        walk_explicit(files, &mut dmap, root_dir.as_ref().to_owned())?;
> +

Do you need to do this to `root_dir`? The `walk_explicit` function seems to only need `AsRef<Path>` so you should be able to just pass it. You might need to update the argument to be a reference.

> files.rs:112
> +        // TODO support other platforms
> +        unimplemented!()
> +    }

Isn't it better to avoid implementing this so that we get a compile error rather than a runtime error?

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7058/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

To: Alphare, #hg-reviewers, kevincox
Cc: yuja, martinvonz, durin42, kevincox, mercurial-devel
phabricator - Oct. 15, 2019, 2:43 p.m.
Alphare added inline comments.
Alphare marked an inline comment as done.

INLINE COMMENTS

> kevincox wrote in status.rs:23
> This doesn't appear to be consumed by the function so you should take a `&[HgPathBuf]`. Or since I don't think you use the path either probably a `&[HgPath]`.

Done with `&[impl AsRef<HgPath> + Sync]` which is more generic.

> kevincox wrote in status.rs:168
> This seems like a very complicated way to write this. Why not separate the `match state` and do it first. Or put it in an `else` block? Furthermore why do you have two separate `match state`? Why not handle them all in one `match`?

I could indeed probably simplify it. I'll update this part later today (Paris time) .

> kevincox wrote in status.rs:240
> Do you need to do this to `root_dir`? The `walk_explicit` function seems to only need `AsRef<Path>` so you should be able to just pass it. You might need to update the argument to be a reference.

I had to add a `Copy` trait bound for it to work, otherwise it would be moved by `walk_explicit` before usage in `stat_dmap_entries`

> kevincox wrote in files.rs:112
> Isn't it better to avoid implementing this so that we get a compile error rather than a runtime error?

It is indeed a much better idea. I'll also send a patch for the other uses of `unimplemented!()` in the rest of the codebase

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7058/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

To: Alphare, #hg-reviewers, kevincox
Cc: yuja, martinvonz, durin42, kevincox, mercurial-devel
phabricator - Oct. 15, 2019, 2:48 p.m.
kevincox added a comment.
kevincox accepted this revision.


  Thanks, looks good.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7058/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

To: Alphare, #hg-reviewers, kevincox
Cc: yuja, martinvonz, durin42, kevincox, mercurial-devel
phabricator - Oct. 15, 2019, 5:11 p.m.
martinvonz added inline comments.

INLINE COMMENTS

> status.rs:24
> +    files: Vec<HgPathBuf>,
> +    dmap: &mut DirstateMap,
> +    root_dir: P,

Do we need to borrow it mutably?

> status.rs:49
> +            (normalized, Ok(stat)) => {
> +                if stat.is_file() {
> +                    results.insert(

Should this be `stat.file_type().is_file() || stat.file_type().is_symlink()` as below?

> status.rs:156-160
> +            None if match state {
> +                EntryState::Normal
> +                | EntryState::Merged
> +                | EntryState::Added => true,
> +                _ => false,

This nested match was hard for me to read. I think it would be simpler if you merged this one with the next one inside a simple `None => ` match block, like this:

  None => {
      match state {
          EntryState::Normal
          | EntryState::Merged
          | EntryState::Added => {
              deleted.push(filename);
          },
          EntryState::Removed => {
              removed.push(filename);
          },
          _ => {}
      }
  }

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7058/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

To: Alphare, #hg-reviewers, kevincox
Cc: yuja, martinvonz, durin42, kevincox, mercurial-devel
phabricator - Oct. 15, 2019, 7:13 p.m.
Alphare added inline comments.

INLINE COMMENTS

> martinvonz wrote in status.rs:24
> Do we need to borrow it mutably?

We don't anymore, thanks. I'll be reworking the overly complicated match expression within the next hour.

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7058/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

To: Alphare, #hg-reviewers, kevincox
Cc: yuja, martinvonz, durin42, kevincox, mercurial-devel
phabricator - Oct. 16, 2019, 5:48 p.m.
martinvonz added inline comments.

INLINE COMMENTS

> status.rs:103-110
> +                    Err(ref e)
> +                        if e.kind() == std::io::ErrorKind::NotFound
> +                            || e.raw_os_error() == Some(20) =>
> +                    {
> +                        // Rust does not yet have an `ErrorKind` for
> +                        // `NotADirectory` (errno 20)
> +                        Ok((filename.to_owned(), None))

What does this case mean? I.e. why would we get "NotADirectory"? Oh, I guess if e.g. `dir/file` is in the dirstate, but `dir` is now a file? Could you add a comment here saying that that is what this is about?

REPOSITORY
  rHG Mercurial

CHANGES SINCE LAST ACTION
  https://phab.mercurial-scm.org/D7058/new/

REVISION DETAIL
  https://phab.mercurial-scm.org/D7058

To: Alphare, #hg-reviewers, kevincox
Cc: yuja, martinvonz, durin42, kevincox, mercurial-devel

Patch

diff --git a/rust/hg-core/src/utils/files.rs b/rust/hg-core/src/utils/files.rs
--- a/rust/hg-core/src/utils/files.rs
+++ b/rust/hg-core/src/utils/files.rs
@@ -12,6 +12,7 @@ 
 use crate::utils::hg_path::{HgPath, HgPathBuf};
 use std::iter::FusedIterator;
 
+use std::fs::{read_link, Metadata};
 use std::path::Path;
 
 pub fn get_path_from_bytes(bytes: &[u8]) -> &Path {
@@ -79,6 +80,57 @@ 
     path.to_ascii_lowercase()
 }
 
+/// Get name in the case stored in the filesystem
+/// The name should be relative to root, and be normcase-ed for efficiency.
+///
+/// Note that this function is unnecessary, and should not be
+//  called, for case-sensitive filesystems (simply because it's expensive).
+/// The root should be normcase-ed, too.
+pub fn filesystem_path<P: AsRef<HgPath>>(root: &HgPath, path: P) -> HgPathBuf {
+    // TODO path for case-insensitive filesystems, for now this is transparent
+    root.to_owned().join(path.as_ref())
+}
+
+/// Returns `true` if path refers to an existing path.
+/// Returns `true` for broken symbolic links.
+/// Equivalent to `exists()` on platforms lacking `lstat`.
+pub fn lexists<P: AsRef<Path>>(path: P) -> bool {
+    if !path.as_ref().exists() {
+        return read_link(path).is_ok();
+    }
+    true
+}
+
+#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone)]
+pub struct HgMetadata {
+    pub st_dev: u64,
+    pub st_mode: u32,
+    pub st_nlink: u64,
+    pub st_size: u64,
+    pub st_mtime: i64,
+    pub st_ctime: i64,
+}
+
+impl HgMetadata {
+    #[cfg(unix)]
+    pub fn from_metadata(metadata: Metadata) -> Self {
+        use std::os::linux::fs::MetadataExt;
+        Self {
+            st_dev: metadata.st_dev(),
+            st_mode: metadata.st_mode(),
+            st_nlink: metadata.st_nlink(),
+            st_size: metadata.st_size(),
+            st_mtime: metadata.st_mtime(),
+            st_ctime: metadata.st_ctime(),
+        }
+    }
+    #[cfg(not(unix))]
+    pub fn from_metdata(metadata: Metadata) -> Self {
+        // TODO support other platforms
+        unimplemented!()
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/rust/hg-core/src/lib.rs b/rust/hg-core/src/lib.rs
--- a/rust/hg-core/src/lib.rs
+++ b/rust/hg-core/src/lib.rs
@@ -12,6 +12,7 @@ 
     dirs_multiset::{DirsMultiset, DirsMultisetIter},
     dirstate_map::DirstateMap,
     parsers::{pack_dirstate, parse_dirstate, PARENT_SIZE},
+    status::status,
     CopyMap, CopyMapIter, DirstateEntry, DirstateParents, EntryState,
     StateMap, StateMapIter,
 };
diff --git a/rust/hg-core/src/dirstate/status.rs b/rust/hg-core/src/dirstate/status.rs
new file mode 100644
--- /dev/null
+++ b/rust/hg-core/src/dirstate/status.rs
@@ -0,0 +1,280 @@ 
+// status.rs
+//
+// Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
+//
+// This software may be used and distributed according to the terms of the
+// GNU General Public License version 2 or any later version.
+
+//! Rust implementation of dirstate.status (dirstate.py).
+//! It is currently missing a lot of functionality compared to the Python one
+//! and will only be triggered in narrow cases.
+
+use crate::utils::files::HgMetadata;
+use crate::utils::hg_path::{hg_path_to_path_buf, HgPath, HgPathBuf};
+use crate::{DirstateEntry, DirstateMap, EntryState};
+use rayon::prelude::*;
+use std::collections::HashMap;
+use std::fs::Metadata;
+use std::path::Path;
+
+/// Get stat data about the files explicitly specified by match.
+/// TODO subrepos
+pub fn walk_explicit<P: AsRef<Path>>(
+    files: Vec<HgPathBuf>,
+    dmap: &mut DirstateMap,
+    root_dir: P,
+) -> std::io::Result<HashMap<HgPathBuf, Option<HgMetadata>>>
+where
+    P: Sync,
+{
+    let mut results = HashMap::new();
+
+    // Sentinel value to prevent subrepo walks.
+    results.insert(HgPathBuf::from_bytes(b".hg"), None);
+
+    let files = if files.is_empty() {
+        let mut v = Vec::new();
+        v.push(HgPathBuf::new());
+        v
+    } else {
+        files.to_vec()
+    };
+
+    let stats_res: std::io::Result<
+        Vec<Option<(&HgPathBuf, &HgPathBuf, std::io::Result<Metadata>)>>,
+    > = files
+        .par_iter()
+        .map(|filename| {
+            // TODO normalization
+            let normalized = filename;
+
+            if results.contains_key(normalized) {
+                return Ok(None);
+            }
+            let target_filename =
+                root_dir.as_ref().join(hg_path_to_path_buf(filename)?);
+
+            Ok(Some((
+                normalized,
+                filename,
+                target_filename.symlink_metadata(),
+            )))
+        })
+        .collect();
+
+    for res in stats_res? {
+        match res {
+            Some((normalized, _, Ok(stat))) => {
+                if stat.is_dir() {
+                    if dmap.contains_key(normalized) {
+                        results.insert(normalized.to_owned(), None);
+                    }
+                } else if stat.is_file() {
+                    results.insert(
+                        normalized.to_owned(),
+                        Some(HgMetadata::from_metadata(stat)),
+                    );
+                } else {
+                    if dmap.contains_key(normalized) {
+                        results.insert(normalized.to_owned(), None);
+                    }
+                }
+            }
+            None => {}
+            Some((nf, _, Err(_))) => {
+                if dmap.contains_key(nf) {
+                    results.insert(nf.to_owned(), None);
+                }
+            }
+        };
+    }
+
+    Ok(results)
+}
+
+// Stat all entries in the `DirstateMap` and return their new metadata.
+pub fn stat_dmap_entries<'a, P: AsRef<Path> + Sync>(
+    dmap: &'a DirstateMap,
+    results: &HashMap<HgPathBuf, Option<HgMetadata>>,
+    root_dir: P,
+) -> std::io::Result<Vec<(HgPathBuf, Option<HgMetadata>)>> {
+    dmap.par_iter()
+        .filter_map(
+            // Getting file metadata is costly, so we don't do it if the
+            // file is already present in the results, hence `filter_map`
+            |(filename, _)| -> Option<
+                std::io::Result<(HgPathBuf, Option<HgMetadata>)>,
+            > {
+                if results.contains_key(filename) {
+                    return None;
+                }
+                let meta = match hg_path_to_path_buf(filename) {
+                    Ok(p) => root_dir.as_ref().join(p).symlink_metadata(),
+                    Err(e) => return Some(Err(e.into())),
+                };
+
+                Some(match meta {
+                    Ok(ref m)
+                        if !(m.file_type().is_file()
+                            || m.file_type().is_symlink()) =>
+                    {
+                        Ok((filename.to_owned(), None))
+                    }
+                    Ok(m) => Ok((
+                        filename.to_owned(),
+                        Some(HgMetadata::from_metadata(m)),
+                    )),
+                    Err(ref e)
+                        if e.kind() == std::io::ErrorKind::NotFound
+                            || e.raw_os_error() == Some(20) =>
+                    {
+                        // Rust does not yet have an `ErrorKind` for
+                        // `NotADirectory` (errno 20)
+                        Ok((filename.to_owned(), None))
+                    }
+                    Err(e) => Err(e),
+                })
+            },
+        )
+        .collect()
+}
+
+pub struct StatusResult {
+    pub modified: Vec<HgPathBuf>,
+    pub added: Vec<HgPathBuf>,
+    pub removed: Vec<HgPathBuf>,
+    pub deleted: Vec<HgPathBuf>,
+    pub clean: Vec<HgPathBuf>,
+    // TODO ignored
+    // TODO unknown
+}
+
+fn build_response(
+    dmap: &DirstateMap,
+    list_clean: bool,
+    last_normal_time: i64,
+    check_exec: bool,
+    results: HashMap<HgPathBuf, Option<HgMetadata>>,
+) -> ((Vec<HgPathBuf>, StatusResult)) {
+    let mut lookup = Vec::new();
+    let mut modified = Vec::new();
+    let mut added = Vec::new();
+    let mut removed = Vec::new();
+    let mut deleted = Vec::new();
+    let mut clean = Vec::new();
+
+    for (filename, metadata_option) in results.into_iter() {
+        let DirstateEntry {
+            state,
+            mode,
+            mtime,
+            size,
+        } = match dmap.get(&filename) {
+            None => {
+                continue;
+            }
+            Some(e) => *e,
+        };
+
+        match metadata_option {
+            None if match state {
+                EntryState::Normal
+                | EntryState::Merged
+                | EntryState::Added => true,
+                _ => false,
+            } =>
+            {
+                deleted.push(filename);
+            }
+            None => match state {
+                EntryState::Removed => removed.push(filename),
+                _ => {}
+            },
+            Some(HgMetadata {
+                st_mode,
+                st_size,
+                st_mtime,
+                ..
+            }) => {
+                match state {
+                    EntryState::Normal => {
+                        // Dates and times that are outside the 31-bit signed
+                        // range are compared modulo 2^31. This should prevent
+                        // it from behaving badly with very large files or
+                        // corrupt dates while still having a high probability
+                        // of detecting changes. (issue2608)
+                        let range_mask = 0x7fffffff;
+
+                        let size_changed = (size != st_size as i32)
+                            && size != (st_size as i32 & range_mask);
+                        let mode_changed = (mode ^ st_mode as i32) & 0o100
+                            != 0o000
+                            && check_exec;
+                        if size >= 0
+                            && (size_changed || mode_changed)
+                            || size == -2  // other parent
+                            || dmap.copy_map.contains_key(&filename)
+                        {
+                            modified.push(filename);
+                        } else if mtime != st_mtime as i32
+                            && mtime != (st_mtime as i32 & range_mask)
+                        {
+                            lookup.push(filename);
+                        } else if st_mtime == last_normal_time {
+                            // the file may have just been marked as normal and
+                            // it may have changed in the same second without
+                            // changing its size. This can happen if we quickly
+                            // do multiple commits. Force lookup, so we don't
+                            // miss such a racy file change.
+                            lookup.push(filename);
+                        } else if list_clean {
+                            clean.push(filename);
+                        }
+                    }
+                    EntryState::Merged => modified.push(filename),
+                    EntryState::Added => added.push(filename),
+                    EntryState::Removed => removed.push(filename),
+                    EntryState::Unknown => {}
+                }
+            }
+        }
+    }
+
+    (
+        lookup,
+        StatusResult {
+            modified,
+            added,
+            removed,
+            deleted,
+            clean,
+        },
+    )
+}
+
+pub fn status<P: AsRef<Path>>(
+    mut dmap: &mut DirstateMap,
+    root_dir: P,
+    files: Vec<HgPathBuf>,
+    list_clean: bool,
+    last_normal_time: i64,
+    check_exec: bool,
+) -> std::io::Result<(Vec<HgPathBuf>, StatusResult)>
+where
+    P: Sync,
+{
+    let mut results =
+        walk_explicit(files, &mut dmap, root_dir.as_ref().to_owned())?;
+
+    // Sentinel value to prevent subrepo walks
+    results.remove(HgPath::new(b".hg"));
+    results.extend(stat_dmap_entries(&dmap, &results, root_dir)?);
+
+    Ok(build_response(
+        &dmap,
+        list_clean,
+        last_normal_time,
+        check_exec,
+        results,
+    ))
+}
diff --git a/rust/hg-core/src/dirstate.rs b/rust/hg-core/src/dirstate.rs
--- a/rust/hg-core/src/dirstate.rs
+++ b/rust/hg-core/src/dirstate.rs
@@ -13,6 +13,7 @@ 
 pub mod dirs_multiset;
 pub mod dirstate_map;
 pub mod parsers;
+pub mod status;
 
 #[derive(Debug, PartialEq, Clone)]
 pub struct DirstateParents {
diff --git a/rust/hg-core/Cargo.toml b/rust/hg-core/Cargo.toml
--- a/rust/hg-core/Cargo.toml
+++ b/rust/hg-core/Cargo.toml
@@ -15,3 +15,4 @@ 
 rand = "> 0.6.4"
 rand_pcg = "> 0.1.0"
 regex = "^1.1"
+rayon = "1.2.0"
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -9,6 +9,14 @@ 
 ]
 
 [[package]]
+name = "arrayvec"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "autocfg"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -24,6 +32,11 @@ 
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
+name = "cfg-if"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
 name = "cloudabi"
 version = "0.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -43,6 +56,50 @@ 
 ]
 
 [[package]]
+name = "crossbeam-deque"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "memoffset 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "either"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
 name = "fuchsia-cprng"
 version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -56,6 +113,7 @@ 
  "memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "rand_pcg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
@@ -94,11 +152,32 @@ 
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
+name = "memoffset"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "nodrop"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
 name = "num-traits"
 version = "0.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
+name = "num_cpus"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "python27-sys"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -213,6 +292,28 @@ 
 ]
 
 [[package]]
+name = "rayon"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rayon-core 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "rdrand"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -249,6 +350,11 @@ 
 ]
 
 [[package]]
+name = "scopeguard"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
+[[package]]
 name = "semver"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -300,16 +406,26 @@ 
 
 [metadata]
 "checksum aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9a933f4e58658d7b12defcf96dc5c720f20832deebe3e0a19efd3b6aaeeb9e"
+"checksum arrayvec 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "b8d73f9beda665eaa98ab9e4f7442bd4e7de6652587de55b2525e52e29c1b0ba"
 "checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799"
 "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
 "checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb"
+"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
 "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
 "checksum cpython 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b489034e723e7f5109fecd19b719e664f89ef925be785885252469e9822fa940"
+"checksum crossbeam-deque 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b18cd2e169ad86297e6bc0ad9aa679aee9daa4f19e8163860faf7c164e4f5a71"
+"checksum crossbeam-epoch 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fedcd6772e37f3da2a9af9bf12ebe046c0dfe657992377b4df982a2b54cd37a9"
+"checksum crossbeam-queue 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7c979cd6cfe72335896575c6b5688da489e420d36a27a0b9eb0c73db574b4a4b"
+"checksum crossbeam-utils 0.6.6 (registry+https://github.com/rust-lang/crates.io-index)" = "04973fa96e96579258a5091af6003abde64af786b860f18622b82e026cca60e6"
+"checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b"
 "checksum fuchsia-cprng 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "81f7f8eb465745ea9b02e2704612a9946a59fa40572086c6fd49d6ddcf30bf31"
 "checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
 "checksum libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)" = "2d2857ec59fadc0773853c664d2d18e7198e83883e7060b63c924cb077bd5c74"
 "checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
+"checksum memoffset 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ce6075db033bbbb7ee5a0bbd3a3186bbae616f57fb001c485c7ff77955f8177f"
+"checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945"
 "checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1"
+"checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273"
 "checksum python27-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "56114c37d4dca82526d74009df7782a28c871ac9d36b19d4cb9e67672258527e"
 "checksum python3-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "61e4aac43f833fd637e429506cb2ac9d7df672c4b68f2eaaa163649b7fdc0444"
 "checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
@@ -322,10 +438,13 @@ 
 "checksum rand_os 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b7c690732391ae0abafced5015ffb53656abfaec61b342290e5eb56b286a679d"
 "checksum rand_pcg 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "086bd09a33c7044e56bb44d5bdde5a60e7f119a9e95b0775f545de759a32fe05"
 "checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
+"checksum rayon 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "83a27732a533a1be0a0035a111fe76db89ad312f6f0347004c220c57f209a123"
+"checksum rayon-core 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "98dcf634205083b17d0861252431eb2acbfb698ab7478a2d20de07954f47ec7b"
 "checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
 "checksum regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37e7cbbd370869ce2e8dff25c7018702d10b21a20ef7135316f8daecd6c25b7f"
 "checksum regex-syntax 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4e47a2ed29da7a9e1960e1639e7a982e6edc6d49be308a3b02daf511504a16d1"
 "checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
 "checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
 "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"