dirwalk 1.1.1

Platform-optimized recursive directory walker with metadata
Documentation
/// A file or directory entry with metadata captured during enumeration.
#[derive(Clone, Debug)]
pub struct Entry {
    /// Relative to the walk root. Just the filename for `scan_dir` results.
    pub relative_path: String,
    /// Relative to the walk root. 0 for `scan_dir` results.
    pub depth: u32,
    /// 0 for directories.
    pub size: u64,
    pub is_dir: bool,
    pub is_symlink: bool,
    pub is_hidden: bool,
    /// Unix timestamp (seconds since epoch).
    pub modified: i64,
}

impl AsRef<Entry> for Entry {
    fn as_ref(&self) -> &Entry {
        self
    }
}

impl Entry {
    #[inline]
    pub fn name(&self) -> &str {
        match self.relative_path.rfind(['/', '\\']) {
            Some(pos) => &self.relative_path[pos + 1..],
            None => &self.relative_path,
        }
    }

    #[inline]
    pub fn extension(&self) -> Option<&str> {
        if self.is_dir {
            return None;
        }
        let name = self.name();
        let dot = name.rfind('.')?;
        if dot == 0 {
            return None;
        }
        let ext = &name[dot + 1..];
        if ext.is_empty() {
            return None;
        }
        Some(ext)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn file(relative_path: &str) -> Entry {
        Entry {
            relative_path: relative_path.to_owned(),
            depth: 1,
            size: 1,
            is_dir: false,
            is_symlink: false,
            is_hidden: false,
            modified: 0,
        }
    }

    fn dir(relative_path: &str) -> Entry {
        Entry {
            relative_path: relative_path.to_owned(),
            depth: 1,
            size: 0,
            is_dir: true,
            is_symlink: false,
            is_hidden: false,
            modified: 0,
        }
    }

    #[test]
    fn name_no_separator() {
        assert_eq!(file("foo.txt").name(), "foo.txt");
    }

    #[test]
    fn name_forward_slash() {
        assert_eq!(file("sub/foo.txt").name(), "foo.txt");
    }

    #[test]
    fn name_backslash() {
        assert_eq!(file("sub\\foo.txt").name(), "foo.txt");
    }

    #[test]
    fn name_nested() {
        assert_eq!(file("a/b/c.rs").name(), "c.rs");
    }

    #[test]
    fn extension_simple() {
        assert_eq!(file("foo.txt").extension(), Some("txt"));
    }

    #[test]
    fn extension_multiple_dots() {
        assert_eq!(file("archive.tar.gz").extension(), Some("gz"));
    }

    #[test]
    fn extension_trailing_dot() {
        assert_eq!(file("foo.").extension(), None);
    }

    #[test]
    fn extension_no_dot() {
        assert_eq!(file("Makefile").extension(), None);
    }

    #[test]
    fn extension_hidden_file_no_ext() {
        assert_eq!(file(".hidden").extension(), None);
    }

    #[test]
    fn extension_hidden_file_with_ext() {
        assert_eq!(file(".env.bak").extension(), Some("bak"));
    }

    #[test]
    fn extension_preserves_case() {
        assert_eq!(file("photo.PNG").extension(), Some("PNG"));
    }

    #[test]
    fn extension_dir_always_none() {
        assert_eq!(dir("src.bak").extension(), None);
    }

    #[test]
    fn extension_in_subdirectory() {
        assert_eq!(file("sub/foo.rs").extension(), Some("rs"));
    }
}

#[cfg(feature = "serde")]
impl serde::Serialize for Entry {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        use serde::ser::SerializeStruct;
        let mut s = serializer.serialize_struct("Entry", 9)?;
        s.serialize_field("name", self.name())?;
        s.serialize_field("relative_path", &self.relative_path)?;
        s.serialize_field("depth", &self.depth)?;
        s.serialize_field("size", &self.size)?;
        s.serialize_field("is_dir", &self.is_dir)?;
        s.serialize_field("is_symlink", &self.is_symlink)?;
        s.serialize_field("is_hidden", &self.is_hidden)?;
        s.serialize_field("modified", &self.modified)?;
        match self.extension() {
            Some(ext) => s.serialize_field("extension", ext)?,
            None => s.skip_field("extension")?,
        }
        s.end()
    }
}