corevm-host 0.1.28

Types that are common across CoreVM service, builder, monitor, tooling
Documentation
use super::*;
use alloc::{collections::BTreeSet, string::String, vec};
use codec::DecodeAll;
use rand::{distr::Alphanumeric, Rng};
use std::{
	io::Cursor,
	path::{Path, PathBuf},
};
use tempfile::TempDir;

impl BlockRef {
	fn zero() -> Self {
		Self { service_id: 0, hash: Hash::default() }
	}
}

#[test]
fn file_name_works() {
	assert!(FileName::new(c"hello".into()).is_ok());
	assert!(FileName::new(c"a/b".into()).is_err());
	assert!(FileName::new(c"".into()).is_err());
	assert!(FileName::new({
		let mut name = vec![b'a'; MAX_FILE_NAME_LEN - 1];
		name.push(0);
		CString::from_vec_with_nul(name).unwrap()
	})
	.is_ok());
	assert!(FileName::new({
		let mut name = vec![b'a'; MAX_FILE_NAME_LEN];
		name.push(0);
		CString::from_vec_with_nul(name).unwrap()
	})
	.is_err());
}

#[test]
fn file_name_encode_decode_work() {
	for name in [
		CString::from(c"hello"),
		c"a".into(),
		c"aa".into(),
		c"aaa".into(),
		CString::from_vec_with_nul({
			let mut name = vec![b'a'; MAX_FILE_NAME_LEN - 1];
			name.push(0);
			name
		})
		.unwrap(),
	] {
		let file_name = FileName::new(name).unwrap();
		let bytes = file_name.encode();
		let actual = FileName::decode_all(&mut &bytes[..]).unwrap();
		assert_eq!(file_name, actual);
	}
}

#[test]
fn main_block_encode_decode_work() {
	for expected in [
		MainBlock {
			kind: NodeKind::File,
			file_size: 0,
			block_size: MAX_BLOCK_SIZE as u64,
			block_refs: Default::default(),
			first_block: FileBlock::new(Default::default()).unwrap(),
		},
		MainBlock {
			kind: NodeKind::File,
			file_size: 1,
			block_size: MAX_BLOCK_SIZE as u64,
			block_refs: Default::default(),
			first_block: FileBlock::new(vec![b'x'; 1].into()).unwrap(),
		},
		MainBlock {
			kind: NodeKind::File,
			file_size: MAX_BLOCK_SIZE as u64,
			block_size: MAX_BLOCK_SIZE as u64,
			block_refs: vec![BlockRef::zero(); 1],
			first_block: FileBlock::new(vec![b'x'; MIN_BLOCK_SIZE].into()).unwrap(),
		},
	] {
		let mut bytes = Vec::new();
		expected.encode_to(&mut bytes);
		let actual = MainBlock::decode(bytes.into())
			.inspect_err(|e| panic!("{e}, main block = {expected:?}"))
			.unwrap();
		assert_eq!(expected.encode(), actual.encode());
	}
}

impl MainBlock {
	fn encode(&self) -> Vec<u8> {
		let mut buf = Vec::new();
		self.encode_to(&mut buf);
		buf
	}
}

