diff --git a/crates/spfs-vfs/src/winfsp/mount.rs b/crates/spfs-vfs/src/winfsp/mount.rs index e992f6e995..3e8809c748 100644 --- a/crates/spfs-vfs/src/winfsp/mount.rs +++ b/crates/spfs-vfs/src/winfsp/mount.rs @@ -171,12 +171,10 @@ impl Mount { return None; }; - const TRIM_START: &[char] = &['/', '.']; const TRIM_END: &[char] = &['/']; let path = str_path.replace('\\', "/"); - let path = path - .trim_start_matches(TRIM_START) - .trim_end_matches(TRIM_END); + let path = + spfs::tracking::Manifest::trim_leading_slash(path.as_str()).trim_end_matches(TRIM_END); let mut entry = self .inodes .get(&ROOT_INODE) diff --git a/crates/spfs/src/tracking/manifest.rs b/crates/spfs/src/tracking/manifest.rs index dfbefc26b2..ba390f74e7 100644 --- a/crates/spfs/src/tracking/manifest.rs +++ b/crates/spfs/src/tracking/manifest.rs @@ -115,12 +115,8 @@ impl Manifest { /// Get an entry in this manifest given its filepath. pub fn get_path>(&self, path: P) -> Option<&Entry> { - const TRIM_START: &[char] = &['/', '.']; const TRIM_END: &[char] = &['/']; - let path = path - .as_ref() - .trim_start_matches(TRIM_START) - .trim_end_matches(TRIM_END); + let path = Self::trim_leading_slash(path.as_ref()).trim_end_matches(TRIM_END); let mut entry = &self.root; if path.is_empty() { return Some(entry); @@ -228,8 +224,7 @@ where /// file mode, but can and should be replaced by a new entry in the /// case where this is not desired. pub fn mkdirs>(&mut self, path: P) -> MkResult<&mut Entry> { - const TRIM_PAT: &[char] = &['/', '.']; - let path = path.as_ref().trim_start_matches(TRIM_PAT); + let path = Self::trim_leading_slash(path.as_ref()); if path.is_empty() { return Err(MkError::AlreadyExists(path.into())); } @@ -262,15 +257,31 @@ where } impl Manifest { + /// Remove any leading '/' or elements at the front of the path that are + /// redundant, like "/./". + #[inline] + pub fn trim_leading_slash(path: &str) -> &str { + match *path.as_bytes() { + [b'.', b'/', ref rest @ ..] => { + // Safety: we know that the rest of the path is valid utf-8 + Self::trim_leading_slash(unsafe { std::str::from_utf8_unchecked(rest) }) + } + [b'/', ref rest @ ..] => { + // Safety: we know that the rest of the path is valid utf-8 + Self::trim_leading_slash(unsafe { std::str::from_utf8_unchecked(rest) }) + } + _ => path, + } + } + pub fn mknod>( &mut self, path: P, new_entry: Entry, ) -> MkResult<&mut Entry> { use relative_path::Component; - const TRIM_PAT: &[char] = &['/', '.']; - let path = path.as_ref().trim_start_matches(TRIM_PAT); + let path = Self::trim_leading_slash(path.as_ref()); if path.is_empty() { return Err(MkError::AlreadyExists(path.into())); } diff --git a/crates/spk-cli/cmd-build/src/cmd_build_test/mod.rs b/crates/spk-cli/cmd-build/src/cmd_build_test/mod.rs index c7020245dd..129f7342e9 100644 --- a/crates/spk-cli/cmd-build/src/cmd_build_test/mod.rs +++ b/crates/spk-cli/cmd-build/src/cmd_build_test/mod.rs @@ -653,3 +653,78 @@ build: .await .unwrap(); } + +/// A package may contain files/directories with a leading dot +#[rstest] +#[tokio::test] +async fn test_dot_files_are_collected(tmpdir: tempfile::TempDir) { + let rt = spfs_runtime().await; + + build_package!( + tmpdir, + "dot.spk.yaml", + br#" +api: v0/package +pkg: dot/1.0.0 +build: + script: + - touch /spfs/no_dot + - touch /spfs/.dot + - mkdir /spfs/dot + - touch /spfs/dot/.dot + - mkdir /spfs/.dot2 + - touch /spfs/.dot2/dot + - touch /spfs/.dot2/.dot + - ln -s .dot2 /spfs/.dot3 +"#, + ); + + let build = rt + .tmprepo + .list_package_builds(&version_ident!("dot/1.0.0")) + .await + .unwrap() + .into_iter() + .find(|b| !b.is_source()) + .unwrap(); + + let digest = *rt + .tmprepo + .read_components(&build) + .await + .unwrap() + .get(&Component::Run) + .unwrap(); + + let spk_storage::RepositoryHandle::SPFS(repo) = &*rt.tmprepo else { + panic!("Expected SPFS repo"); + }; + + let layer = repo.read_layer(digest).await.unwrap(); + + let manifest = repo + .read_manifest( + *layer + .manifest() + .expect("Layer should have a manifest in this test"), + ) + .await + .unwrap() + .to_tracking_manifest(); + + for path in &[ + "/no_dot", + "/.dot", + "/dot/.dot", + "/.dot2", + "/.dot2/dot", + "/.dot2/.dot", + "/.dot3", + ] { + let entry = manifest.get_path(path); + assert!( + entry.is_some(), + "should capture file/directory with leading dot: {path}" + ); + } +}