paks 0.1.2

A light-weight encrypted archive inspired by the Quake PAK format.
Documentation
use std::ptr;
use super::*;

// fn example_dir() -> Vec<Descriptor> {
// 	vec![
// 		Descriptor::file(b"before"),
// 		Descriptor::dir(b"a", 3),
// 		Descriptor::dir(b"b", 2),
// 		Descriptor::dir(b"c", 1),
// 		Descriptor::file(b"file"),
// 	]
// }

#[test]
fn name_eq_example() {
	// Create an empty descriptor with name "test"
	let mut desc = Descriptor::default();
	desc.name.set(b"test");

	assert_eq!(name_eq(&desc, b"test"), Some(&b""[..]));
	assert_eq!(name_eq(&desc, b"test/a/b"), Some(&b"a/b"[..]));
	assert_eq!(name_eq(&desc, b"testing"), None);
	assert_eq!(name_eq(&desc, b"te"), None);
}

#[test]
fn next_sibling_example() {
	// Given the following directory structure:
	//
	// ```text
	// +--. Foo
	// |  |   Bar
	// |  `   Baz
	// |
	// +--. Sub
	// |  `-. Dir
	// |
	// `   File
	// ```
	//
	// Iterating over the top level directory:

	let dir = [
		// ...
		Descriptor::dir(b"Foo", 2),
		Descriptor::file(b"Bar"),
		Descriptor::file(b"Baz"),
		Descriptor::dir(b"Sub", 1),
		Descriptor::dir(b"Dir", 0),
		Descriptor::file(b"File"),
	];
	let results = [true, false, false, true, false, true];

	let mut i = 0;
	let end = dir.len();
	while i < end {
		let desc = &dir[i];
		let next_i = next_sibling(desc, i, end);

		// Process the descriptor
		println!("processing dir[{}] out of {}", i, end);
		assert!(results[i]);

		// Advance the iteration
		i = next_i;
	}

	// Prints the following:
	//
	// ```text
	// processing dir[0] out of 6
	// processing dir[3] out of 6
	// processing dir[5] out of 6
	// ```
}