#[test]
fn node_reader_writer_works() {
	let _ = env_logger::try_init();
	for block_size in
		[MIN_BLOCK_SIZE, MIN_BLOCK_SIZE + (MAX_BLOCK_SIZE - MIN_BLOCK_SIZE) / 3, MAX_BLOCK_SIZE]
	{
		for file_size in [0, 1, 2, 133, block_size - 1, block_size, block_size + 1] {
			for buf_len in [1, 123] {
				std::panic::catch_unwind(|| {
					let mut rng = rand::rng();
					let blocks: VecMap<BlockRef, Bytes> = VecMap::new();
					let mut writer =
						NodeWriter::new(0, blocks, file_size as u64, block_size).unwrap();
					let contents: Vec<u8> = (&mut rng).random_iter().take(file_size).collect();
					writer.write_all(&contents).unwrap();
					let (block_ref, blocks) = writer.finish().unwrap();
					let mut reader = NodeReader::new(&block_ref, blocks).unwrap();
					assert_eq!(file_size as u64, reader.file().main_block().file_size());
					let mut buf = Vec::new();
					let remaining = (reader.file().main_block().file_size() -
						reader.file().position()) as usize;
					buf.resize(remaining, 0_u8);
					let mut offset = 0;
					while offset != buf.len() {
						let n = (buf.len() - offset).min(buf_len);
						let nread = reader.read(&mut buf[offset..offset + n]).unwrap();
						assert_eq!(n, nread);
						offset += n;
					}
					assert_eq!(contents, buf);
					// Read the same data in one go.
					buf.clear();
					reader.seek(0).unwrap();
					let nread = reader.read_to_end(&mut buf).unwrap();
					assert_eq!(file_size, nread);
					assert_eq!(contents, buf);
				})
				.inspect_err(|_| {
					std::eprintln!(
						"Panic context: block size = {block_size}, \
                            file size = {file_size}, read buffer len = {buf_len}"
					);
				})
				.unwrap();
			}
		}
	}
}

#[test]
fn copy_file_in_out_works() {
	let _ = env_logger::try_init();
	for block_size in
		[MIN_BLOCK_SIZE, MIN_BLOCK_SIZE + (MAX_BLOCK_SIZE - MIN_BLOCK_SIZE) / 3, MAX_BLOCK_SIZE]
	{
		for file_size in [0, 1, 2, 133, block_size - 1, block_size, block_size + 1] {
			std::panic::catch_unwind(|| {
				let mut rng = rand::rng();
				let contents: Vec<u8> = (&mut rng).random_iter().take(file_size).collect();
				let mut blocks: VecMap<BlockRef, Bytes> = VecMap::new();
				let block_ref =
					copy_file_in(&mut Cursor::new(&contents[..]), 0, &mut blocks, block_size)
						.unwrap();
				let mut buf = vec![0_u8; contents.len()];
				copy_file_out(&block_ref, &mut blocks, &mut &mut buf[..]).unwrap();
				assert_eq!(contents, buf);
			})
			.inspect_err(|_| {
				std::eprintln!("Panic context: block size = {block_size}, file size = {file_size}");
			})
			.unwrap();
		}
	}
}

#[test]
fn copy_dir_in_out_works() {
	let _ = env_logger::try_init();
	for block_size in
		[MIN_BLOCK_SIZE, MIN_BLOCK_SIZE + (MAX_BLOCK_SIZE - MIN_BLOCK_SIZE) / 3, MAX_BLOCK_SIZE]
	{
		for _ in 0..100 {
			std::panic::catch_unwind(|| {
				let mut blocks: VecMap<BlockRef, Bytes> = VecMap::new();
				let input_dir = random_dir();
				let dir_reader = StdDirReader::new(input_dir.path().to_path_buf()).unwrap();
				let block_ref = copy_dir_in(dir_reader, 0, &mut blocks, block_size).unwrap();
				let out_dir = TempDir::new().unwrap();
				let dir_writer = StdDirWriter::new(out_dir.path().to_path_buf());
				copy_dir_out(&block_ref, &mut blocks, dir_writer).unwrap();
				assert_eq!(list_dir_all(input_dir.path()), list_dir_all(out_dir.path()));
			})
			.inspect_err(|_| {
				std::eprintln!("Panic context: block size = {block_size}");
			})
			.unwrap();
		}
	}
}

