diff --git a/lib/src/tar/write.rs b/lib/src/tar/write.rs index 57c367bf..dd02bda5 100644 --- a/lib/src/tar/write.rs +++ b/lib/src/tar/write.rs @@ -159,6 +159,23 @@ fn remap_etc_path(path: &Utf8Path) -> Cow { } } +/// Checks whether the given path has `prefix` as a prefix, ignoring or relative path +/// prefixes. The input paths are also normalized. +fn path_starts_with_ignoring_absolute( + path: impl AsRef, + prefix: impl AsRef, +) -> bool { + fn filter_prefix(c: &Utf8Component) -> bool { + !matches!( + c, + camino::Utf8Component::RootDir | camino::Utf8Component::CurDir, + ) + } + let path = path.as_ref().components().filter(filter_prefix); + let prefix = prefix.as_ref().components().filter(filter_prefix); + path.zip(prefix).all(|(a, b)| a == b) +} + fn normalize_validate_path<'a>( path: &'a Utf8Path, config: &'_ TarImportConfig, @@ -266,7 +283,7 @@ pub(crate) fn filter_tar( let is_modified = header.mtime().unwrap_or_default() > 0; let is_regular = header.entry_type() == tar::EntryType::Regular; - if path.strip_prefix(crate::tar::REPO_PREFIX).is_ok() { + if path_starts_with_ignoring_absolute(&path, crate::tar::REPO_PREFIX) { // If it's a modified file in /sysroot, it may be a target for future hardlinks. // In that case, we copy the data off to a temporary file. Then the first hardlink // to it becomes instead the real file, and any *further* hardlinks refer to that @@ -294,7 +311,7 @@ pub(crate) fn filter_tar( .ok_or_else(|| anyhow!("Invalid empty hardlink"))?; let target: &Utf8Path = (&*target).try_into()?; // If this is a hardlink into /sysroot... - if target.strip_prefix(crate::tar::REPO_PREFIX).is_ok() { + if path_starts_with_ignoring_absolute(&target, crate::tar::REPO_PREFIX) { // And we found a previously processed modified file there if let Some((mut header, data)) = changed_sysroot_objects.remove(target) { tracing::debug!("Making {path} canonical for sysroot link {target}"); @@ -509,6 +526,33 @@ mod tests { } } + #[test] + fn test_path_match_ignoring_absolute() { + for (path, prefix) in [ + ("foo", "foo"), + ("foo", "/"), + ("foo/bar/baz", "foo"), + ("/foo", "foo"), + ("/foo/bar", "foo"), + ("/foo/bar", "/foo"), + ("/foo/bar", "/foo/bar"), + ("///foo", "foo"), + ("///foo", "foo"), + ] { + assert!(path_starts_with_ignoring_absolute(path, prefix)); + } + + for (path, prefix) in [ + ("bar", "foo"), + ("foo/bar/baz", "foo/baz"), + ("/bar", "foo"), + ("/foo/bar", "baz"), + ("/foo/bar", "/baz"), + ] { + assert!(!path_starts_with_ignoring_absolute(path, prefix)); + } + } + #[test] fn test_normalize_path() { let imp_default = &TarImportConfig { diff --git a/lib/tests/it/main.rs b/lib/tests/it/main.rs index dc044804..5fd29c87 100644 --- a/lib/tests/it/main.rs +++ b/lib/tests/it/main.rs @@ -1132,7 +1132,16 @@ async fn test_container_var_content() -> Result<()> { } #[tokio::test] -async fn test_container_etc_hardlinked() -> Result<()> { +async fn test_container_etc_hardlinked_absolute() -> Result<()> { + test_container_etc_hardlinked(true).await +} + +#[tokio::test] +async fn test_container_etc_hardlinked_relative() -> Result<()> { + test_container_etc_hardlinked(false).await +} + +async fn test_container_etc_hardlinked(absolute: bool) -> Result<()> { let fixture = Fixture::new_v1()?; let imgref = fixture.export_container().await.unwrap().0; @@ -1186,7 +1195,13 @@ async fn test_container_etc_hardlinked() -> Result<()> { h.set_entry_type(tar::EntryType::Link); h.set_mtime(42); h.set_size(0); - layer_tar.append_link(&mut h.clone(), "sysroot/ostree/repo/objects/45/7279b28b541ca20358bec8487c81baac6a3d5ed3cea019aee675137fab53cb.file", "etc/dnf.conf")?; + let path = "sysroot/ostree/repo/objects/45/7279b28b541ca20358bec8487c81baac6a3d5ed3cea019aee675137fab53cb.file"; + let path = if absolute { + Cow::Owned(format!("/{path}")) + } else { + Cow::Borrowed(path) + }; + layer_tar.append_link(&mut h.clone(), &*path, "etc/dnf.conf")?; layer_tar.finish()?; Ok(()) },