#[test]
fn test_to_string() {
	let dir = [
		Descriptor::dir(b"Foo", 2),
		Descriptor::file(b"Bar"),
		Descriptor::file(b"Baz"),
		Descriptor::dir(b"Sub", 1),
		Descriptor::dir(b"Dir", 0),
		Descriptor::file(b"File"),
	];

	let expected = "\
./
+- Foo/
|  |  Bar
|  `  Baz
|  
+- Sub/
|  `- Dir/
|  
`  File
";

	let result = DirFmt::new(".", &dir, &TreeArt::ASCII).to_string();
	println!("\n{}", result);
	assert_eq!(expected, result);
}

#[test]
fn test_find_empty() {
	assert_eq!(find(&[], b"path"), &[]);
}

#[test]
fn test_find_desc01() {
	let mut dir = Vec::new();
	create(&mut dir, b"A/B/C");

	let result1 = find_desc(&dir, b"A/B/C");
	let result2 = find_desc(&dir, b"A/B/D");

	assert_eq!(result1.unwrap().name(), b"C");
	assert!(result2.is_none());
}

#[test]
fn test_find() {
	let dir = [
		Descriptor::file(b"before"),
		Descriptor::dir(b"a", 3),
		Descriptor::dir(b"b", 2),
		Descriptor::dir(b"c", 1),
		Descriptor::file(b"file"),
	];

	assert!(ptr::eq(find(&dir, b"before"), &dir[0..1]));
	assert!(ptr::eq(find(&dir, b"a"), &dir[1..]));

	assert!(ptr::eq(find(&dir[2..], b"b"), &dir[2..]));

	assert_eq!(find(&dir, "file".as_ref()).len(), 0);
	assert!(ptr::eq(find(&dir[4..], b"file"), &dir[4..]));

	assert_eq!(find_desc(&dir, b"a\\b\\c\\file").map(|x| x as *const _), Some(&dir[4] as *const _));
}

#[test]
fn test_find_skips_file_when_more_path_remains() {
	let dir = [
		Descriptor::file(b"mods"),
		Descriptor::dir(b"mods", 2),
		Descriptor::file(b"config.txt"),
		Descriptor::file(b"notes.txt"),
		Descriptor::file(b"after"),
	];

	assert_eq!(find_desc(&dir, b"mods/config.txt").map(Descriptor::name), Some(&b"config.txt"[..]));
	assert_eq!(find(&dir, b"mods/missing.txt"), &[]);
	assert_eq!(find(&dir, b"after"), &dir[4..5]);
	assert_eq!(find(&dir, b"mods")[0].name(), b"mods");
	assert!(find_dir(&dir, b"mods").is_some());
	assert_eq!(find_dir(&dir, b"mods/config.txt"), Some(&[][..]));
}

#[test]
fn test_find_encrypted_skips_file_and_reports_missing_paths() {
	let ref key = [42, 13];
	let mut directory = Directory::from(vec![
		Descriptor::file(b"mods"),
		Descriptor::dir(b"mods", 2),
		Descriptor::file(b"config.txt"),
		Descriptor::file(b"notes.txt"),
		Descriptor::file(b"after"),
	]);
	let mut section = Section {
		offset: 0,
		size: directory.as_blocks().len() as u32,
		nonce: Block::default(),
		mac: Block::default(),
	};
	crate::crypt::encrypt_section(directory.as_blocks_mut(), &mut section, key);

	let found = find_encrypted(directory.as_ref(), b"mods/config.txt", &section, key).expect("encrypted file should be found");
	assert_eq!(found.name(), b"config.txt");
	assert!(found.is_file());
	assert!(find_encrypted(directory.as_ref(), b"mods/missing.txt", &section, key).is_none());
	assert_eq!(find_encrypted(directory.as_ref(), b"after", &section, key).unwrap().name(), b"after");
	assert!(find_encrypted(directory.as_ref(), b"", &section, key).is_none());
}

#[test]
fn test_create_simple() {
	let path = b"stuff.txt";

	let mut dir = Vec::new();
	create(&mut dir, path);

	assert_eq!(dir.len(), 1);
	let file = &dir[0];

	assert_eq!(file.content_type, 0);
	assert_eq!(file.content_size, 0);
	assert_eq!(file.section, Section::default());
	assert_eq!(file.name(), path);
}

#[test]
fn test_create_simple_dirs() {
	let path1 = b"A/FOO";
	let path2 = b"A/BAR";

	let mut dir = Vec::new();
	create(&mut dir, path1);
	create(&mut dir, path2);

	let result = [
		Descriptor::dir(b"A", 2),
		Descriptor::dir(b"FOO", 0),
		Descriptor::dir(b"BAR", 0),
	];
	assert_eq!(dir, result);
}

#[test]
fn test_create_reuses_existing_descriptor() {
	let mut dir = Vec::new();
	let first_ptr = {
		let first = create(&mut dir, b"save.bin");
		first.content_type = 9;
		first.content_size = 33;
		first as *mut Descriptor
	};

	let (second_ptr, second_content_type, second_content_size) = {
		let second = create(&mut dir, b"save.bin");
		(second as *mut Descriptor, second.content_type, second.content_size)
	};
	assert_eq!(first_ptr, second_ptr);
	assert_eq!(dir.len(), 1);
	assert_eq!(second_content_type, 9);
	assert_eq!(second_content_size, 33);
}

#[test]
fn test_create_supports_trailing_separators_and_file_name_conflicts() {
	let mut dir = vec![Descriptor::file(b"config")];
	let created = create(&mut dir, b"config/user/");

	assert!(created.is_dir());
	assert_eq!(created.name(), b"user");
	assert_eq!(dir, vec![
		Descriptor::dir(b"config", 1),
		Descriptor::dir(b"user", 0),
		Descriptor::file(b"config"),
	]);
}

#[test]
fn test_remove_missing_path_returns_none() {
	let mut dir = Vec::new();
	create(&mut dir, b"assets/logo.png");
	let snapshot = dir.clone();

	assert!(remove(&mut dir, b"assets/missing.png").is_none());
	assert_eq!(dir, snapshot);
}

#[test]
fn test_fsck_reports_invalid_file_metadata() {
	let mut invalid_name_len = Descriptor::file(b"len");
	invalid_name_len.name.buffer[NAME_BUF_LEN - 1] = 255;

	let mut invalid_utf8 = Descriptor::file(b"utf8");
	invalid_utf8.name.buffer[..2].copy_from_slice(&[0xff, 0xfe]);
	invalid_utf8.name.buffer[NAME_BUF_LEN - 1] = 2;

	let mut overlaps_header = Descriptor::file(b"header");
	overlaps_header.content_size = 1;
	overlaps_header.section = Section { offset: 0, size: 1, nonce: Block::default(), mac: Block::default() };

	let mut size_too_large = Descriptor::file(b"big");
	size_too_large.content_size = 1;
	size_too_large.section = Section { offset: Header::BLOCKS_LEN as u32, size: 9, nonce: Block::default(), mac: Block::default() };

	let mut overlaps_directory = Descriptor::file(b"directory");
	overlaps_directory.content_size = 1;
	overlaps_directory.section = Section { offset: 8, size: 1, nonce: Block::default(), mac: Block::default() };

	let mut content_too_large = Descriptor::file(b"content");
	content_too_large.content_size = BLOCK_SIZE as u32 + 1;
	content_too_large.section = Section { offset: Header::BLOCKS_LEN as u32, size: 1, nonce: Block::default(), mac: Block::default() };

	let dir = [invalid_name_len, invalid_utf8, overlaps_header, size_too_large, overlaps_directory, content_too_large];
	let mut log = String::new();

	assert!(!fsck(&dir, 8, &mut log));
	assert!(log.contains("invalid name length"));
	assert!(log.contains("invalid name ("));
	assert!(log.contains("overlaps the header"));
	assert!(log.contains("size too large"));
	assert!(log.contains("overlaps the directory"));
	assert!(log.contains("larger than its section"));
}

#[test]
fn test_fsck_reports_nested_errors_and_invalid_child_counts() {
	let mut nested_bad = Descriptor::file(b"broken.bin");
	nested_bad.content_size = 1;
	nested_bad.section = Section { offset: 0, size: 1, nonce: Block::default(), mac: Block::default() };
	let nested = [Descriptor::dir(b"root", 1), nested_bad];
	let mut nested_log = String::new();

	assert!(!fsck(&nested, 8, &mut nested_log));
	assert!(nested_log.contains("/root/broken.bin: invalid file section"));

	let malformed = [Descriptor::dir(b"root", 2), Descriptor::file(b"child")];
	let mut malformed_log = String::new();
	assert!(!fsck(&malformed, 8, &mut malformed_log));
	assert!(malformed_log.contains("invalid directory: too many children"));
}