From 0ef0505829fca7783fa46f0ed9a8b4876791b91a Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Fri, 2 Oct 2026 02:23:22 +0200 Subject: [PATCH] pull: keep self-prefixed multi-orig components in place A component tarball whose entries already carry the addon prefix ('www/...' in sqlite3's 'sqlite3_3.53.4.orig-www.tar.xz') extracts to its end state, but the wrapper detection named it a wrapper and unwrap_into then cleared the addon target - destroying the component just extracted into it - and unwrapped the emptied directory into itself: no 'www' at all, and the deb failed at dh_installdocs over the missing 'www/*' docs. dpkg-source unpacks a component into '/': when the tarball's own layout already is that, the extraction is the end state; leave it alone. --- src/pull.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/pull.rs b/src/pull.rs index 8666670..6044349 100644 --- a/src/pull.rs +++ b/src/pull.rs @@ -167,18 +167,23 @@ fn component_addon(name: &str) -> Option<&str> { /// `dir/` for a multi-orig component, replacing any pre-existing /// addon directory. fn unwrap_into(dir: &Path, wrapper: &Path, addon: Option<&str>) -> Result<(), Box> { - let target = match addon { + match addon { + // dpkg-source unpacks a component into '/': a tarball + // whose entries already carry that prefix ('www/...' in + // 'sqlite3_3.53.4.orig-www.tar.xz') extracts to its end state, and + // unwrapping it would clear the target and move the cleared + // directory into itself. + Some(addon) if wrapper.file_name().is_some_and(|name| name == addon) => Ok(()), Some(addon) => { let target = dir.join(addon); if target.is_dir() { std::fs::remove_dir_all(&target)?; } std::fs::create_dir_all(&target)?; - target + unwrap_wrapper(&target, wrapper) } - None => dir.to_path_buf(), - }; - unwrap_wrapper(&target, wrapper) + None => unwrap_wrapper(dir, wrapper), + } } /// Move the contents of an archive's wrapper directory up into `dir` itself. @@ -1450,6 +1455,41 @@ mod tests { assert!(!tree.join("dovecot-pigeonhole-2.4.5").exists()); } + #[test] + fn test_unwrap_component_already_prefixed_with_addon() { + // Regression: a component tarball whose entries already carry the + // addon prefix ('www/...' in 'sqlite3_3.53.4.orig-www.tar.xz') + // extracts to its end state. It used to be treated as a wrapper of + // the same name as the addon: unwrap_into cleared the target — + // destroying the component just extracted into it — and unwrapped + // the emptied directory into itself, leaving no 'www' at all. + let temp_dir = tempfile::tempdir().unwrap(); + let tar_path = temp_dir.path().join("pkg_1.0.orig-www.tar.gz"); + std::fs::write( + &tar_path, + build_tar(&[("www/README", b"docs"), ("www/img/x", b"img")]), + ) + .unwrap(); + + let tree = temp_dir.path().join("tree"); + let extracted = extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap(); + unwrap_into( + &tree, + &extracted.wrapper.unwrap(), + component_addon("pkg_1.0.orig-www.tar.gz"), + ) + .unwrap(); + + assert_eq!( + std::fs::read_to_string(tree.join("www/README")).unwrap(), + "docs" + ); + assert_eq!( + std::fs::read_to_string(tree.join("www/img/x")).unwrap(), + "img" + ); + } + /// A minimal ustar archive of `pkg-1.0/{Makefile,doc/readme}`, /// bz2-compressed. const BZ2_TAR_WRAPPED: &[u8] = &[