paks 0.1.2

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

const EXAMPLE: &[u8] = include_str!("../../tests/data/example.txt").as_bytes();

#[test]
fn test_simple() {
	let ref key = [1, 2];

	// Create a new PAKS file and finish it
	let (blocks, _) = MemoryEditor::new().finish(key);

	// Re-open the PAKS file for editing
	let mut edit = MemoryEditor::from_blocks(blocks, key).expect("failed to edit");

	// Add the test file
	edit.create_file(b"example", EXAMPLE, key);

	// Finish the test PAKS file
	let (blocks, _) = edit.finish(key);

	// Re-open the PAKS file for reading
	let reader = MemoryReader::from_blocks(blocks, key).expect("failed to read");

	// Check the directory listing
	let dir = &*reader;
	let listing = dir::DirFmt::new(".", dir.as_ref(), &dir::TreeArt::ASCII).to_string();
	assert_eq!(dbg!(listing), "./\n`  example\n");

	// Check the test file
	let desc = reader.find_file(b"example").expect("example file not found");
	let example = reader.read_data(desc, key).expect("failed to read example");
	assert_eq!(example, EXAMPLE);
}

#[test]
fn test_bundled_archive() {
	let ref key = [7, 11];

	let mut edit = MemoryEditor::new();
	edit.create_file(b"nested/example.txt", EXAMPLE, key);
	let (blocks, _) = edit.finish(key);

	let reader = BundleReader::open(&blocks, *key).expect("failed to open embedded archive");
	let desc = reader.find_file(b"nested/example.txt").expect("missing embedded file");
	let data = reader.read_data(&desc, key).expect("failed to read embedded file");

	assert_eq!(data, EXAMPLE);
}

#[test]
fn test_bundle_rejects_tampered_directory() {
	let ref key = [7, 11];

	let mut edit = MemoryEditor::new();
	edit.create_file(b"nested/example.txt", EXAMPLE, key);
	let (mut blocks, _) = edit.finish(key);

	blocks.last_mut().unwrap()[0] ^= 1;

	assert!(matches!(BundleReader::open(&blocks, *key), Err(ErrorKind::InvalidData)));
}

#[test]
fn test_gc_reclaims_removed_file_space() {
	let ref key = [5, 8];

	let mut edit = MemoryEditor::new();
	edit.create_file(b"keep.txt", EXAMPLE, key);
	edit.create_file(b"remove.txt", &[0xAB; 96], key);
	let high_mark_before_remove = edit.high_mark();

	let removed = edit.remove(b"remove.txt").expect("file should be removable");
	assert!(removed.is_file());

	edit.gc();

	assert!(edit.high_mark() < high_mark_before_remove);
	let keep = edit.read(b"keep.txt", key).expect("remaining file should still read");
	assert_eq!(keep, EXAMPLE);
	let keep_desc = edit.find_file(b"keep.txt").expect("remaining file should still exist");
	assert_eq!(keep_desc.section.offset, Header::BLOCKS_LEN as u32);
	assert_eq!(edit.high_mark(), keep_desc.section.offset + keep_desc.section.size);

	let (blocks, _) = edit.finish(key);
	let reader = MemoryReader::from_blocks(blocks.clone(), key).expect("compacted archive should reopen");
	let reopened = MemoryEditor::from_blocks(blocks, key).expect("compacted archive should reopen for editing");
	assert_eq!(reader.read(b"keep.txt", key).unwrap(), EXAMPLE);
	assert!(matches!(reader.read(b"remove.txt", key), Err(ErrorKind::NotFound)));
	assert!(reader.find_file(b"keep.txt").is_some());
	assert!(reader.find_file(b"remove.txt").is_none());

	let mut log = String::new();
	let reopened_keep = reopened.find_file(b"keep.txt").expect("remaining file should still exist after reopen");
	assert_eq!(reopened.high_mark(), reopened_keep.section.offset + reopened_keep.section.size);
	assert!(reopened.fsck(reopened.high_mark(), &mut log), "fsck failed: {log}");
}

#[test]
fn test_read_data_into_reads_requested_window() {
	let ref key = [21, 34];
	let payload = b"header:payload:footer";

	let mut edit = MemoryEditor::new();
	edit.create_file(b"logs/current.txt", payload, key);
	let (blocks, _) = edit.finish(key);

	let reader = MemoryReader::from_blocks(blocks, key).expect("failed to open archive");
	let desc = reader.find_file(b"logs/current.txt").expect("file should exist");
	let mut slice = [0u8; 7];
	reader.read_data_into(desc, key, 7, &mut slice).expect("partial read should succeed");

	assert_eq!(&slice, b"payload");
	assert!(matches!(reader.read(b"logs/missing.txt", key), Err(ErrorKind::NotFound)));
}