fn random_dir() -> TempDir {
	let dir = TempDir::new().unwrap();
	let mut current_path = dir.path().to_path_buf();
	let mut all_paths = BTreeSet::new();
	let mut rng = rand::rng();
	let rng = &mut rng;
	let num_files = rng.random_range(0..=10);
	for _ in 0..num_files {
		let file_path = loop {
			let file_name_len = rng.random_range(1..10);
			let file_name: String =
				rng.sample_iter(&Alphanumeric).take(file_name_len).map(char::from).collect();
			// MacOS/Windows file systems are case-insensitive.
			let file_path = current_path.join(file_name.to_lowercase());
			if all_paths.insert(file_path.clone()) {
				// Avoid duplicates.
				break file_path;
			}
		};
		if rng.random::<bool>() {
			// Create a file.
			let file_size = rng.random_range(0..=100);
			let contents: Vec<u8> = rng.random_iter().take(file_size).collect();
			std::fs::write(&file_path, &contents)
				.unwrap_or_else(|e| panic!("Failed to create file {file_path:?}: {e}"));
		} else {
			// Create a directory.
			std::fs::create_dir(&file_path)
				.unwrap_or_else(|e| panic!("Failed to create directory {file_path:?}: {e}"));
			if rng.random::<bool>() {
				// Enter the directory.
				current_path = file_path;
			}
		}
	}
	dir
}

/// List directory contents recursively.
fn list_dir_all(dir_path: &Path) -> Vec<(PathBuf, Entry)> {
	let mut all_entries = Vec::new();
	let mut queue = VecDeque::new();
	queue.push_back(dir_path.to_path_buf());
	while let Some(path) = queue.pop_front() {
		let dir = std::fs::read_dir(&path).unwrap();
		for entry in dir {
			let entry = entry.unwrap();
			let path = entry.path();
			let relative_path = path.strip_prefix(dir_path).unwrap().to_path_buf();
			let file_type = entry.file_type().unwrap();
			if file_type.is_file() {
				let contents = std::fs::read(&path).unwrap();
				all_entries.push((relative_path, Entry::File(contents)));
				continue;
			}
			if file_type.is_dir() {
				queue.push_back(entry.path());
				all_entries.push((relative_path, Entry::Dir));
				continue;
			}
			panic!("Unsupported file type {file_type:?}");
		}
	}
	all_entries.sort();
	all_entries
}

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Entry {
	File(Vec<u8>),
	Dir,
}

#[test]
fn resolve_path_works() {
	let mut blocks: VecMap<BlockRef, Bytes> = VecMap::new();
	let input_dir = {
		let dir = TempDir::new().unwrap();
		std::fs::write(dir.path().join("x"), "123").unwrap();
		std::fs::create_dir_all(dir.path().join("a").join("b")).unwrap();
		std::fs::write(dir.path().join("a").join("y"), "456").unwrap();
		dir
	};
	let dir_reader = StdDirReader::new(input_dir.path().to_path_buf()).unwrap();
	let root_dir_hash = copy_dir_in(dir_reader, 0, &mut blocks, MAX_BLOCK_SIZE).unwrap();
	// Determine file hashes.
	let x_hash = copy_file_in(
		&mut std::fs::File::open(input_dir.path().join("x")).unwrap(),
		0,
		&mut blocks,
		MAX_BLOCK_SIZE,
	)
	.unwrap();
	let y_hash = copy_file_in(
		&mut std::fs::File::open(input_dir.path().join("a").join("y")).unwrap(),
		0,
		&mut blocks,
		MAX_BLOCK_SIZE,
	)
	.unwrap();
	for path in [c"/", c"//", c"/./", c"/.", c"/..", c"/./../", c"/../..", c"/a/..", c"/a/b/../.."]
	{
		assert_eq!(
			root_dir_hash,
			resolve_path(root_dir_hash, c"", path, &mut blocks).unwrap(),
			"path = {path:?}"
		);
	}
	for path in [c"/x", c"/a/../x", c"/a/../../x"] {
		assert_eq!(
			x_hash,
			resolve_path(root_dir_hash, c"", path, &mut blocks).unwrap(),
			"path = {path:?}"
		);
	}
	for path in [c"/a/y", c"/a/b/../y", c"/a/../../a/y"] {
		assert_eq!(
			y_hash,
			resolve_path(root_dir_hash, c"", path, &mut blocks).unwrap(),
			"path = {path:?}"
		);
	}
	for (path, cwd) in [(c"..", c"/a"), (c"../..", c"/a/b")] {
		assert_eq!(
			root_dir_hash,
			resolve_path(root_dir_hash, cwd, path, &mut blocks).unwrap(),
			"path = {path:?}"
		);
	}
}