use crate::location::FilePath;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ContentHash([u8; 32]);
impl ContentHash {
#[must_use]
pub const fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl std::fmt::Display for ContentHash {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in &self.0 {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ReadOutcome {
Found(ContentHash),
Absent,
Refused,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TrackedRead {
pub path: FilePath,
pub outcome: ReadOutcome,
}
impl TrackedRead {
#[must_use]
pub const fn found(path: FilePath, hash: ContentHash) -> Self {
Self {
path,
outcome: ReadOutcome::Found(hash),
}
}
#[must_use]
pub const fn absent(path: FilePath) -> Self {
Self {
path,
outcome: ReadOutcome::Absent,
}
}
#[must_use]
pub const fn refused(path: FilePath) -> Self {
Self {
path,
outcome: ReadOutcome::Refused,
}
}
#[must_use]
pub const fn hash(&self) -> Option<ContentHash> {
match self.outcome {
ReadOutcome::Found(hash) => Some(hash),
ReadOutcome::Absent | ReadOutcome::Refused => None,
}
}
}
pub fn sort(reads: &mut [TrackedRead]) {
reads.sort_by(|a, b| a.path.cmp(&b.path));
}
#[cfg(test)]
mod tests {
use super::*;
fn hash(seed: u8) -> ContentHash {
ContentHash::new([seed; 32])
}
#[test]
fn a_hash_renders_as_hex() {
let mut bytes = [0u8; 32];
bytes[0] = 0x0a;
bytes[31] = 0xff;
let rendered = ContentHash::new(bytes).to_string();
assert_eq!(rendered.len(), 64, "a blake3 digest is 64 hex characters");
assert!(rendered.starts_with("0a"), "{rendered}");
assert!(rendered.ends_with("ff"), "{rendered}");
}
#[test]
fn an_absent_read_is_still_a_dependency() {
let read = TrackedRead::absent(FilePath::new("tsconfig.json"));
assert_eq!(read.hash(), None);
assert_ne!(
read,
TrackedRead::found(FilePath::new("tsconfig.json"), hash(0)),
"absence and presence must not compare equal"
);
}
#[test]
fn a_refusal_is_neither_absence_nor_a_reading() {
let path = FilePath::new("node_modules/pkg/index.d.ts");
let refused = TrackedRead::refused(path.clone());
assert_eq!(refused.outcome, ReadOutcome::Refused);
assert_eq!(refused.hash(), None, "nothing was read");
assert_ne!(refused, TrackedRead::absent(path.clone()));
assert_ne!(refused, TrackedRead::found(path, hash(0)));
}
#[test]
fn dependencies_sort_by_path() {
let mut reads = vec![
TrackedRead::found(FilePath::new("b.json"), hash(1)),
TrackedRead::absent(FilePath::new("a.json")),
TrackedRead::found(FilePath::new("c.json"), hash(2)),
];
sort(&mut reads);
assert_eq!(
reads.iter().map(|r| r.path.as_str()).collect::<Vec<_>>(),
vec!["a.json", "b.json", "c.json"]
);
}
#[test]
fn the_order_reads_happened_in_does_not_survive() {
let one = {
let mut reads = vec![
TrackedRead::found(FilePath::new("b.json"), hash(1)),
TrackedRead::found(FilePath::new("a.json"), hash(2)),
];
sort(&mut reads);
reads
};
let other = {
let mut reads = vec![
TrackedRead::found(FilePath::new("a.json"), hash(2)),
TrackedRead::found(FilePath::new("b.json"), hash(1)),
];
sort(&mut reads);
reads
};
assert_eq!(one, other);
}
}