paks 0.1.2

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

/// Defer a closure on drop.
pub struct Defer<F: FnMut()>(pub F);
impl<F: FnMut()> Drop for Defer<F> {
	fn drop(&mut self) {
		(self.0)()
	}
}
macro_rules! defer {
	($($body:tt)*) => {
		let __deferred = Defer(|| { $($body)* });
	};
}
macro_rules! temp_file {
	($file_name:expr) => {
		defer! {
			let _ = dbg!(std::fs::remove_file($file_name));
		}
	};
}

const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";

#[test]
fn test_corrupt1() {
	if cfg!(miri) {
		return;
	}

	let ref key = Key::default();

	temp_file!("corrupt1b");

	// Step 1: Create the empty PAKS file
	FileEditor::create_empty("corrupt1b", key).unwrap();

	// Step 2: Add example
	{
		let mut edit = FileEditor::open("corrupt1b", key).unwrap();
		edit.create_file(b"example", ALPHABET, key).unwrap();
		edit.finish(key).unwrap();
	}

	// Step 5: Read linked
	let example_text = {
		let reader = FileReader::open("corrupt1b", key).unwrap();
		reader.read(b"example", key).unwrap()
	};

	// Corruption!
	assert_eq!(example_text, ALPHABET);
}

#[test]
fn test_drop_without_finish_discards_changes() {
	if cfg!(miri) {
		return;
	}

	let ref key = [2, 3];

	temp_file!("drop_without_finish.paks");
	FileEditor::create_empty("drop_without_finish.paks", key).unwrap();

	{
		let mut edit = FileEditor::open("drop_without_finish.paks", key).unwrap();
		edit.create_file(b"pending.txt", ALPHABET, key).unwrap();
	}

	let reader = FileReader::open("drop_without_finish.paks", key).unwrap();
	assert!(matches!(reader.read(b"pending.txt", key), Err(err) if err.kind() == io::ErrorKind::NotFound));
	assert!(reader.as_ref().is_empty());
	assert_eq!(reader.high_mark(), Header::BLOCKS_LEN as u32);
}

#[test]
fn test_nested_roundtrip_persists_after_finish() {
	if cfg!(miri) {
		return;
	}

	let ref key = [9, 10];
	let nested = b"profiles/player1/settings.json";

	temp_file!("nested_roundtrip.paks");

	{
		let mut edit = FileEditor::create_new("nested_roundtrip.paks", key).unwrap();
		edit.create_file(nested, br#"{"volume":7,"difficulty":"hard"}"#, key).unwrap();
		edit.finish(key).unwrap();
	}

	let reader = FileReader::open("nested_roundtrip.paks", key).unwrap();
	assert_eq!(reader.read(nested, key).unwrap(), br#"{"volume":7,"difficulty":"hard"}"#);

	let tree = reader.display_children(Some("profiles"), &dir::TreeArt::ASCII).unwrap().to_string();
	assert_eq!(tree, "profiles/\n`- player1/\n   `  settings.json\n");
	let desc = reader.find_file(nested).expect("nested file should exist");
	assert_eq!(reader.high_mark(), desc.section.offset + desc.section.size);
	assert!(reader.info().directory.offset >= reader.high_mark());
}

#[test]
fn test_file_reader_and_editor_read_variants_agree() {
	if cfg!(miri) {
		return;
	}

	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 = [11, 12];
	let ref file_key = [13, 14];
	let path = b"logs/current.txt";
	let expected = TEXT_CONTENT.as_bytes();

	temp_file!("read_variants.paks");

	{
		let mut edit = FileEditor::create_new("read_variants.paks", archive_key).unwrap();
		edit.create_file(path, expected, file_key).unwrap();
		edit.finish(archive_key).unwrap();
	}

	let reader = FileReader::open("read_variants.paks", archive_key).unwrap();
	let editor = FileEditor::open("read_variants.paks", archive_key).unwrap();

	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 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,
	);
}

#[test]
fn test_read_only_surfaces_write_errors() {
	if cfg!(miri) {
		return;
	}

	let ref archive_key = [15, 16];
	let ref file_key = [17, 18];

	temp_file!("read_only_errors.paks");
	{
		let mut edit = FileEditor::create_new("read_only_errors.paks", archive_key).unwrap();
		edit.create_file(b"logs/current.txt", b"read only errors", file_key).unwrap();
		edit.finish(archive_key).unwrap();
	}

	let mut create_edit = FileEditor::read_only("read_only_errors.paks", archive_key).unwrap();
	assert!(create_edit.create_file(b"logs/new.txt", b"new", file_key).is_err());

	let mut finish_edit = FileEditor::read_only("read_only_errors.paks", archive_key).unwrap();
	finish_edit.create_dir(b"logs/archive");
	assert!(finish_edit.finish(archive_key).is_err());
}

#[test]
fn test_file_edit_file_zero_data_and_reencrypt() {
	if cfg!(miri) {
		return;
	}

	let ref archive_key = [21, 22];
	let ref old_file_key = [23, 24];
	let ref new_file_key = [25, 26];

	temp_file!("reencrypt_zero.paks");
	{
		let mut edit = FileEditor::create_new("reencrypt_zero.paks", archive_key).unwrap();
		{
			let mut file = edit.edit_file(b"generated/zero.bin");
			file.set_content(7, 19);
			file.allocate_data().zero_data(old_file_key).unwrap();
			file.reencrypt_data(old_file_key, new_file_key).unwrap();
		}
		edit.finish(archive_key).unwrap();
	}

	let reader = FileReader::open("reencrypt_zero.paks", archive_key).unwrap();
	assert_eq!(reader.read(b"generated/zero.bin", new_file_key).unwrap(), vec![0; 19]);
	assert!(reader.read(b"generated/zero.bin", old_file_key).is_err());
}