1
0
Fork 0
mirror of https://github.com/imjasonh/oci2squashfs synced 2026-07-07 00:33:35 +00:00

implement tests for canonical.rs and layers.rs

Signed-off-by: Steven Noonan <steven@edera.dev>
This commit is contained in:
Steven Noonan 2026-03-11 17:40:25 -07:00
parent b0b23b889b
commit db70b4907e
No known key found for this signature in database
GPG key ID: 408EEB508ED0CD4D

View file

@ -0,0 +1,202 @@
//! Direct tests for canonical.rs and layers.rs.
//!
//! canonical.rs: focuses on write_hardlink_to_tar's PAX extension ordering —
//! the property that linkpath lands on the main entry, not on a GNU LongName
//! auxiliary entry that would consume the extensions before the reader sees
//! the hardlink itself.
//!
//! layers.rs: the unsupported media type error path in open_layer.
use oci2squashfs::canonical::CanonicalTarHeader;
use tar::{Builder, EntryType};
use tempfile::NamedTempFile;
// ── helpers ───────────────────────────────────────────────────────────────────
/// Build a tar containing a single hardlink entry via `write_hardlink_to_tar`
/// and return the raw bytes.
fn hardlink_tar(link_path: &str, target_path: &str) -> Vec<u8> {
// Build a minimal CanonicalTarHeader representing the inode metadata.
// Entry type and link fields will be overwritten by write_hardlink_to_tar.
let mut src_header = tar::Header::new_ustar();
src_header.set_entry_type(EntryType::Regular);
src_header.set_size(0);
src_header.set_mode(0o644);
src_header.set_mtime(0);
src_header.set_uid(0);
src_header.set_gid(0);
src_header.set_path("placeholder").unwrap();
src_header.set_cksum();
let canonical = CanonicalTarHeader {
header: src_header,
pax_extensions: vec![],
};
let mut out = Vec::new();
let mut builder = Builder::new(&mut out);
canonical
.write_hardlink_to_tar(
std::path::Path::new(link_path),
std::path::Path::new(target_path),
&mut builder,
)
.expect("write_hardlink_to_tar must succeed");
builder.finish().unwrap();
drop(builder);
out
}
/// Read the first non-PAX entry from a tar and return its CanonicalTarHeader.
fn first_main_entry(tar_bytes: &[u8]) -> CanonicalTarHeader {
let mut archive = tar::Archive::new(tar_bytes);
for mut entry in archive.entries().unwrap().flatten() {
let et = entry.header().entry_type();
if matches!(et, EntryType::XHeader | EntryType::XGlobalHeader) {
continue;
}
return CanonicalTarHeader::from_entry(&mut entry).unwrap();
}
panic!("no main entry found in tar");
}
// ── canonical.rs: write_hardlink_to_tar ──────────────────────────────────────
/// Short paths (≤ 100 bytes) must produce a hardlink entry whose link_name()
/// returns the correct target without any PAX extensions being required.
#[test]
fn write_hardlink_short_paths_roundtrip() {
let tar = hardlink_tar("link.txt", "target.txt");
let entry = first_main_entry(&tar);
assert_eq!(entry.entry_type(), EntryType::Link);
assert_eq!(
entry.path().unwrap().to_string_lossy(),
"link.txt",
"link path must round-trip"
);
assert_eq!(
entry.link_name().unwrap().unwrap().to_string_lossy(),
"target.txt",
"link target must round-trip"
);
}
/// A long link path (> 100 bytes) must round-trip correctly via the PAX `path`
/// extension and must land on the main hardlink entry, not on a GNU LongName
/// auxiliary entry that would be invisible to PAX-aware readers.
#[test]
fn write_hardlink_long_link_path_pax_roundtrip() {
let long_link: String = "l".repeat(110);
let tar = hardlink_tar(&long_link, "short_target.txt");
let entry = first_main_entry(&tar);
assert_eq!(
entry.entry_type(),
EntryType::Link,
"entry must be a hardlink"
);
assert_eq!(
entry.path().unwrap().to_string_lossy(),
long_link.as_str(),
"long link path must round-trip via PAX path extension"
);
assert_eq!(
entry.link_name().unwrap().unwrap().to_string_lossy(),
"short_target.txt",
);
}
/// A long target path (> 100 bytes) must round-trip correctly via the PAX
/// `linkpath` extension on the main entry. This is the exact failure mode
/// that was the production bug: the linkpath extension was being consumed by
/// the GNU LongName auxiliary entry instead of the hardlink entry.
#[test]
fn write_hardlink_long_target_path_pax_roundtrip() {
let long_target: String = "t".repeat(110);
let tar = hardlink_tar("link.txt", &long_target);
let entry = first_main_entry(&tar);
assert_eq!(
entry.entry_type(),
EntryType::Link,
"entry must be a hardlink"
);
assert_eq!(
entry.link_name().unwrap().unwrap().to_string_lossy(),
long_target.as_str(),
"long target path must round-trip via PAX linkpath extension on the main entry, \
not consumed by a GNU LongName auxiliary entry"
);
}
/// Both link path and target path long: both PAX extensions must land on the
/// main entry.
#[test]
fn write_hardlink_both_paths_long_pax_roundtrip() {
let long_link: String = "l".repeat(110);
let long_target: String = "t".repeat(110);
let tar = hardlink_tar(&long_link, &long_target);
let entry = first_main_entry(&tar);
assert_eq!(entry.entry_type(), EntryType::Link);
assert_eq!(
entry.path().unwrap().to_string_lossy(),
long_link.as_str(),
"long link path must survive"
);
assert_eq!(
entry.link_name().unwrap().unwrap().to_string_lossy(),
long_target.as_str(),
"long target path must survive"
);
}
/// Verify that the PAX extensions appear on the main entry by counting entry
/// types in the raw tar. A GNU LongName auxiliary entry (type 'L') must not
/// appear when paths are handled via PAX.
#[test]
fn write_hardlink_long_paths_produce_no_gnu_longname_entry() {
let long_link: String = "l".repeat(110);
let long_target: String = "t".repeat(110);
let tar = hardlink_tar(&long_link, &long_target);
let mut archive = tar::Archive::new(tar.as_slice());
for entry in archive.entries().unwrap().flatten() {
assert_ne!(
entry.header().entry_type(),
EntryType::GNULongName,
"GNU LongName auxiliary entry must not be emitted when PAX extensions are used; \
its presence would cause linkpath to be consumed by the wrong entry"
);
}
}
// ── layers.rs: open_layer ─────────────────────────────────────────────────────
#[test]
fn open_layer_unsupported_media_type_returns_error() {
let f = NamedTempFile::new().unwrap();
let result = oci2squashfs::layers::open_layer(f.path(), "application/vnd.edera.custom+lz4");
assert!(
result.is_err(),
"unsupported media type must return an error"
);
let err = result.err().unwrap();
let msg = err.to_string();
assert!(
msg.contains("unsupported layer media type"),
"error must identify the problem; got: {msg}"
);
assert!(
msg.contains("application/vnd.edera.custom+lz4"),
"error must include the bad media type string; got: {msg}"
);
}
#[test]
fn open_layer_empty_media_type_returns_error() {
let f = NamedTempFile::new().unwrap();
let result = oci2squashfs::layers::open_layer(f.path(), "");
assert!(result.is_err(), "empty media type must return an error");
}