use crate::error::{Mf4Error, Result};
use std::collections::HashSet;
#[derive(Debug, Default)]
pub struct LinkChain {
seen: HashSet<u64>,
}
impl LinkChain {
pub fn new() -> Self {
Self {
seen: HashSet::new(),
}
}
pub fn visit(&mut self, offset: u64, chain: &str) -> Result<()> {
if !self.seen.insert(offset) {
return Err(Mf4Error::CyclicLink {
chain: chain.to_string(),
offset,
});
}
Ok(())
}
pub fn len(&self) -> usize {
self.seen.len()
}
pub fn is_empty(&self) -> bool {
self.seen.is_empty()
}
}
pub const MAX_COMPOSITION_DEPTH: usize = 32;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allows_each_offset_once() {
let mut chain = LinkChain::new();
assert!(chain.visit(100, "dg_next").is_ok());
assert!(chain.visit(200, "dg_next").is_ok());
assert!(chain.visit(300, "dg_next").is_ok());
assert_eq!(chain.len(), 3);
}
#[test]
fn rejects_a_self_referential_link() {
let mut chain = LinkChain::new();
assert!(chain.visit(100, "dg_next").is_ok());
let err = chain.visit(100, "dg_next").unwrap_err();
assert!(matches!(err, Mf4Error::CyclicLink { .. }));
}
#[test]
fn rejects_a_longer_loop() {
let mut chain = LinkChain::new();
for offset in [10, 20, 30] {
chain.visit(offset, "cg_next").unwrap();
}
assert!(chain.visit(10, "cg_next").is_err());
}
#[test]
fn names_the_chain_in_the_error() {
let mut chain = LinkChain::new();
chain.visit(0x40, "cn_next").unwrap();
let msg = chain.visit(0x40, "cn_next").unwrap_err().to_string();
assert!(msg.contains("cn_next"), "error should name the link: {msg}");
assert!(msg.contains("40"), "error should give the offset: {msg}");
}
#[test]
fn separate_chains_do_not_interfere() {
let mut a = LinkChain::new();
let mut b = LinkChain::new();
a.visit(100, "dg_next").unwrap();
assert!(
b.visit(100, "cg_next").is_ok(),
"one chain visiting an offset must not block another"
);
}
#[test]
fn starts_empty() {
let chain = LinkChain::new();
assert!(chain.is_empty());
assert_eq!(chain.len(), 0);
}
}