mirror of
https://github.com/imjasonh/oci2squashfs
synced 2026-07-08 00:55:51 +00:00
implement tests for canonical.rs and layers.rs
Signed-off-by: Steven Noonan <steven@edera.dev>
This commit is contained in:
parent
b0b23b889b
commit
db70b4907e
1 changed files with 202 additions and 0 deletions
202
tests/canonical_and_layers.rs
Normal file
202
tests/canonical_and_layers.rs
Normal 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");
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue