From d7336c133c742bdc968086ec2e4c6287b730cec4 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 3 Jun 2024 10:29:06 +0200 Subject: [PATCH 1/4] Mark Path and PathBuf functions as const if possible --- CHANGELOG.md | 1 + src/path.rs | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a17370346..942cc9ec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added object-safe traits `DynFile`, `DynFilesystem` and `DynStorage` for accessing `Storage`, `Filesystem` and `File` implementations for any storage. - Added `Filesystem::mount_or_else` function ([#57][]) +- Marked `Path::is_empty`, `Path::from_bytes_with_nul`, `Path::from_cstr`, `Path::from_cstr_unchecked`, `Path::as_str_ref_with_trailing_nul`, `Path::as_str`, and `PathBuf::new` as `const`. ## Fixed diff --git a/src/path.rs b/src/path.rs index 3cfc982ef..ee9710ba0 100644 --- a/src/path.rs +++ b/src/path.rs @@ -158,7 +158,7 @@ impl Path { /// assert!(path!("").is_empty()); /// assert!(!path!("something").is_empty()); /// ``` - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { self.inner.to_bytes().is_empty() } @@ -250,9 +250,11 @@ impl Path { /// /// The buffer will be first interpreted as a `CStr` and then checked to be comprised only of /// ASCII characters. - pub fn from_bytes_with_nul(bytes: &[u8]) -> Result<&Self> { - let cstr = CStr::from_bytes_with_nul(bytes).map_err(|_| Error::NotCStr)?; - Self::from_cstr(cstr) + pub const fn from_bytes_with_nul(bytes: &[u8]) -> Result<&Self> { + match CStr::from_bytes_with_nul(bytes) { + Ok(cstr) => Self::from_cstr(cstr), + Err(_) => Err(Error::NotCStr), + } } /// Unchecked version of `from_bytes_with_nul` @@ -267,7 +269,7 @@ impl Path { /// /// The string will be checked to be comprised only of ASCII characters // XXX should we reject empty paths (`""`) here? - pub fn from_cstr(cstr: &CStr) -> Result<&Self> { + pub const fn from_cstr(cstr: &CStr) -> Result<&Self> { let bytes = cstr.to_bytes(); let n = cstr.to_bytes().len(); if n > consts::PATH_MAX { @@ -283,7 +285,7 @@ impl Path { /// /// # Safety /// `cstr` must be comprised only of ASCII characters - pub unsafe fn from_cstr_unchecked(cstr: &CStr) -> &Self { + pub const unsafe fn from_cstr_unchecked(cstr: &CStr) -> &Self { &*(cstr as *const CStr as *const Path) } @@ -304,12 +306,12 @@ impl Path { } // helpful for debugging wither the trailing nul is indeed a trailing nul. - pub fn as_str_ref_with_trailing_nul(&self) -> &str { + pub const 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 { + pub const fn as_str(&self) -> &str { // SAFETY: ASCII is valid UTF-8 unsafe { str::from_utf8_unchecked(self.inner.to_bytes()) } } @@ -424,7 +426,7 @@ impl Default for PathBuf { } impl PathBuf { - pub fn new() -> Self { + pub const fn new() -> Self { Self { buf: [0; consts::PATH_MAX_PLUS_ONE], len: 1, From 571aad9b46e7b5b68298232b5d2abd34b5eea681 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 3 Jun 2024 10:22:34 +0200 Subject: [PATCH 2/4] Remove Path::from_bytes_with_nul_unchecked Path::from_bytes_with_nul_unchecked was a wrapper for CStr::from_bytes_with_nul_unchecked and Path::from_cstr_unchecked. Keeping the two methods separate clearly communicates the requirements and reduces the likelihood of unsound usage. --- CHANGELOG.md | 10 +++++++--- src/path.rs | 25 +++++++++++-------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 942cc9ec7..e9735808f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,25 +7,29 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased -## Added +### Added - Added object-safe traits `DynFile`, `DynFilesystem` and `DynStorage` for accessing `Storage`, `Filesystem` and `File` implementations for any storage. - Added `Filesystem::mount_or_else` function ([#57][]) - Marked `Path::is_empty`, `Path::from_bytes_with_nul`, `Path::from_cstr`, `Path::from_cstr_unchecked`, `Path::as_str_ref_with_trailing_nul`, `Path::as_str`, and `PathBuf::new` as `const`. -## Fixed +### Fixed - Fixed macro hygiene for `path!`. - Fixed build error that would occur on Windows systems. - Fixed compilation without default features. - Added path iteration utilities ([#47][]) -## Changed +### Changed - Enforced const evaluation for `path!`. - Removed `cstr_core` and `cty` dependencies. - Updated `littlefs2-sys` dependency to 0.2.0. +### Removed + +- Removed `Path::from_bytes_with_nul_unchecked`. Use `CStr::from_bytes_with_nul_unchecked` and `Path::from_cstr_unchecked` instead. + [#47]: https://github.com/trussed-dev/littlefs2/pull/47 [#57]: https://github.com/trussed-dev/littlefs2/pull/57 diff --git a/src/path.rs b/src/path.rs index ee9710ba0..fb31ee4b2 100644 --- a/src/path.rs +++ b/src/path.rs @@ -188,7 +188,10 @@ impl Path { None | Some((_, "\x00")) => None, Some((_, path)) => { debug_assert!(path.ends_with('\x00')); - Some(unsafe { Path::from_bytes_with_nul_unchecked(path.as_bytes()) }) + unsafe { + let cstr = CStr::from_bytes_with_nul_unchecked(path.as_bytes()); + Some(Path::from_cstr_unchecked(cstr)) + } } } } @@ -243,7 +246,10 @@ impl Path { } assert!(!bytes.is_empty(), "must not be empty"); assert!(bytes[i] == 0, "last byte must be null"); - unsafe { Self::from_bytes_with_nul_unchecked(bytes) } + unsafe { + let cstr = CStr::from_bytes_with_nul_unchecked(bytes); + Self::from_cstr_unchecked(cstr) + } } /// Creates a path from a byte buffer @@ -257,14 +263,6 @@ impl Path { } } - /// Unchecked version of `from_bytes_with_nul` - /// - /// # Safety - /// `bytes` must be null terminated string comprised of only ASCII characters - pub const unsafe fn from_bytes_with_nul_unchecked(bytes: &[u8]) -> &Self { - &*(bytes as *const [u8] as *const Path) - } - /// Creates a path from a C string /// /// The string will be checked to be comprised only of ASCII characters @@ -546,10 +544,9 @@ impl ops::Deref for PathBuf { fn deref(&self) -> &Path { unsafe { - Path::from_bytes_with_nul_unchecked(slice::from_raw_parts( - self.buf.as_ptr().cast(), - self.len, - )) + let bytes = slice::from_raw_parts(self.buf.as_ptr().cast(), self.len); + let cstr = CStr::from_bytes_with_nul_unchecked(bytes); + Path::from_cstr_unchecked(cstr) } } } From caad4110e4b0381a0dd5c3933b104756e831d000 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 3 Jun 2024 10:53:56 +0200 Subject: [PATCH 3/4] Explicitly document Path invariants --- src/path.rs | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/path.rs b/src/path.rs index fb31ee4b2..4ff29f4ce 100644 --- a/src/path.rs +++ b/src/path.rs @@ -13,11 +13,11 @@ use crate::consts; /// A path /// -/// Paths must be null terminated ASCII strings -/// -/// This assumption is not needed for littlefs itself (it works like Linux and -/// accepts arbitrary C strings), but the assumption makes `AsRef` trivial -/// to implement. +/// Paths must be null terminated ASCII strings with at most [`PATH_MAX`](`consts::PATH_MAX`) bytes +/// (not including the trailing null). +// Invariants: +// 1. inner.to_bytes().is_ascii() +// 2. inner.to_bytes().len() <= consts::PATH_MAX #[derive(PartialEq, Eq)] #[repr(transparent)] pub struct Path { @@ -234,8 +234,9 @@ impl Path { /// Creates a path from a string. /// - /// The string must only consist of ASCII characters, expect for the last character which must - /// be null. If these conditions are not met, this function panics. + /// The string must only consist of ASCII characters. The last character must be null. It + /// must contain at most [`PATH_MAX`](`consts::PATH_MAX`) bytes, not including the trailing + /// null. If these conditions are not met, this function panics. pub const fn from_str_with_nul(s: &str) -> &Self { let bytes = s.as_bytes(); let mut i = 0; @@ -252,10 +253,11 @@ impl Path { } } - /// Creates a path from a byte buffer + /// Creates a path from a byte buffer. /// - /// The buffer will be first interpreted as a `CStr` and then checked to be comprised only of - /// ASCII characters. + /// The byte buffer must only consist of ASCII characters. The last character must be null. + /// It must contain at most [`PATH_MAX`](`consts::PATH_MAX`) bytes, not including the trailing + /// null. If these conditions are not met, this function returns an error. pub const fn from_bytes_with_nul(bytes: &[u8]) -> Result<&Self> { match CStr::from_bytes_with_nul(bytes) { Ok(cstr) => Self::from_cstr(cstr), @@ -263,9 +265,11 @@ impl Path { } } - /// Creates a path from a C string + /// Creates a path from a C string. /// - /// The string will be checked to be comprised only of ASCII characters + /// The string must only consist of ASCII characters. It must contain at most + /// [`PATH_MAX`](`consts::PATH_MAX`) bytes, not including the trailing null. If these + /// conditions are not met, this function returns an error. // XXX should we reject empty paths (`""`) here? pub const fn from_cstr(cstr: &CStr) -> Result<&Self> { let bytes = cstr.to_bytes(); @@ -279,10 +283,11 @@ impl Path { } } - /// Unchecked version of `from_cstr` + /// Creates a path from a C string without checking the invariants. /// /// # Safety - /// `cstr` must be comprised only of ASCII characters + /// The string must only consist of ASCII characters. It must contain at most + /// [`PATH_MAX`](`consts::PATH_MAX`) bytes, not including the trailing null. pub const unsafe fn from_cstr_unchecked(cstr: &CStr) -> &Self { &*(cstr as *const CStr as *const Path) } @@ -399,6 +404,13 @@ array_impls!( ); /// An owned, mutable path +/// +/// Paths must be null terminated ASCII strings with at most [`PATH_MAX`](`consts::PATH_MAX`) bytes +/// (not including the trailing null). +// Invariants: +// 1. 0 < len <= consts::PATH_MAX_PLUS_ONE +// 2. buf[len - 1] == 0 +// 3. buf[i].is_ascii() for 0 <= i < len - 1 #[derive(Clone)] pub struct PathBuf { buf: [c_char; consts::PATH_MAX_PLUS_ONE], From 9a97d10a72fbc57efcbc6c9e77ca92a565be9d59 Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Mon, 3 Jun 2024 11:24:01 +0200 Subject: [PATCH 4/4] Make panicking Path/PathBuf conversions fallible To avoid unintended panics and to make it clear which conversions can fail and which cannot, this patch changes all panicking Path/PathBuf conversions to return a Result instead. Fixes: https://github.com/trussed-dev/littlefs2/issues/63 --- CHANGELOG.md | 3 ++ src/fs.rs | 7 ++-- src/lib.rs | 9 +++-- src/path.rs | 101 ++++++++++++++++++++++++++------------------------- src/tests.rs | 8 ++-- 5 files changed, 69 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9735808f..8e8a6522c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Enforced const evaluation for `path!`. - Removed `cstr_core` and `cty` dependencies. - Updated `littlefs2-sys` dependency to 0.2.0. +- Replace all panicking `Path`/`PathBuf` conversions with fallible alternatives: + - Return a `Result` from `Path::from_str_with_nul`. + - Replace the `From<_>` implementations for `Path` and `PathBuf` with `TryFrom<_>`, except for `From<&Path> for PathBuf`. ### Removed diff --git a/src/fs.rs b/src/fs.rs index 8fa16d8d1..2f8be1706 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -1254,7 +1254,7 @@ impl<'a, Storage: driver::Storage> Filesystem<'a, Storage> { let path_slice = path.as_ref().as_bytes(); for i in 0..path_slice.len() { if path_slice[i] == b'/' { - let dir = PathBuf::from(&path_slice[..i]); + let dir = PathBuf::try_from(&path_slice[..i])?; #[cfg(test)] println!("generated PathBuf dir {:?} using i = {}", &dir, i); match self.create_dir(&dir) { @@ -1362,6 +1362,7 @@ impl<'a, Storage: driver::Storage> Filesystem<'a, Storage> { #[cfg(test)] mod tests { use super::*; + use crate::path; use core::convert::TryInto; use driver::Storage as LfsStorage; use io::Result as LfsResult; @@ -1483,7 +1484,7 @@ mod tests { // (...) // fs.remove_dir_all(&PathBuf::from(b"/tmp\0"))?; // fs.remove_dir_all(&PathBuf::from(b"/tmp"))?; - fs.remove_dir_all(&PathBuf::from("/tmp"))?; + fs.remove_dir_all(path!("/tmp"))?; } Ok(()) @@ -1493,7 +1494,7 @@ mod tests { let mut alloc = Allocation::new(); let fs = Filesystem::mount(&mut alloc, &mut test_storage).unwrap(); // fs.write(b"/z.txt\0".try_into().unwrap(), &jackson5).unwrap(); - fs.write(&PathBuf::from("z.txt"), jackson5).unwrap(); + fs.write(path!("z.txt"), jackson5).unwrap(); } #[cfg(feature = "dir-entry-path")] diff --git a/src/lib.rs b/src/lib.rs index 8186606d6..4b336f483 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -93,7 +93,7 @@ Separately, keeping track of the allocations is a chore, we hope that ``` # use littlefs2::fs::{Filesystem, File, OpenOptions}; # use littlefs2::io::prelude::*; -# use littlefs2::path::PathBuf; +# use littlefs2::path; # # use littlefs2::{consts, ram_storage, driver, io::Result}; # @@ -113,7 +113,7 @@ let mut fs = Filesystem::mount(&mut alloc, &mut storage).unwrap(); let mut buf = [0u8; 11]; fs.open_file_with_options_and_then( |options| options.read(true).write(true).create(true), - &PathBuf::from(b"example.txt"), + path!("example.txt"), |file| { file.write(b"Why is black smoke coming out?!")?; file.seek(SeekFrom::End(-24)).unwrap(); @@ -199,7 +199,10 @@ pub struct Version { macro_rules! path { ($path:literal) => {{ const _PATH: &$crate::path::Path = - $crate::path::Path::from_str_with_nul(::core::concat!($path, "\0")); + match $crate::path::Path::from_str_with_nul(::core::concat!($path, "\0")) { + Ok(path) => path, + Err(_) => panic!("invalid littlefs2 path"), + }; _PATH }}; } diff --git a/src/path.rs b/src/path.rs index 4ff29f4ce..396070dcb 100644 --- a/src/path.rs +++ b/src/path.rs @@ -9,7 +9,7 @@ use core::{ ops, ptr, slice, str, }; -use crate::consts; +use crate::{consts, path}; /// A path /// @@ -94,14 +94,14 @@ impl<'a> Iterator for Ancestors<'a> { return None; } else if self.path == "/" { self.path = ""; - return Some("/".into()); + return Some(path!("/").into()); } let item = self.path; let Some((rem, item_name)) = self.path.rsplit_once('/') else { self.path = ""; - return Some(item.into()); + return item.try_into().ok(); }; if self.path.starts_with('/') && rem.is_empty() { @@ -114,7 +114,7 @@ impl<'a> Iterator for Ancestors<'a> { if item_name.is_empty() { self.next(); } - Some(item.into()) + item.try_into().ok() } } @@ -135,17 +135,17 @@ impl<'a> Iterator for Iter<'a> { } if self.path.starts_with('/') { self.path = &self.path[1..]; - return Some("/".into()); + return Some(path!("/").into()); } let Some((path, rem)) = self.path.split_once('/') else { - let ret_val = Some(self.path.into()); + let ret_val = self.path.try_into().ok(); self.path = ""; return ret_val; }; self.path = rem; - Some(path.into()) + path.try_into().ok() } } @@ -236,21 +236,9 @@ impl Path { /// /// The string must only consist of ASCII characters. The last character must be null. It /// must contain at most [`PATH_MAX`](`consts::PATH_MAX`) bytes, not including the trailing - /// null. If these conditions are not met, this function panics. - pub const fn from_str_with_nul(s: &str) -> &Self { - let bytes = s.as_bytes(); - let mut i = 0; - while i < bytes.len().saturating_sub(1) { - assert!(bytes[i] != 0, "must not contain null"); - assert!(bytes[i].is_ascii(), "must be ASCII"); - i += 1; - } - assert!(!bytes.is_empty(), "must not be empty"); - assert!(bytes[i] == 0, "last byte must be null"); - unsafe { - let cstr = CStr::from_bytes_with_nul_unchecked(bytes); - Self::from_cstr_unchecked(cstr) - } + /// null. If these conditions are not met, this function returns an error. + pub const fn from_str_with_nul(s: &str) -> Result<&Self> { + Self::from_bytes_with_nul(s.as_bytes()) } /// Creates a path from a byte buffer. @@ -322,14 +310,16 @@ impl Path { pub fn parent(&self) -> Option { 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 => Some(PathBuf::from("/")), + Some(0) if rk_path_bytes.len() != 1 => Some(path!("/").into()), Some(slash_index) => { // if we have a directory that ends with `/`, // still need to "go up" one parent if slash_index + 1 == rk_path_bytes.len() { - PathBuf::from(&rk_path_bytes[..slash_index]).parent() + PathBuf::try_from(&rk_path_bytes[..slash_index]) + .ok()? + .parent() } else { - Some(PathBuf::from(&rk_path_bytes[..slash_index])) + PathBuf::try_from(&rk_path_bytes[..slash_index]).ok() } } None => None, @@ -382,9 +372,11 @@ macro_rules! array_impls { } } - impl From<&[u8; $N]> for PathBuf { - fn from(bytes: &[u8; $N]) -> Self { - Self::from(&bytes[..]) + impl TryFrom<&[u8; $N]> for PathBuf { + type Error = Error; + + fn try_from(bytes: &[u8; $N]) -> Result { + Self::try_from(&bytes[..]) } } @@ -521,11 +513,11 @@ impl From<&Path> for PathBuf { } } -impl From<&[u8]> for PathBuf { - /// Accepts byte string, with or without trailing nul. - /// - /// PANICS: when there are embedded nuls - fn from(bytes: &[u8]) -> Self { +/// Accepts byte strings, with or without trailing nul. +impl TryFrom<&[u8]> for PathBuf { + type Error = Error; + + fn try_from(bytes: &[u8]) -> Result { // NB: This needs to set the final NUL byte, unless it already has one // It also checks that there are no inner NUL bytes let bytes = if !bytes.is_empty() && bytes[bytes.len() - 1] == b'\0' { @@ -533,21 +525,31 @@ impl From<&[u8]> for PathBuf { } else { bytes }; - let has_no_embedded_nul = !bytes.iter().any(|&byte| byte == b'\0'); - assert!(has_no_embedded_nul); + if bytes.len() > consts::PATH_MAX { + return Err(Error::TooLarge); + } + for byte in bytes { + if *byte == 0 { + return Err(Error::NotCStr); + } + if !byte.is_ascii() { + return Err(Error::NotAscii); + } + } let mut buf = [0; consts::PATH_MAX_PLUS_ONE]; let len = bytes.len(); - assert!(len <= consts::PATH_MAX); - assert!(bytes.is_ascii()); unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), buf.as_mut_ptr().cast(), len) } - Self { buf, len: len + 1 } + Ok(Self { buf, len: len + 1 }) } } -impl From<&str> for PathBuf { - fn from(s: &str) -> Self { - PathBuf::from(s.as_bytes()) +/// Accepts strings, with or without trailing nul. +impl TryFrom<&str> for PathBuf { + type Error = Error; + + fn try_from(s: &str) -> Result { + PathBuf::try_from(s.as_bytes()) } } @@ -597,7 +599,7 @@ impl<'de> serde::Deserialize<'de> for PathBuf { if v.len() > consts::PATH_MAX { return Err(E::invalid_length(v.len(), &self)); } - Ok(PathBuf::from(v)) + PathBuf::try_from(v).map_err(|_| E::custom("invalid path buffer")) } } @@ -670,8 +672,8 @@ mod tests { #[test] fn path_macro() { - assert_eq!(EMPTY, &*PathBuf::from("")); - assert_eq!(SLASH, &*PathBuf::from("/")); + assert_eq!(EMPTY, &*PathBuf::try_from("").unwrap()); + assert_eq!(SLASH, &*PathBuf::try_from("/").unwrap()); } // does not compile: @@ -679,15 +681,13 @@ mod tests { // const NULL: &Path = path!("ub\0er"); #[test] - #[should_panic] fn nul_in_from_str_with_nul() { - Path::from_str_with_nul("ub\0er"); + assert!(Path::from_str_with_nul("ub\0er").is_err()); } #[test] - #[should_panic] fn non_ascii_in_from_str_with_nul() { - Path::from_str_with_nul("über"); + assert!(Path::from_str_with_nul("über").is_err()); } #[test] @@ -725,7 +725,10 @@ mod tests { #[test] fn trailing_nuls() { - assert_eq!(PathBuf::from("abc"), PathBuf::from("abc\0")); + assert_eq!( + PathBuf::try_from("abc").unwrap(), + PathBuf::try_from("abc\0").unwrap() + ); } #[test] diff --git a/src/tests.rs b/src/tests.rs index 7e7498d23..1e32e5fcf 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -195,20 +195,20 @@ fn test_create() { assert_eq!(fs.available_blocks().unwrap(), 512 - 2); assert_eq!(fs.available_space().unwrap(), 130_560); - assert!(!crate::path::PathBuf::from(b"/test_open.txt").exists(fs)); + assert!(!path!("/test_open.txt").exists(fs)); assert_eq!( File::open_and_then(fs, b"/test_open.txt\0".try_into().unwrap(), |_| { Ok(()) }) .map(drop) .unwrap_err(), // "real" contains_err is experimental Error::NoSuchEntry ); - assert!(!crate::path::PathBuf::from(b"/test_open.txt").exists(fs)); + assert!(!path!("/test_open.txt").exists(fs)); fs.create_dir(b"/tmp\0".try_into().unwrap()).unwrap(); assert_eq!(fs.available_blocks().unwrap(), 512 - 2 - 2); // can create new files - assert!(!crate::path::PathBuf::from(b"/tmp/test_open.txt").exists(fs)); + assert!(!path!("/tmp/test_open.txt").exists(fs)); fs.create_file_and_then(b"/tmp/test_open.txt\0".try_into().unwrap(), |file| { // can write to files assert!(file.write(&[0u8, 1, 2]).unwrap() == 3); @@ -219,7 +219,7 @@ fn test_create() { // file.close()?; Ok(()) })?; - assert!(crate::path::PathBuf::from(b"/tmp/test_open.txt").exists(fs)); + assert!(path!("/tmp/test_open.txt").exists(fs)); // // cannot remove non-empty directories assert_eq!(