mod support;
use support::{container, metadata, open, payload_of, raw_zip, Member};
use slpc::{EntryKind, Error, Malformed, NameError, Unsupported, METADATA_MEMBER};
#[test]
fn reads_a_container() {
let bytes = container("report.pdf", b"%PDF-1.7 not really\n");
let mut c = open(&bytes).unwrap();
assert_eq!(c.version(), "1.0");
assert_eq!(c.payload_name(), "report.pdf");
assert_eq!(payload_of(&mut c), b"%PDF-1.7 not really\n");
}
#[test]
fn reads_a_deflated_payload() {
let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let opts = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
let payload: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
w.start_file(METADATA_MEMBER, opts).unwrap();
std::io::Write::write_all(&mut w, metadata("big.bin").as_bytes()).unwrap();
w.start_file("big.bin", opts).unwrap();
std::io::Write::write_all(&mut w, &payload).unwrap();
let bytes = w.finish().unwrap().into_inner();
let mut c = open(&bytes).unwrap();
assert_eq!(c.payload_name(), "big.bin");
assert_eq!(payload_of(&mut c), payload);
}
#[test]
fn passes_through_keys_and_members_it_does_not_recognise() {
let meta = "slipcase_version = \"1.0\"\ntitle = \"Q3\"\n\n[payload]\nfile = \"a.txt\"\n\n[custom]\nnested = { deep = [1, 2] }\n";
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, meta.as_bytes()),
Member::new("a.txt", b"payload\n"),
Member::new("__MACOSX/._a.txt", b"junk"),
Member::new(".DS_Store", b"junk"),
]);
let c = open(&bytes).unwrap();
assert_eq!(c.metadata()["title"].as_str(), Some("Q3"));
assert!(c.metadata()["custom"]["nested"]["deep"].is_array());
}
#[test]
fn metadata_bytes_are_the_member_as_stored() {
let meta =
"# hand written\nslipcase_version = \"1.0\"\n\n[payload]\nfile = \"a.txt\" # kept\n";
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, meta.as_bytes()),
Member::new("a.txt", b"x"),
]);
let c = open(&bytes).unwrap();
assert_eq!(c.metadata_bytes(), meta.as_bytes());
assert_eq!(c.metadata().to_string(), meta);
}
#[test]
fn member_order_does_not_matter() {
let payload_first = raw_zip(&[
Member::new("a.txt", b"payload\n"),
Member::new(METADATA_MEMBER, metadata("a.txt").as_bytes()),
]);
let mut c = open(&payload_first).unwrap();
assert_eq!(payload_of(&mut c), b"payload\n");
}
#[test]
fn rejects_an_archive_with_no_metadata_member() {
let bytes = raw_zip(&[Member::new("a.txt", b"lonely")]);
assert!(matches!(
open(&bytes),
Err(Error::Malformed(Malformed::NoMetadataMember))
));
}
#[test]
fn rejects_metadata_that_is_not_utf8() {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, b"slipcase_version = \"\xff\xfe\"\n"),
Member::new("a.txt", b"x"),
]);
assert!(matches!(
open(&bytes),
Err(Error::Malformed(Malformed::MetadataNotUtf8))
));
}
#[test]
fn rejects_metadata_that_is_not_toml() {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, b"this is not = = toml\n"),
Member::new("a.txt", b"x"),
]);
assert!(matches!(
open(&bytes),
Err(Error::Malformed(Malformed::MetadataNotToml(_)))
));
}
#[test]
fn rejects_metadata_missing_either_required_key() {
for (meta, missing) in [
("[payload]\nfile = \"a.txt\"\n", "slipcase_version"),
("slipcase_version = \"1.0\"\n", "payload.file"),
("slipcase_version = \"1.0\"\n[payload]\n", "payload.file"),
] {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, meta.as_bytes()),
Member::new("a.txt", b"x"),
]);
match open(&bytes) {
Err(Error::Malformed(Malformed::MissingKey(k))) => assert_eq!(k, missing),
other => panic!(
"expected MissingKey({missing}), got {other:?}",
other = other.err()
),
}
}
}
#[test]
fn rejects_required_keys_that_are_not_strings() {
for (meta, key) in [
(
"slipcase_version = 1.0\n[payload]\nfile = \"a.txt\"\n",
"slipcase_version",
),
(
"slipcase_version = \"1.0\"\n[payload]\nfile = 7\n",
"payload.file",
),
] {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, meta.as_bytes()),
Member::new("a.txt", b"x"),
]);
match open(&bytes) {
Err(Error::Malformed(Malformed::KeyNotAString(k))) => assert_eq!(k, key),
other => panic!(
"expected KeyNotAString({key}), got {other:?}",
other = other.err()
),
}
}
}
#[test]
fn rejects_a_payload_file_that_is_not_a_plain_filename() {
for (name, want) in [
("", NameError::Empty),
(".", NameError::Relative),
("..", NameError::Relative),
("../etc/passwd", NameError::Separator('/')),
("..\\windows", NameError::Separator('\\')),
("C:evil", NameError::Colon),
(METADATA_MEMBER, NameError::ReservedForMetadata),
] {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata(name).as_bytes()),
Member::new("a.txt", b"x"),
]);
match open(&bytes) {
Err(Error::Malformed(Malformed::PayloadName(e))) => assert_eq!(e, want, "{name:?}"),
other => panic!(
"expected PayloadName({want:?}) for {name:?}, got {:?}",
other.err()
),
}
}
}
#[test]
fn rejects_a_payload_file_that_names_nothing() {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("absent.txt").as_bytes()),
Member::new("present.txt", b"x"),
]);
match open(&bytes) {
Err(Error::Malformed(Malformed::NoPayloadMember(n))) => assert_eq!(n, "absent.txt"),
other => panic!("expected NoPayloadMember, got {:?}", other.err()),
}
}
#[test]
fn rejects_a_payload_that_is_not_a_regular_file_entry() {
for (mode, want) in [
(0o120_777, EntryKind::Symlink),
(0o040_755, EntryKind::Directory),
(0o010_644, EntryKind::Other(0o1)),
(0o140_644, EntryKind::Other(0o14)),
(0o020_644, EntryKind::Other(0o2)),
] {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("odd").as_bytes()),
Member::new("odd", b"payload").with_mode(mode),
]);
match open(&bytes) {
Err(Error::Malformed(Malformed::PayloadNotARegularFile { kind, .. })) => {
assert_eq!(kind, want, "mode {mode:o}");
}
other => panic!(
"mode {mode:o}: expected PayloadNotARegularFile, got {:?}",
other.err()
),
}
}
}
#[test]
fn rejects_more_than_one_member_of_either_name() {
let two_metadata = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("a.txt").as_bytes()),
Member::new(METADATA_MEMBER, metadata("a.txt").as_bytes()),
Member::new("a.txt", b"x"),
]);
match open(&two_metadata) {
Err(Error::Malformed(Malformed::DuplicateMetadataMember(n))) => assert_eq!(n, 2),
other => panic!("expected DuplicateMetadataMember, got {:?}", other.err()),
}
let two_payloads = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("a.txt").as_bytes()),
Member::new("a.txt", b"first"),
Member::new("a.txt", b"second"),
]);
match open(&two_payloads) {
Err(Error::Malformed(Malformed::DuplicatePayloadMember { name, count })) => {
assert_eq!((name.as_str(), count), ("a.txt", 2));
}
other => panic!("expected DuplicatePayloadMember, got {:?}", other.err()),
}
}
#[test]
fn rejects_a_payload_file_containing_a_control_character() {
for c in ['\u{0}', '\n', '\r', '\u{1f}', '\u{7f}'] {
let name = format!("rep{c}ort.pdf");
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata(&name).as_bytes()),
Member::new(&name, b"x"),
]);
match open(&bytes) {
Err(Error::Malformed(Malformed::PayloadName(NameError::ControlCharacter(got)))) => {
assert_eq!(got, c);
}
other => panic!("U+{:04X}: got {:?}", c as u32, other.err()),
}
}
}
#[test]
fn an_entry_made_on_dos_is_not_taken_for_a_symlink() {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("a.txt").as_bytes()),
Member::new("a.txt", b"from windows\n").dos_made(),
]);
let mut c = open(&bytes).unwrap();
assert_eq!(payload_of(&mut c), b"from windows\n");
}
#[test]
fn matches_a_name_stored_as_cp437() {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("caf\u{e7}.txt").as_bytes()),
Member::named_raw(b"caf\x87.txt", b"cp437\n"),
]);
let mut c = open(&bytes).unwrap();
assert_eq!(c.payload_name(), "caf\u{e7}.txt");
assert_eq!(payload_of(&mut c), b"cp437\n");
}
#[test]
fn never_matches_a_name_the_crate_decoded_lossily() {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("caf\u{fffd}.txt").as_bytes()),
Member::named_raw(b"caf\xff.txt", b"impostor\n").flagged_utf8(),
]);
match open(&bytes) {
Err(Error::Malformed(Malformed::NoPayloadMember(n))) => assert_eq!(n, "caf\u{fffd}.txt"),
other => panic!("expected NoPayloadMember, got {:?}", other.err()),
}
}
#[test]
fn an_unrecognised_version_parses_and_reports_but_yields_no_payload() {
let meta = "slipcase_version = \"9.4\"\n\n[payload]\nfile = \"a.txt\"\n";
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, meta.as_bytes()),
Member::new("a.txt", b"x"),
]);
let mut c = open(&bytes).unwrap();
assert_eq!(c.version(), "9.4");
assert_eq!(c.payload_name(), "a.txt");
assert_eq!(c.metadata_bytes(), meta.as_bytes());
let got = c.payload();
match got {
Err(Error::Unsupported(Unsupported::Version(v))) => assert_eq!(v, "9.4"),
other => panic!("expected Unsupported::Version, got {:?}", other.err()),
}
}
#[test]
fn a_payload_compressed_beyond_this_build_still_validates() {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("a.txt").as_bytes()),
Member::new("a.txt", b"pretend this is bzip2").claims_method(12),
]);
slpc::validate(std::io::Cursor::new(bytes.clone())).unwrap();
let mut c = open(&bytes).unwrap();
let got = c.payload();
match got {
Err(Error::Unsupported(Unsupported::Compression(m))) => assert_eq!(m, 12),
other => panic!("expected Unsupported::Compression, got {:?}", other.err()),
}
}
#[test]
fn an_encrypted_payload_still_validates() {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("a.txt").as_bytes()),
Member::new("a.txt", b"ciphertext").encrypted(),
]);
slpc::validate(std::io::Cursor::new(bytes.clone())).unwrap();
let mut c = open(&bytes).unwrap();
let got = c.payload();
assert!(matches!(
got,
Err(Error::Unsupported(Unsupported::Encrypted))
));
}
#[test]
fn a_container_may_be_its_own_payload() {
let inner = container("report.pdf", b"inner\n");
let outer = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("report.pdf.slpc").as_bytes()),
Member::new("report.pdf.slpc", &inner),
]);
let mut c = open(&outer).unwrap();
let nested = payload_of(&mut c);
assert_eq!(nested, inner);
let mut c = open(&nested).unwrap();
assert_eq!(c.payload_name(), "report.pdf");
assert_eq!(payload_of(&mut c), b"inner\n");
}
#[test]
fn reports_the_payloads_uncompressed_size() {
let payload = b"%PDF-1.7 not really\n";
let c = open(&container("report.pdf", payload)).unwrap();
assert_eq!(c.payload_size().unwrap(), payload.len() as u64);
}
#[test]
fn the_name_and_the_size_can_be_asked_for_together() {
let c = open(&container("report.pdf", b"1234")).unwrap();
assert_eq!(
format!(
"{} is {} bytes",
c.payload_name(),
c.payload_size().unwrap()
),
"report.pdf is 4 bytes"
);
}
#[test]
fn a_payload_of_zero_length_has_a_size_and_not_an_error() {
let c = open(&container("empty.bin", b"")).unwrap();
assert_eq!(c.payload_size().unwrap(), 0);
}
#[test]
fn the_size_is_the_uncompressed_one() {
let text = "a".repeat(4096);
let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let opts: zip::write::FileOptions<'_, ()> =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
w.start_file(METADATA_MEMBER, opts).unwrap();
std::io::Write::write_all(&mut w, metadata("big.txt").as_bytes()).unwrap();
w.start_file("big.txt", opts).unwrap();
std::io::Write::write_all(&mut w, text.as_bytes()).unwrap();
let bytes = w.finish().unwrap().into_inner();
let c = open(&bytes).unwrap();
assert_eq!(c.payload_size().unwrap(), 4096);
assert!(
bytes.len() < 2048,
"the fixture did not compress, so this proves nothing"
);
}
#[test]
fn an_unrecognized_version_has_no_payload_to_size() {
let doc = "slipcase_version = \"9.4\"\n\n[payload]\nfile = \"report.pdf\"\n";
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, doc.as_bytes()),
Member::new("report.pdf", b"x"),
]);
let c = open(&bytes).unwrap();
assert!(matches!(
c.payload_size(),
Err(Error::Unsupported(Unsupported::Version(v))) if v == "9.4"
));
}
fn metadata_of(bytes: &[u8]) -> slpc::Result<slpc::toml_edit::DocumentMut> {
slpc::metadata_of(std::io::Cursor::new(bytes.to_vec()))
}
#[test]
fn hands_back_the_document_of_a_conformant_container() {
let doc = metadata_of(&container("report.pdf", b"x")).unwrap();
assert_eq!(doc["payload"]["file"].as_str(), Some("report.pdf"));
}
#[test]
fn hands_back_the_document_when_payload_file_names_no_member() {
let bytes = raw_zip(&[Member::new(
METADATA_MEMBER,
metadata("absent.pdf").as_bytes(),
)]);
assert!(matches!(
open(&bytes),
Err(Error::Malformed(Malformed::NoPayloadMember(_)))
));
let doc = metadata_of(&bytes).unwrap();
assert_eq!(doc["payload"]["file"].as_str(), Some("absent.pdf"));
}
#[test]
fn hands_back_the_document_when_a_required_key_is_absent() {
let bytes = raw_zip(&[Member::new(
METADATA_MEMBER,
b"title = \"a document with no version key\"\n",
)]);
assert!(matches!(
open(&bytes),
Err(Error::Malformed(Malformed::MissingKey(_)))
));
let doc = metadata_of(&bytes).unwrap();
assert_eq!(
doc["title"].as_str(),
Some("a document with no version key")
);
}
#[test]
fn hands_back_the_document_when_payload_file_is_a_path() {
let bytes = raw_zip(&[Member::new(
METADATA_MEMBER,
metadata("../etc/passwd").as_bytes(),
)]);
assert!(matches!(
open(&bytes),
Err(Error::Malformed(Malformed::PayloadName(
NameError::Separator('/')
)))
));
assert_eq!(
metadata_of(&bytes).unwrap()["payload"]["file"].as_str(),
Some("../etc/passwd")
);
}
#[test]
fn keeps_comments_and_key_order() {
let doc = "# who owns this\nslipcase_version = \"1.0\"\nzzz = 1\naaa = 2\n\n[payload]\nfile = \"absent.pdf\"\n";
let bytes = raw_zip(&[Member::new(METADATA_MEMBER, doc.as_bytes())]);
assert_eq!(metadata_of(&bytes).unwrap().to_string(), doc);
}
#[test]
fn refuses_what_spec_2_2_requires_of_the_member_itself() {
let no_member = raw_zip(&[Member::new("report.pdf", b"x")]);
assert!(matches!(
metadata_of(&no_member),
Err(Error::Malformed(Malformed::NoMetadataMember))
));
let two = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("a.txt").as_bytes()),
Member::new(METADATA_MEMBER, metadata("b.txt").as_bytes()),
]);
assert!(matches!(
metadata_of(&two),
Err(Error::Malformed(Malformed::DuplicateMetadataMember(2)))
));
let not_toml = raw_zip(&[Member::new(METADATA_MEMBER, b"= not a document\n")]);
assert!(matches!(
metadata_of(¬_toml),
Err(Error::Malformed(Malformed::MetadataNotToml(_)))
));
let not_utf8 = raw_zip(&[Member::new(METADATA_MEMBER, b"title = \"\xff\xfe\"\n")]);
assert!(matches!(
metadata_of(¬_utf8),
Err(Error::Malformed(Malformed::MetadataNotUtf8))
));
}