mod support;
use std::io::Read;
use support::{container, metadata, open, raw_zip, Member};
use slpc::{Error, Malformed, NameError, Unsupported, METADATA_MEMBER};
fn payload_of(c: &mut slpc::Container<std::io::Cursor<Vec<u8>>>) -> Vec<u8> {
let mut got = Vec::new();
c.payload().unwrap().read_to_end(&mut got).unwrap();
got
}
#[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_a_symlink_entry() {
let bytes = raw_zip(&[
Member::new(METADATA_MEMBER, metadata("link.txt").as_bytes()),
Member::new("link.txt", b"/etc/passwd").symlink(),
]);
assert!(matches!(
open(&bytes),
Err(Error::Malformed(Malformed::PayloadIsSymlink(_)))
));
}
#[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");
}