168 lines
5.3 KiB
Rust
168 lines
5.3 KiB
Rust
use std::env;
|
|
use std::ffi::OsString;
|
|
use std::fs;
|
|
use std::io::{self, Read};
|
|
use std::os::unix::ffi::{OsStrExt, OsStringExt};
|
|
use std::os::unix::fs::{FileTypeExt, MetadataExt};
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use rustix::fs::{AtFlags, StatxFlags, statx};
|
|
use sha2::{Digest, Sha256};
|
|
|
|
const FIXTURE_CONTEXT: &[u8] = b"lazaret-ignored-workspace-fixture-v1\0";
|
|
|
|
fn main() {
|
|
if let Err(error) = run() {
|
|
eprintln!("lazaret workspace benchmark refused: {error}");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
|
|
fn run() -> io::Result<()> {
|
|
let arguments = env::args_os().skip(1).collect::<Vec<_>>();
|
|
let root = parse_metadata_arguments(&arguments)?;
|
|
let result = measure_metadata(&root)?;
|
|
println!(
|
|
"{}",
|
|
serde_json::json!({
|
|
"content_bytes": result.content_bytes,
|
|
"fixture_identity": hex(&result.fixture_identity),
|
|
"regular_files": result.regular_files,
|
|
})
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_metadata_arguments(arguments: &[OsString]) -> io::Result<PathBuf> {
|
|
if arguments.len() != 6
|
|
|| arguments[0] != "metadata"
|
|
|| arguments[1] != "--root"
|
|
|| arguments[3] != "--bytewise-order"
|
|
|| arguments[4] != "--statx-mask"
|
|
|| arguments[5] != "basic-stats"
|
|
{
|
|
return Err(io::Error::other(
|
|
"usage: lazaret-workspace-benchmark metadata --root ROOT --bytewise-order --statx-mask basic-stats",
|
|
));
|
|
}
|
|
Ok(PathBuf::from(&arguments[2]))
|
|
}
|
|
|
|
fn measure_metadata(root: &Path) -> io::Result<MetadataResult> {
|
|
let mut entries = Vec::new();
|
|
collect_entries(root, Path::new(""), &mut entries)?;
|
|
entries.sort_by(|left, right| {
|
|
left.relative
|
|
.as_os_str()
|
|
.as_bytes()
|
|
.cmp(right.relative.as_os_str().as_bytes())
|
|
});
|
|
|
|
let mut stream = Vec::from(FIXTURE_CONTEXT);
|
|
stream.extend_from_slice(
|
|
&u32::try_from(entries.len())
|
|
.map_err(|_| io::Error::other("fixture contains too many entries"))?
|
|
.to_be_bytes(),
|
|
);
|
|
let mut regular_files = 0_u64;
|
|
let mut content_bytes = 0_u64;
|
|
for entry in entries {
|
|
let path_bytes = entry.relative.as_os_str().as_bytes();
|
|
stream.extend_from_slice(
|
|
&u32::try_from(path_bytes.len())
|
|
.map_err(|_| io::Error::other("fixture path is too long"))?
|
|
.to_be_bytes(),
|
|
);
|
|
stream.extend_from_slice(path_bytes);
|
|
stream.push(entry.kind);
|
|
stream.extend_from_slice(&(entry.metadata.mode() & 0o7777).to_be_bytes());
|
|
let payload_length = u64::try_from(entry.payload.len())
|
|
.map_err(|_| io::Error::other("fixture payload is too large"))?;
|
|
stream.extend_from_slice(&payload_length.to_be_bytes());
|
|
stream.extend_from_slice(&Sha256::digest(&entry.payload));
|
|
if entry.kind == 2 {
|
|
regular_files = regular_files.saturating_add(1);
|
|
content_bytes = content_bytes.saturating_add(payload_length);
|
|
}
|
|
}
|
|
Ok(MetadataResult {
|
|
regular_files,
|
|
content_bytes,
|
|
fixture_identity: Sha256::digest(stream).into(),
|
|
})
|
|
}
|
|
|
|
fn collect_entries(
|
|
root: &Path,
|
|
relative: &Path,
|
|
entries: &mut Vec<FixtureEntry>,
|
|
) -> io::Result<()> {
|
|
let mut children = fs::read_dir(root.join(relative))?
|
|
.map(|entry| entry.map(|entry| entry.file_name()))
|
|
.collect::<io::Result<Vec<_>>>()?;
|
|
children.sort_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
|
|
for name in children {
|
|
let child_relative = relative.join(name);
|
|
let path = root.join(&child_relative);
|
|
let metadata = fs::symlink_metadata(&path)?;
|
|
statx(
|
|
rustix::fs::CWD,
|
|
&path,
|
|
AtFlags::SYMLINK_NOFOLLOW,
|
|
StatxFlags::BASIC_STATS,
|
|
)?;
|
|
let file_type = metadata.file_type();
|
|
let (kind, payload) = if file_type.is_dir() {
|
|
(1, Vec::new())
|
|
} else if file_type.is_file() {
|
|
let mut payload = Vec::new();
|
|
fs::File::open(&path)?.read_to_end(&mut payload)?;
|
|
(2, payload)
|
|
} else if file_type.is_symlink() {
|
|
(3, fs::read_link(&path)?.into_os_string().into_vec())
|
|
} else if file_type.is_socket()
|
|
|| file_type.is_fifo()
|
|
|| file_type.is_block_device()
|
|
|| file_type.is_char_device()
|
|
{
|
|
return Err(io::Error::other(
|
|
"fixture contains an unsupported entry kind",
|
|
));
|
|
} else {
|
|
return Err(io::Error::other("fixture entry kind is unknown"));
|
|
};
|
|
entries.push(FixtureEntry {
|
|
relative: child_relative.clone(),
|
|
metadata,
|
|
kind,
|
|
payload,
|
|
});
|
|
if kind == 1 {
|
|
collect_entries(root, &child_relative, entries)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn hex(bytes: &[u8]) -> String {
|
|
const DIGITS: &[u8; 16] = b"0123456789abcdef";
|
|
let mut encoded = String::with_capacity(bytes.len() * 2);
|
|
for byte in bytes {
|
|
encoded.push(char::from(DIGITS[usize::from(byte >> 4)]));
|
|
encoded.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
|
|
}
|
|
encoded
|
|
}
|
|
|
|
struct FixtureEntry {
|
|
relative: PathBuf,
|
|
metadata: fs::Metadata,
|
|
kind: u8,
|
|
payload: Vec<u8>,
|
|
}
|
|
|
|
struct MetadataResult {
|
|
regular_files: u64,
|
|
content_bytes: u64,
|
|
fixture_identity: [u8; 32],
|
|
}
|