pull: fix archive extraction and tree assembly
Three archive-side defects broke pulled trees, each surfacing as a later build failure rather than at pull time: - '.tar.bz2'/'.tbz2' origs (busybox) were downloaded but never extracted: the dispatch only knew gz and xz. dpkg-source -b then packed the missing tree as a mass of ignored deletions and the chroot's quilt run failed on every patch. Extract bz2 through bzip2-rs, and match every '.tar.*' of the source package so an unsupported compression fails loudly instead of silently skipping the extraction. - Hard link entries (jq) aborted the whole extraction: resolve their targets against the archive root, where tar guarantees the shared entry was already extracted, with the same confinement checks as regular entries. - The 'package-version/' wrapper was detected from explicit tar directory entries (tree has none, so its sources stayed nested one level too deep) and relocated through a copy that flattened top-level symlinks (htop's 'README -> README.md' became a plain file, which dpkg-source refuses to represent). Recognize the wrapper by the shape of the extraction result - exactly one real directory - and unwrap it by rename, preserving links and timestamps.
This commit is contained in:
+355
-120
@@ -154,14 +154,58 @@ fn copy_file_times(src: &Path, dest: &Path) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Result of extracting an archive
|
||||
struct ExtractedArchive {
|
||||
/// Paths of the extracted files
|
||||
files: Vec<String>,
|
||||
/// The archive used './'-prefixed entries: its contents were extracted
|
||||
/// directly into the destination directory, and there is no
|
||||
/// 'package-version/' top-level directory to relocate
|
||||
in_place: bool,
|
||||
/// Move the contents of a single top-level directory up into `dir` itself.
|
||||
///
|
||||
/// Upstream tarballs are usually wrapped in a 'package-version/' directory
|
||||
/// while the tree root is `dir`. The wrapper's entries are moved up one
|
||||
/// level by rename — symlinks and timestamps survive a move, while a copy
|
||||
/// flattens links and resets times — and the emptied wrapper is removed.
|
||||
/// Any other extraction shape (several top-level entries, files at the
|
||||
/// root, an empty archive) is already in place and is left untouched.
|
||||
fn unwrap_single_dir(dir: &Path) -> Result<(), Box<dyn Error>> {
|
||||
let mut entries: Vec<PathBuf> = Vec::new();
|
||||
for entry in std::fs::read_dir(dir)? {
|
||||
entries.push(entry?.path());
|
||||
}
|
||||
let [wrapper] = entries.as_slice() else {
|
||||
debug!(
|
||||
"{} top-level entries in {}, no wrapper to unwrap",
|
||||
entries.len(),
|
||||
dir.display()
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
// A lone symlink is not a wrapper; unwrapping through it would move
|
||||
// whatever it points to
|
||||
let is_real_dir = std::fs::symlink_metadata(wrapper)
|
||||
.map(|meta| meta.is_dir())
|
||||
.unwrap_or(false);
|
||||
if !is_real_dir {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
debug!("Unwrapping {} into {}", wrapper.display(), dir.display());
|
||||
for entry in std::fs::read_dir(wrapper)? {
|
||||
let entry = entry?;
|
||||
let target = dir.join(entry.file_name());
|
||||
if !target.exists() {
|
||||
std::fs::rename(entry.path(), &target)?;
|
||||
} else {
|
||||
// The tree root already carries that name (a multi-orig archive
|
||||
// laid files there before): merge over it instead of clobbering
|
||||
if entry.path().is_dir() {
|
||||
std::fs::create_dir_all(&target)?;
|
||||
copy_dir_all(&entry.path(), &target)?;
|
||||
std::fs::remove_dir_all(entry.path())?;
|
||||
} else {
|
||||
std::fs::copy(entry.path(), &target)?;
|
||||
copy_file_times(&entry.path(), &target)?;
|
||||
std::fs::remove_file(entry.path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::fs::remove_dir(wrapper)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Helper function to extract tar archive with progress tracking
|
||||
@@ -170,7 +214,7 @@ fn extract_tar_archive<D, F>(
|
||||
dest: &Path,
|
||||
progress: ProgressCallback<'_>,
|
||||
decoder_factory: F,
|
||||
) -> Result<ExtractedArchive, Box<dyn Error>>
|
||||
) -> Result<(), Box<dyn Error>>
|
||||
where
|
||||
D: std::io::Read,
|
||||
F: Fn(File) -> D,
|
||||
@@ -188,23 +232,15 @@ where
|
||||
let decoder = decoder_factory(file);
|
||||
let mut archive = Archive::new(decoder);
|
||||
|
||||
let mut extracted_files = Vec::new();
|
||||
let mut in_place = false;
|
||||
|
||||
for entry in archive.entries()? {
|
||||
let mut entry = entry?;
|
||||
let path = entry.path()?.to_path_buf();
|
||||
|
||||
// Archives built with './'-prefixed entries (common in third-party
|
||||
// repositories) target the destination directory itself, with no
|
||||
// 'package-version/' wrapper; skip their root entry, and remember
|
||||
// the layout for the caller
|
||||
let relative = if path.to_string_lossy().starts_with("./") {
|
||||
in_place = true;
|
||||
path.strip_prefix("./").unwrap_or(&path).to_path_buf()
|
||||
} else {
|
||||
path
|
||||
};
|
||||
// 'package-version/' wrapper; strip the prefix so entries land there
|
||||
// too, and skip their root entry
|
||||
let relative = path.strip_prefix("./").unwrap_or(&path).to_path_buf();
|
||||
|
||||
if relative.as_os_str().is_empty() {
|
||||
continue;
|
||||
@@ -239,9 +275,51 @@ where
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
// Extract the file
|
||||
entry.unpack(&dest_path)?;
|
||||
extracted_files.push(dest_path.to_string_lossy().to_string());
|
||||
// A hard link shares its content with an earlier entry of the
|
||||
// archive: tar stores the target path relative to the archive root,
|
||||
// where the shared entry has already been extracted. Resolving it
|
||||
// any other way (as `Entry::unpack` does) misses the target and
|
||||
// aborts the whole extraction.
|
||||
if entry.header().entry_type().is_hard_link() {
|
||||
let link_target = entry.link_name()?.ok_or_else(|| {
|
||||
format!(
|
||||
"archive entry '{}' is a hard link without a link target",
|
||||
relative.display()
|
||||
)
|
||||
})?;
|
||||
// Absolute link targets land under the destination root, like
|
||||
// GNU tar treats them; anything else escaping 'dest' is refused
|
||||
let link_relative = link_target.strip_prefix("/").unwrap_or(&link_target);
|
||||
let escapes_dest = link_relative.components().any(|component| {
|
||||
matches!(
|
||||
component,
|
||||
Component::Prefix(_) | Component::RootDir | Component::ParentDir
|
||||
)
|
||||
});
|
||||
if escapes_dest {
|
||||
return Err(format!(
|
||||
"Refusing to extract '{}': hard link target '{}' is \
|
||||
absolute or contains '..' and would escape the \
|
||||
destination directory '{}'",
|
||||
relative.display(),
|
||||
link_relative.display(),
|
||||
dest.display()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let link_path = dest.join(link_relative);
|
||||
std::fs::hard_link(&link_path, &dest_path).map_err(|e| {
|
||||
format!(
|
||||
"hard linking archive entry '{}' to its target '{}' \
|
||||
failed: {e}",
|
||||
relative.display(),
|
||||
link_relative.display()
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
// Extract the entry
|
||||
entry.unpack(&dest_path)?;
|
||||
}
|
||||
|
||||
current_entry += 1;
|
||||
|
||||
@@ -251,23 +329,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExtractedArchive {
|
||||
files: extracted_files,
|
||||
in_place,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_archive(
|
||||
path: &Path,
|
||||
dest: &Path,
|
||||
progress: ProgressCallback<'_>,
|
||||
) -> Result<ExtractedArchive, Box<dyn Error>> {
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let filename = path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
if filename.ends_with(".tar.gz") || filename.ends_with(".tgz") {
|
||||
extract_tar_archive(path, dest, progress, GzDecoder::new)
|
||||
} else if filename.ends_with(".tar.xz") || filename.ends_with(".txz") {
|
||||
extract_tar_archive(path, dest, progress, XzDecoder::new)
|
||||
} else if filename.ends_with(".tar.bz2") || filename.ends_with(".tbz2") {
|
||||
extract_tar_archive(path, dest, progress, bzip2_rs::DecoderReader::new)
|
||||
} else {
|
||||
Err(format!("Unsupported archive format: {}", filename).into())
|
||||
}
|
||||
@@ -562,66 +639,36 @@ async fn fetch_archive_sources(
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Extract all tar archives, merging extracted directories
|
||||
if file.name.ends_with(".tar.gz") || file.name.ends_with(".tar.xz") {
|
||||
// Extract all tar archives. Every tar of the source package matches
|
||||
// (orig, debian tar, native tar, extra components); a tarball in an
|
||||
// unsupported compression fails loudly in extract_archive rather
|
||||
// than being left unextracted — a silently missing tree only
|
||||
// surfaces as warnings from dpkg-source and broken patches later.
|
||||
// Detached signatures ('.tar.gz.asc' and friends) are not tars.
|
||||
let is_tar = !file.name.ends_with(".asc")
|
||||
&& (file.name.contains(".tar.")
|
||||
|| file.name.ends_with(".tgz")
|
||||
|| file.name.ends_with(".tbz2"));
|
||||
if is_tar {
|
||||
let path = package_dir.join(&file.name);
|
||||
let extract_dir = package_dir.join(&info.stanza.package);
|
||||
|
||||
let extracted = extract_archive(&path, &extract_dir, progress)?;
|
||||
extract_archive(&path, &extract_dir, progress)?;
|
||||
|
||||
// Special case: the debian tar does only contain 'debian'
|
||||
if file.name.contains("debian.tar.") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Archives with './'-prefixed entries are already laid out
|
||||
// directly in the package directory; only 'package-version/'
|
||||
// style ones need their contents relocated
|
||||
if !extracted.in_place {
|
||||
// List root directories extracted and use the first one as the source directory
|
||||
debug!("Root directories extracted:");
|
||||
let mut source_dir: Option<PathBuf> = None;
|
||||
for file in &extracted.files {
|
||||
let path = Path::new(file);
|
||||
// Check if this is a directory and is at the archive root level
|
||||
// (i.e., the path relative to extract_dir has no parent components)
|
||||
if let Ok(relative_path) = path.strip_prefix(&extract_dir)
|
||||
&& relative_path.components().count() == 1
|
||||
&& path.is_dir()
|
||||
{
|
||||
debug!("- {}", relative_path.file_name().unwrap().to_string_lossy());
|
||||
// Use the first directory found as the source
|
||||
if source_dir.is_none() {
|
||||
source_dir = Some(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the extracted directory as the source, assuming there is only one
|
||||
if let Some(src_dir) = source_dir {
|
||||
let target_dir = package_dir.join(&info.stanza.package);
|
||||
|
||||
if target_dir.exists() {
|
||||
// Target exists, we need to merge contents
|
||||
for sub_entry in std::fs::read_dir(&src_dir)? {
|
||||
let sub_entry = sub_entry?;
|
||||
let sub_path = sub_entry.path();
|
||||
let target_path = target_dir.join(sub_entry.file_name());
|
||||
if sub_path.is_dir() {
|
||||
std::fs::create_dir_all(&target_path)?;
|
||||
// Recursively copy directory contents
|
||||
copy_dir_all(&sub_path, &target_path)?;
|
||||
} else {
|
||||
std::fs::copy(&sub_path, &target_path)?;
|
||||
copy_file_times(&sub_path, &target_path)?;
|
||||
}
|
||||
}
|
||||
std::fs::remove_dir_all(&src_dir)?;
|
||||
} else {
|
||||
std::fs::rename(&src_dir, &target_dir)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The tree root is the extraction directory itself: an archive
|
||||
// wrapped in a single 'package-version/' directory (the usual
|
||||
// upstream layout) has that wrapper unwrapped, anything else is
|
||||
// already laid out in place. The wrapper is recognized by the
|
||||
// shape of the extraction result rather than by tar directory
|
||||
// entries: tarballs are not required to carry entries for their
|
||||
// directories, and a missing entry used to leave the sources
|
||||
// nested one level too deep.
|
||||
unwrap_single_dir(&extract_dir)?;
|
||||
}
|
||||
|
||||
// Extract and apply .diff.gz if present (old packages)
|
||||
@@ -946,39 +993,54 @@ mod tests {
|
||||
fn build_tar(entries: &[(&str, &[u8])]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
for (name, data) in entries {
|
||||
let mut block = [0u8; 512];
|
||||
block[..name.len()].copy_from_slice(name.as_bytes());
|
||||
block[100..108].copy_from_slice(b"0000644\0");
|
||||
block[124..136].copy_from_slice(format!("{:011o}\0", data.len()).as_bytes());
|
||||
block[136..148].copy_from_slice(b"00000000000\0");
|
||||
block[156] = b'0'; // regular file
|
||||
block[257..263].copy_from_slice(b"ustar\0");
|
||||
block[263..265].copy_from_slice(b"00");
|
||||
|
||||
// Checksum: sum of the header bytes with the checksum field
|
||||
// (bytes 148..156) taken as spaces
|
||||
let mut checksum: u32 = 0;
|
||||
for (i, byte) in block.iter().enumerate() {
|
||||
checksum += if (148..156).contains(&i) {
|
||||
u32::from(b' ')
|
||||
} else {
|
||||
u32::from(*byte)
|
||||
};
|
||||
}
|
||||
block[148..154].copy_from_slice(format!("{checksum:06o}").as_bytes());
|
||||
block[154] = 0;
|
||||
block[155] = b' ';
|
||||
|
||||
out.extend_from_slice(&block);
|
||||
out.extend_from_slice(data);
|
||||
let padding = (512 - (data.len() % 512)) % 512;
|
||||
out.extend_from_slice(&vec![0u8; padding]);
|
||||
push_tar_entry(&mut out, name, b'0', data, None);
|
||||
}
|
||||
// End-of-archive marker: two zero-filled blocks
|
||||
out.extend_from_slice(&[0u8; 1024]);
|
||||
out
|
||||
}
|
||||
|
||||
/// Append one raw ustar entry: `typeflag` selects the type ('0' regular
|
||||
/// file, '1' hard link, '2' symlink) and `linkname` is the stored link
|
||||
/// target for the link types.
|
||||
fn push_tar_entry(
|
||||
out: &mut Vec<u8>,
|
||||
name: &str,
|
||||
typeflag: u8,
|
||||
data: &[u8],
|
||||
linkname: Option<&str>,
|
||||
) {
|
||||
let mut block = [0u8; 512];
|
||||
block[..name.len()].copy_from_slice(name.as_bytes());
|
||||
block[100..108].copy_from_slice(b"0000644\0");
|
||||
block[124..136].copy_from_slice(format!("{:011o}\0", data.len()).as_bytes());
|
||||
block[136..148].copy_from_slice(b"00000000000\0");
|
||||
block[156] = typeflag;
|
||||
if let Some(linkname) = linkname {
|
||||
block[157..157 + linkname.len()].copy_from_slice(linkname.as_bytes());
|
||||
}
|
||||
block[257..263].copy_from_slice(b"ustar\0");
|
||||
block[263..265].copy_from_slice(b"00");
|
||||
|
||||
// Checksum: sum of the header bytes with the checksum field
|
||||
// (bytes 148..156) taken as spaces
|
||||
let mut checksum: u32 = 0;
|
||||
for (i, byte) in block.iter().enumerate() {
|
||||
checksum += if (148..156).contains(&i) {
|
||||
u32::from(b' ')
|
||||
} else {
|
||||
u32::from(*byte)
|
||||
};
|
||||
}
|
||||
block[148..154].copy_from_slice(format!("{checksum:06o}").as_bytes());
|
||||
block[154] = 0;
|
||||
block[155] = b' ';
|
||||
|
||||
out.extend_from_slice(&block);
|
||||
out.extend_from_slice(data);
|
||||
let padding = (512 - (data.len() % 512)) % 512;
|
||||
out.extend_from_slice(&vec![0u8; padding]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_tar_rejects_parent_dir_traversal() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
@@ -1056,7 +1118,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let dest = temp_dir.path().join("dest");
|
||||
let extracted = extract_tar_archive(&tar_path, &dest, None, |f| f).unwrap();
|
||||
extract_tar_archive(&tar_path, &dest, None, |f| f).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(dest.join("hello-1.0/file.txt")).unwrap(),
|
||||
@@ -1066,19 +1128,192 @@ mod tests {
|
||||
std::fs::read_to_string(dest.join("debian/rules")).unwrap(),
|
||||
"#!/m"
|
||||
);
|
||||
assert!(extracted.in_place);
|
||||
assert!(
|
||||
extracted.files.contains(
|
||||
&dest
|
||||
.join("hello-1.0/file.txt")
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_tar_hardlink_resolved_against_archive_root() {
|
||||
// Regression: a hard link entry (e.g. the jq orig tarball) aborted
|
||||
// the whole extraction when its target was not found where the
|
||||
// extractor resolved it; tar stores link targets relative to the
|
||||
// archive root
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let tar_path = temp_dir.path().join("hardlink.orig.tar");
|
||||
let mut tar_bytes = Vec::new();
|
||||
push_tar_entry(&mut tar_bytes, "pkg-1.0/manual.txt", b'0', b"content", None);
|
||||
push_tar_entry(
|
||||
&mut tar_bytes,
|
||||
"pkg-1.0/docs/v1/manual.txt",
|
||||
b'1',
|
||||
b"",
|
||||
Some("pkg-1.0/manual.txt"),
|
||||
);
|
||||
assert!(
|
||||
extracted
|
||||
.files
|
||||
.contains(&dest.join("debian/rules").to_string_lossy().to_string())
|
||||
tar_bytes.extend_from_slice(&[0u8; 1024]);
|
||||
std::fs::write(&tar_path, tar_bytes).unwrap();
|
||||
|
||||
let dest = temp_dir.path().join("dest");
|
||||
extract_tar_archive(&tar_path, &dest, None, |f| f).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(dest.join("pkg-1.0/docs/v1/manual.txt")).unwrap(),
|
||||
"content"
|
||||
);
|
||||
// A real hard link shares the inode with its target
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
assert_eq!(
|
||||
dest.join("pkg-1.0/manual.txt").metadata().unwrap().ino(),
|
||||
dest.join("pkg-1.0/docs/v1/manual.txt")
|
||||
.metadata()
|
||||
.unwrap()
|
||||
.ino()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_tar_rejects_hardlink_escape() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let tar_path = temp_dir.path().join("malicious.orig.tar");
|
||||
let mut tar_bytes = Vec::new();
|
||||
push_tar_entry(&mut tar_bytes, "good.txt", b'0', b"ok", None);
|
||||
push_tar_entry(&mut tar_bytes, "evil.txt", b'1', b"", Some("../evil.txt"));
|
||||
tar_bytes.extend_from_slice(&[0u8; 1024]);
|
||||
std::fs::write(&tar_path, tar_bytes).unwrap();
|
||||
|
||||
let dest = temp_dir.path().join("dest");
|
||||
let result = extract_tar_archive(&tar_path, &dest, None, |f| f);
|
||||
|
||||
let err = match result {
|
||||
Ok(_) => panic!("extraction of an escaping hard link should fail"),
|
||||
Err(e) => e,
|
||||
};
|
||||
assert!(
|
||||
err.to_string().contains("../evil.txt"),
|
||||
"error should name the offending link target, got: {err}"
|
||||
);
|
||||
assert!(!temp_dir.path().join("evil.txt").exists());
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(dest.join("good.txt")).unwrap(),
|
||||
"ok"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unwrap_single_dir_without_dir_entries() {
|
||||
// Regression: tarballs carrying no explicit entries for their
|
||||
// directories (e.g. the 'tree' orig) used to skip the relocation
|
||||
// entirely, leaving the sources nested one level too deep and
|
||||
// dpkg-source rejecting the tree as 'unexpected upstream changes'
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let tar_path = temp_dir.path().join("wrapped.orig.tar");
|
||||
std::fs::write(
|
||||
&tar_path,
|
||||
build_tar(&[
|
||||
("tree-2.3.2/Makefile", b"all:"),
|
||||
("tree-2.3.2/doc/tree.1", b"th"),
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tree = temp_dir.path().join("tree");
|
||||
extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap();
|
||||
unwrap_single_dir(&tree).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(tree.join("Makefile")).unwrap(),
|
||||
"all:"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(tree.join("doc/tree.1")).unwrap(),
|
||||
"th"
|
||||
);
|
||||
assert!(!tree.join("tree-2.3.2").exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unwrap_single_dir_preserves_symlinks() {
|
||||
// Regression: the copy-based relocation used to flatten top-level
|
||||
// symlinks into plain files (dpkg-source: 'cannot represent change
|
||||
// to ...: old version is symlink')
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let tar_path = temp_dir.path().join("wrapped.orig.tar");
|
||||
let mut tar_bytes = Vec::new();
|
||||
push_tar_entry(&mut tar_bytes, "pkg-1.0/README.md", b'0', b"readme", None);
|
||||
push_tar_entry(
|
||||
&mut tar_bytes,
|
||||
"pkg-1.0/README",
|
||||
b'2',
|
||||
b"",
|
||||
Some("README.md"),
|
||||
);
|
||||
tar_bytes.extend_from_slice(&[0u8; 1024]);
|
||||
std::fs::write(&tar_path, tar_bytes).unwrap();
|
||||
|
||||
let tree = temp_dir.path().join("tree");
|
||||
extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap();
|
||||
unwrap_single_dir(&tree).unwrap();
|
||||
|
||||
let meta = std::fs::symlink_metadata(tree.join("README")).unwrap();
|
||||
assert!(meta.file_type().is_symlink());
|
||||
assert_eq!(
|
||||
std::fs::read_link(tree.join("README")).unwrap(),
|
||||
Path::new("README.md")
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(tree.join("README")).unwrap(),
|
||||
"readme"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unwrap_single_dir_leaves_flat_layout() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let tar_path = temp_dir.path().join("flat.orig.tar");
|
||||
std::fs::write(
|
||||
&tar_path,
|
||||
build_tar(&[("Makefile", b"all:"), ("debian/rules", b"#!/m")]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tree = temp_dir.path().join("tree");
|
||||
extract_tar_archive(&tar_path, &tree, None, |f| f).unwrap();
|
||||
unwrap_single_dir(&tree).unwrap();
|
||||
|
||||
assert!(tree.join("Makefile").exists());
|
||||
assert!(tree.join("debian/rules").exists());
|
||||
}
|
||||
|
||||
/// A minimal ustar archive of `pkg-1.0/{Makefile,doc/readme}`,
|
||||
/// bz2-compressed.
|
||||
const BZ2_TAR_WRAPPED: &[u8] = &[
|
||||
66, 90, 104, 57, 49, 65, 89, 38, 83, 89, 88, 108, 110, 100, 0, 0, 154, 255, 128, 201, 128,
|
||||
0, 16, 64, 3, 231, 144, 0, 3, 64, 0, 111, 174, 222, 0, 8, 40, 32, 0, 116, 18, 148, 218,
|
||||
147, 200, 67, 33, 166, 100, 70, 65, 250, 83, 212, 18, 145, 1, 166, 128, 0, 0, 208, 62, 196,
|
||||
104, 44, 169, 64, 3, 160, 68, 65, 10, 40, 112, 177, 128, 161, 163, 8, 68, 16, 80, 10, 79,
|
||||
36, 5, 53, 192, 112, 39, 81, 227, 129, 9, 73, 68, 32, 93, 136, 243, 37, 49, 35, 19, 133,
|
||||
118, 14, 245, 37, 8, 233, 250, 99, 27, 194, 182, 183, 86, 186, 163, 237, 147, 31, 53, 154,
|
||||
53, 20, 231, 189, 6, 40, 100, 34, 7, 241, 119, 36, 83, 133, 9, 5, 134, 198, 230, 64,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_extract_tar_bz2() {
|
||||
// Regression: '.tar.bz2' origs (e.g. busybox) were downloaded but
|
||||
// never extracted — the dispatch only knew gz and xz — leaving a
|
||||
// tree that dpkg-source packed as a mass of ignored deletions
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let tar_path = temp_dir.path().join("wrapped.orig.tar.bz2");
|
||||
std::fs::write(&tar_path, BZ2_TAR_WRAPPED).unwrap();
|
||||
|
||||
let tree = temp_dir.path().join("tree");
|
||||
extract_tar_archive(&tar_path, &tree, None, bzip2_rs::DecoderReader::new).unwrap();
|
||||
unwrap_single_dir(&tree).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(tree.join("Makefile")).unwrap(),
|
||||
"all:"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(tree.join("doc/readme")).unwrap(),
|
||||
"docs"
|
||||
);
|
||||
assert!(!tree.join("pkg-1.0").exists());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user