#[test]
fn test_memory_and_bundle_read_variants_agree() {
	const TEXT_CONTENT: &str = "header:payload:footer";
	const READ_WINDOW_OFFSET: usize = 7;
	const READ_WINDOW: &[u8] = b"payload";

	#[track_caller]
	fn assert_read_variants(
		read: Vec<u8>,
		text: String,
		section: Vec<Block>,
		data: Vec<u8>,
		window: &[u8],
		expected: &[u8],
	) {
		assert_eq!(read, expected);
		assert_eq!(text, std::str::from_utf8(expected).unwrap());
		assert_eq!(data, expected);
		assert_eq!(window, &expected[READ_WINDOW_OFFSET..READ_WINDOW_OFFSET + READ_WINDOW.len()]);
		assert_eq!(&dataview::bytes(section.as_slice())[..expected.len()], expected);
	}

	let ref archive_key = [31, 32];
	let ref file_key = [33, 34];
	let path = b"logs/current.txt";
	let expected = TEXT_CONTENT.as_bytes();

	let mut builder = MemoryEditor::new();
	builder.create_file(path, expected, file_key);
	let (blocks, _) = builder.finish(archive_key);

	let editor = MemoryEditor::from_blocks(blocks.clone(), archive_key).expect("failed to reopen archive for editing");
	let reader = MemoryReader::from_blocks(blocks.clone(), archive_key).expect("failed to reopen archive for reading");
	let bundle = BundleReader::open(&blocks, *archive_key).expect("failed to open bundled archive");

	let editor_desc = *editor.find_file(path).expect("editor descriptor missing");
	let mut editor_window = [0u8; READ_WINDOW.len()];
	editor.read_data_into(&editor_desc, file_key, READ_WINDOW_OFFSET, &mut editor_window).unwrap();
	assert_read_variants(
		editor.read(path, file_key).unwrap(),
		editor.read_to_string(path, file_key).unwrap(),
		editor.read_section(&editor_desc.section, file_key).unwrap(),
		editor.read_data(&editor_desc, file_key).unwrap(),
		&editor_window,
		expected,
	);

	let reader_desc = *reader.find_file(path).expect("reader descriptor missing");
	let mut reader_window = [0u8; READ_WINDOW.len()];
	reader.read_data_into(&reader_desc, file_key, READ_WINDOW_OFFSET, &mut reader_window).unwrap();
	assert_read_variants(
		reader.read(path, file_key).unwrap(),
		reader.read_to_string(path, file_key).unwrap(),
		reader.read_section(&reader_desc.section, file_key).unwrap(),
		reader.read_data(&reader_desc, file_key).unwrap(),
		&reader_window,
		expected,
	);

	let bundle_desc = bundle.find_file(path).expect("bundle descriptor missing");
	let mut bundle_window = [0u8; READ_WINDOW.len()];
	bundle.read_data_into(&bundle_desc, file_key, READ_WINDOW_OFFSET, &mut bundle_window).unwrap();
	assert_read_variants(
		bundle.read(path, file_key).unwrap(),
		bundle.read_to_string(path, file_key).unwrap(),
		bundle.read_section(&bundle_desc.section, file_key).unwrap(),
		bundle.read_data(&bundle_desc, file_key).unwrap(),
		&bundle_window,
		expected,
	);
}

#[test]
fn test_memory_edit_file_zero_data_and_reencrypt() {
	let ref archive_key = [41, 42];
	let ref old_file_key = [43, 44];
	let ref new_file_key = [45, 46];

	let mut edit = MemoryEditor::new();
	{
		let mut file = edit.edit_file(b"generated/zero.bin");
		file.set_content(7, 19);
		file.allocate_data().zero_data(old_file_key);
		file.reencrypt_data(old_file_key, new_file_key);
	}
	let (blocks, _) = edit.finish(archive_key);

	let reader = MemoryReader::from_blocks(blocks, archive_key).expect("failed to reopen archive");
	assert_eq!(reader.read(b"generated/zero.bin", new_file_key).unwrap(), vec![0; 19]);
	assert!(matches!(reader.read(b"generated/zero.bin", old_file_key), Err(ErrorKind::InvalidData)));
}