Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add path manipulation utils #47

Merged
merged 6 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased

## Fixed

- Fixed macro hygiene for `path!`.
- Fixed build error that would occur on Windows systems.
- Added path iteration utilities ([#47][])

[#47]: https://github.com/trussed-dev/littlefs2/pull/47

## [v0.4.0] - 2023-02-07

Expand Down
2 changes: 1 addition & 1 deletion src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ impl<Storage: driver::Storage> Filesystem<'_, Storage> {
// TODO: check if this is equivalent to `is_formatted`.
pub fn is_mountable(storage: &mut Storage) -> bool {
let alloc = &mut Allocation::new();
matches!(Filesystem::mount(alloc, storage), Ok(_))
Filesystem::mount(alloc, storage).is_ok()
}

// Can BorrowMut be implemented "unsafely" instead?
Expand Down
258 changes: 255 additions & 3 deletions src/path.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Paths

use core::{convert::TryFrom, fmt, marker::PhantomData, ops, ptr, slice, str};
use core::{convert::TryFrom, fmt, iter::FusedIterator, marker::PhantomData, ops, ptr, slice, str};

use cstr_core::CStr;
use cty::{c_char, size_t};
Expand All @@ -20,7 +20,155 @@ pub struct Path {
inner: CStr,
}

/// Iterator over the ancestors of a Path
///
/// See documentation for [`Path::ancestors`][]
pub struct Ancestors<'a> {
path: &'a str,
}

impl<'a> Iterator for Ancestors<'a> {
type Item = PathBuf;
fn next(&mut self) -> Option<PathBuf> {
if self.path.is_empty() {
return None;
} else if self.path == "/" {
self.path = "";
return Some("/".into());
}

let item = self.path;

let Some((rem, item_name)) = self.path.rsplit_once('/') else {
self.path = "";
return Some(item.into());
};

if self.path.starts_with('/') && rem.is_empty() {
self.path = "/";
} else {
self.path = rem;
}

// Case of a path ending with a trailing `/`
if item_name.is_empty() {
self.next();
}
Some(item.into())
}
}

impl<'a> FusedIterator for Ancestors<'a> {}

/// Iterator over the components of a Path
///
/// See documentation for [`Path::iter`][]
pub struct Iter<'a> {
path: &'a str,
}

impl<'a> Iterator for Iter<'a> {
type Item = PathBuf;
fn next(&mut self) -> Option<PathBuf> {
if self.path.is_empty() {
return None;
}
if self.path.starts_with('/') {
self.path = &self.path[1..];
return Some("/".into());
}

let Some((path, rem)) = self.path.split_once('/') else {
let ret_val = Some(self.path.into());
self.path = "";
return ret_val;
};

self.path = rem;
Some(path.into())
}
}

impl Path {
/// Return true if the path is empty
///
/// ```rust
///# use littlefs2::path;
///
/// assert!(path!("").is_empty());
/// assert!(!path!("something").is_empty());
/// ```
pub fn is_empty(&self) -> bool {
self.inner.to_bytes().is_empty()
}

/// Get the name of the file this path points to if it points to one
///
/// ```
///# use littlefs2::path;
/// let path = path!("/some/path/file.extension");
/// assert_eq!(path.file_name(), Some(path!("file.extension")));
///
/// let path = path!("/");
/// assert_eq!(path.file_name(), None);
///
/// let path = path!("");
/// assert_eq!(path.file_name(), None);
///
/// let path = path!("/some/path/file.extension/");
/// assert_eq!(path.file_name(), None);
/// ```
pub fn file_name(&self) -> Option<&Path> {
if self.is_empty() {
return None;
}

let this = self.as_str_ref_with_trailing_nul();
match this.rsplit_once('/') {
None | Some((_, "\x00")) => None,
Some((_, path)) => {
debug_assert!(path.ends_with('\x00'));
Some(unsafe { &Path::from_bytes_with_nul_unchecked(path.as_bytes()) })
}
}
}

/// Iterate over the ancestors of the path
///
/// ```
///# use littlefs2::path;
/// let path = path!("/some/path/file.extension");
/// let mut ancestors = path.ancestors();
/// assert_eq!(&*ancestors.next().unwrap(), path!("/some/path/file.extension"));
/// assert_eq!(&*ancestors.next().unwrap(), path!("/some/path"));
/// assert_eq!(&*ancestors.next().unwrap(), path!("/some"));
/// assert_eq!(&*ancestors.next().unwrap(), path!("/"));
/// assert!(ancestors.next().is_none());
/// ```
pub fn ancestors(&self) -> Ancestors {
Ancestors {
path: self.as_str(),
}
}

/// Iterate over the components of the path
///
/// ```
///# use littlefs2::path;
/// let path = path!("/some/path/file.extension");
/// let mut iter = path.iter();
/// assert_eq!(&*iter.next().unwrap(), path!("/"));
/// assert_eq!(&*iter.next().unwrap(), path!("some"));
/// assert_eq!(&*iter.next().unwrap(), path!("path"));
/// assert_eq!(&*iter.next().unwrap(), path!("file.extension"));
/// assert!(iter.next().is_none());
/// ```
pub fn iter(&self) -> Iter {
Iter {
path: self.as_str(),
}
}

/// Creates a path from a string.
///
/// The string must only consist of ASCII characters, expect for the last character which must
Expand Down Expand Up @@ -97,12 +245,21 @@ impl Path {

// helpful for debugging wither the trailing nul is indeed a trailing nul.
pub fn as_str_ref_with_trailing_nul(&self) -> &str {
// SAFETY: ASCII is valid UTF-8
unsafe { str::from_utf8_unchecked(self.inner.to_bytes_with_nul()) }
}

pub fn as_str(&self) -> &str {
// SAFETY: ASCII is valid UTF-8
unsafe { str::from_utf8_unchecked(self.inner.to_bytes()) }
}

pub fn parent(&self) -> Option<PathBuf> {
let rk_path_bytes = self.as_ref()[..].as_bytes();
match rk_path_bytes.iter().rposition(|x| *x == b'/') {
Some(0) if rk_path_bytes.len() != 1 => {
return Some(PathBuf::from("/"));
}
Some(slash_index) => {
// if we have a directory that ends with `/`,
// still need to "go up" one parent
Expand All @@ -119,8 +276,7 @@ impl Path {

impl AsRef<str> for Path {
fn as_ref(&self) -> &str {
// NOTE(unsafe) ASCII is valid UTF-8
unsafe { str::from_utf8_unchecked(self.inner.to_bytes()) }
self.as_str()
}
}

Expand Down Expand Up @@ -498,4 +654,100 @@ mod tests {
fn trailing_nuls() {
assert_eq!(PathBuf::from("abc"), PathBuf::from("abc\0"));
}

#[test]
fn ancestors() {
fn assert_ancestor_parent(path: &Path) {
let mut ancestors = path.ancestors();
if !path.as_str().is_empty() {
assert_eq!(&*ancestors.next().unwrap(), path);
}
let mut buf = PathBuf::from(path);
loop {
let parent = buf.parent();
assert_eq!(parent, ancestors.next());
match parent {
Some(p) => buf = p,
None => return,
}
}
}

let path = path!("/some/path/.././file.extension");
assert_ancestor_parent(path);
let mut ancestors = path.ancestors();
assert_eq!(
&*ancestors.next().unwrap(),
path!("/some/path/.././file.extension")
);
assert_eq!(&*ancestors.next().unwrap(), path!("/some/path/../."));
assert_eq!(&*ancestors.next().unwrap(), path!("/some/path/.."));
assert_eq!(&*ancestors.next().unwrap(), path!("/some/path"));
assert_eq!(&*ancestors.next().unwrap(), path!("/some"));
assert_eq!(&*ancestors.next().unwrap(), path!("/"));
assert!(ancestors.next().is_none());

let path = path!("/some/path/.././file.extension/");
assert_ancestor_parent(path);
let mut ancestors = path.ancestors();
assert_eq!(
&*ancestors.next().unwrap(),
path!("/some/path/.././file.extension/")
);
assert_eq!(&*ancestors.next().unwrap(), path!("/some/path/../."));
assert_eq!(&*ancestors.next().unwrap(), path!("/some/path/.."));
assert_eq!(&*ancestors.next().unwrap(), path!("/some/path"));
assert_eq!(&*ancestors.next().unwrap(), path!("/some"));
assert_eq!(&*ancestors.next().unwrap(), path!("/"));
assert!(ancestors.next().is_none());

let path = path!("some/path/.././file.extension");
assert_ancestor_parent(path);
let mut ancestors = path.ancestors();
assert_eq!(
&*ancestors.next().unwrap(),
path!("some/path/.././file.extension")
);
assert_eq!(&*ancestors.next().unwrap(), path!("some/path/../."));
assert_eq!(&*ancestors.next().unwrap(), path!("some/path/.."));
assert_eq!(&*ancestors.next().unwrap(), path!("some/path"));
assert_eq!(&*ancestors.next().unwrap(), path!("some"));
assert!(ancestors.next().is_none());
}

#[test]
fn iter() {
let path = path!("/some/path/.././file.extension");
let mut ancestors = path.iter();
assert_eq!(&*ancestors.next().unwrap(), path!("/"));
assert_eq!(&*ancestors.next().unwrap(), path!("some"));
assert_eq!(&*ancestors.next().unwrap(), path!("path"));
assert_eq!(&*ancestors.next().unwrap(), path!(".."));
assert_eq!(&*ancestors.next().unwrap(), path!("."));
assert_eq!(&*ancestors.next().unwrap(), path!("file.extension"));
assert!(ancestors.next().is_none());
let path = path!("some/path/.././file.extension/");
let mut ancestors = path.iter();
assert_eq!(&*ancestors.next().unwrap(), path!("some"));
assert_eq!(&*ancestors.next().unwrap(), path!("path"));
assert_eq!(&*ancestors.next().unwrap(), path!(".."));
assert_eq!(&*ancestors.next().unwrap(), path!("."));
assert_eq!(&*ancestors.next().unwrap(), path!("file.extension"));
assert!(ancestors.next().is_none());
}

#[test]
fn file_name() {
let path = path!("/some/path/.././file.extension");
assert_eq!(path.file_name(), Some(path!("file.extension")));

let path = path!("/");
assert_eq!(path.file_name(), None);

let path = path!("");
assert_eq!(path.file_name(), None);

let path = path!("/some/path/.././file.extension/");
assert_eq!(path.file_name(), None);
}
}
Loading