use a7n_proto::v1::FileEntry;
use super::*;
fn file(path: &str, size: u64) -> FileEntry {
FileEntry {
path: path.to_owned(),
kind: FileKind::File.into(),
size,
sha256: vec![7; 32],
..Default::default()
}
}
fn directory(path: &str) -> FileEntry {
FileEntry {
path: path.to_owned(),
kind: FileKind::Directory.into(),
..Default::default()
}
}
#[test]
fn manifest_path_unsafe_paths_are_rejected() {
let cases = [
("", PathError::Empty),
("/root", PathError::Absolute),
("a\\b", PathError::Backslash),
("C:relative", PathError::DrivePrefix),
("a//b", PathError::EmptySegment),
("a/", PathError::EmptySegment),
("a/./b", PathError::DotSegment),
("a/../b", PathError::DotSegment),
("a/\0b", PathError::Control),
("a/\u{85}", PathError::Control),
("e\u{301}.txt", PathError::NotNfc),
("a/coN.txt", PathError::Reserved),
("prn", PathError::Reserved),
("Aux", PathError::Reserved),
("NUL.txt", PathError::Reserved),
("com1", PathError::Reserved),
("COM9.txt", PathError::Reserved),
("lpt1", PathError::Reserved),
("LPT9.txt", PathError::Reserved),
("COM¹.txt", PathError::Reserved),
("com²", PathError::Reserved),
("Com³", PathError::Reserved),
("LPT¹", PathError::Reserved),
("lpt².txt", PathError::Reserved),
("Lpt³", PathError::Reserved),
("a./b", PathError::TrailingDotOrSpace),
("a /b", PathError::TrailingDotOrSpace),
("a<b", PathError::ForbiddenCharacter),
("a>b", PathError::ForbiddenCharacter),
("ab:c", PathError::ForbiddenCharacter),
("a\"b", PathError::ForbiddenCharacter),
("a|b", PathError::ForbiddenCharacter),
("a?b", PathError::ForbiddenCharacter),
("a*b", PathError::ForbiddenCharacter),
];
for (path, error) in cases {
let result = ManifestPath::try_from(path);
assert_eq!(result, Err(error), "{path:?}");
}
}
#[test]
fn manifest_path_safe_paths_are_accepted() {
let paths = [
"résumé/日本語.txt",
"a folder/a.b.c",
".bashrc",
"COM0/COM10/LPT10",
"a..b",
];
for path in paths {
let result = ManifestPath::try_from(path);
let validated = result.unwrap();
assert_eq!(validated.segments().collect::<Vec<_>>().join("/"), path);
}
}
#[test]
fn manifest_path_byte_limits_are_enforced() {
let longest_segment = "a".repeat(limits::MANIFEST_SEGMENT_MAX);
let segment_count = limits::MANIFEST_PATH_MAX / (limits::MANIFEST_SEGMENT_MAX + 1);
let longest_path = std::iter::repeat_n(longest_segment.as_str(), segment_count)
.collect::<Vec<_>>()
.join("/");
let max_path = std::iter::repeat_n(longest_segment.as_str(), segment_count - 1)
.collect::<Vec<_>>()
.join("/")
+ "/"
+ &"b".repeat(limits::MANIFEST_SEGMENT_MAX - 1)
+ "/c";
let cases = [
(longest_segment.clone(), None),
(longest_segment + "a", Some(PathError::SegmentTooLong)),
("é".repeat(128), Some(PathError::SegmentTooLong)),
(longest_path, None),
(max_path, None),
(
"a/".repeat(limits::MANIFEST_PATH_MAX / 2) + "a",
Some(PathError::TooLong),
),
];
for (path, error) in cases {
let result = ManifestPath::try_from(path.as_str());
assert_eq!(result.err(), error);
}
}
#[test]
fn valid_manifest_invalid_entries_are_rejected() {
let mut bad_digest = file("a", 1);
bad_digest.sha256.pop();
let mut bad_directory_size = directory("a");
bad_directory_size.size = 1;
let mut bad_directory_digest = directory("a");
bad_directory_digest.sha256 = vec![7; 32];
let mut unspecified = file("a", 1);
unspecified.kind = FileKind::Unspecified.into();
let mut unknown = file("a", 1);
unknown.kind = 99;
let cases = [
(
vec![file("A", 1), file("a", 1)],
2,
ManifestError::DuplicatePath { index: 1 },
),
(
vec![file("É", 1), file("é", 1)],
2,
ManifestError::DuplicatePath { index: 1 },
),
(
vec![file("a/b/c", 1), directory("A")],
1,
ManifestError::ParentOrder { index: 0 },
),
(
vec![directory("a/b"), directory("a")],
0,
ManifestError::ParentOrder { index: 0 },
),
(
vec![file("a", 1), file("A/b", 1)],
2,
ManifestError::FileParent { index: 1 },
),
(vec![bad_digest], 1, ManifestError::Digest { index: 0 }),
(
vec![bad_directory_size],
0,
ManifestError::DirectorySize { index: 0 },
),
(
vec![bad_directory_digest],
0,
ManifestError::DirectoryDigest { index: 0 },
),
(vec![unspecified], 1, ManifestError::Kind { index: 0 }),
(vec![unknown], 1, ManifestError::Kind { index: 0 }),
(vec![file("a", 1)], 2, ManifestError::TotalSize),
(
vec![file("a", u64::MAX), file("b", 1)],
0,
ManifestError::SizeOverflow { index: 1 },
),
(
vec![file("../secret", 1)],
1,
ManifestError::Path {
index: 0,
source: PathError::DotSegment,
},
),
];
for (entries, total_size, error) in cases {
let manifest = FileManifest {
entries,
total_size,
};
let result = ValidManifest::try_from(&manifest);
assert_eq!(result, Err(error));
}
}
#[test]
fn valid_manifest_ordered_directories_and_implicit_parents_are_accepted() {
let manifest = FileManifest {
entries: vec![
directory("a"),
directory("a/b"),
file("a/b/c", 5),
file("implicit/d", 7),
],
total_size: 12,
};
let result = ValidManifest::try_from(&manifest);
let validated = result.unwrap();
assert_eq!(
validated.entries()[2].kind,
EntryKind::File {
size: 5,
sha256: [7; 32]
}
);
assert_eq!(validated.entries().len(), manifest.entries.len());
}
#[test]
fn valid_manifest_entry_limit_is_enforced() {
let entries: Vec<_> = (0..limits::MANIFEST_ENTRIES_MAX)
.map(|index| directory(&format!("dir{index}")))
.collect();
for extra in [false, true] {
let mut manifest = FileManifest {
entries: entries.clone(),
total_size: 0,
};
if extra {
manifest.entries.push(directory("extra"));
}
let result = ValidManifest::try_from(&manifest);
assert_eq!(result.err(), extra.then_some(ManifestError::TooManyEntries));
}
}
#[test]
fn unique_name_collisions_select_first_free_name() {
let cases = [
("report.txt", vec![], "report.txt"),
("report.txt", vec!["report.txt"], "report (2).txt"),
(
"report.txt",
vec!["report.txt", "report (2).txt", "report (3).txt"],
"report (4).txt",
),
(".bashrc", vec![".bashrc"], ".bashrc (2)"),
("folder", vec!["folder"], "folder (2)"),
(
"archive.tar.gz",
vec!["archive.tar.gz"],
"archive.tar (2).gz",
),
];
for (name, taken, expected) in cases {
let result = unique_name(name, |candidate| taken.contains(&candidate));
assert_eq!(result.unwrap(), expected);
}
}
#[test]
fn unique_name_exhausted_or_invalid_returns_error() {
let cases = [
("name".to_owned(), NameError::Exhausted),
("a/b".to_owned(), NameError::Nested),
("../a".to_owned(), NameError::Path(PathError::DotSegment)),
(
"a".repeat(limits::MANIFEST_SEGMENT_MAX),
NameError::Exhausted,
),
];
for (name, expected) in cases {
let result = unique_name(&name, |_| true);
assert_eq!(result, Err(expected));
}
}
#[test]
fn resume_plan_partial_lengths_select_only_plausible_offsets() {
let manifest = ValidManifest::try_from(&FileManifest {
entries: vec![
directory("folder"),
file("folder/file", 10),
file("empty", 0),
],
total_size: 10,
})
.unwrap();
let cases = [
(1, 4, Some(4)),
(1, 10, None),
(1, 11, None),
(1, 0, None),
(0, 1, None),
(2, 0, None),
(u32::MAX, 1, None),
];
for (file_index, len, expected) in cases {
let result = resume_plan(&manifest, &[Partial { file_index, len }]);
assert_eq!(
result,
expected
.map(|offset| FileResume { file_index, offset })
.into_iter()
.collect::<Vec<_>>()
);
}
}