Merge codeaudit/testutil (0e7e458)

This commit is contained in:
Matthew Jackson
2026-09-29 19:26:33 -07:00
4 changed files with 78 additions and 13 deletions
+26 -6
View File
@@ -121,8 +121,8 @@ impl AacsStateBuilder {
}
/// One stream file of an [`encrypted_bd_image`]: its path under the image root
/// (e.g. `"BDMV/STREAM/00001.m2ts"`), its length in sectors (a multiple of 3 for
/// whole aligned units), and the CPS unit key its units are encrypted with
/// (e.g. `"BDMV/STREAM/00001.m2ts"`), its length in sectors (rounded up to a whole
/// number of aligned units, the tail padded), and the CPS unit key its units are encrypted with
/// (`None`: clear content, every Copy_permission_indicator `00₂`).
#[derive(Debug, Clone)]
pub struct BdFile {
@@ -200,12 +200,16 @@ impl SectorSource for MemSource {
/// keyed file's units carry CPI `11₂` in every packet and are then encrypted
/// (KS-3, KS-4). Panics on a fixture error; test use only.
pub fn encrypted_bd_image(files: &[BdFile], uk_ro: &[u8]) -> EncryptedBdImage {
let root = fixture_dir();
let dir = FixtureDir(fixture_dir());
let root = dir.0.clone();
write(&root.join("AACS/Unit_Key_RO.inf"), uk_ro);
let unit_sectors = (ALIGNED_UNIT_LEN / SECTOR_BYTES) as u32;
for f in files {
// A partial final unit is padded out to a whole aligned unit.
let sectors = f.sectors.div_ceil(unit_sectors) * unit_sectors;
write(
&root.join(&f.path),
&vec![0u8; f.sectors as usize * SECTOR_BYTES],
&vec![0u8; sectors as usize * SECTOR_BYTES],
);
}
let mut img = crate::DirImage::open(&root).expect("fixture DirImage");
@@ -226,10 +230,10 @@ pub fn encrypted_bd_image(files: &[BdFile], uk_ro: &[u8]) -> EncryptedBdImage {
.expect("fixture read");
}
drop(img);
let _ = std::fs::remove_dir_all(&root);
drop(dir);
let mut plain = image.clone();
let unit_sectors = (ALIGNED_UNIT_LEN / SECTOR_BYTES) as u32;
for (f, &(start, sectors)) in files.iter().zip(&extents) {
debug_assert_eq!(sectors % unit_sectors, 0, "{}: whole aligned units", f.path);
for u in 0..sectors / unit_sectors {
let lba = start + u * unit_sectors;
let mut unit = content_unit(lba, f.key.is_some());
@@ -261,6 +265,21 @@ fn content_unit(lba: u32, encrypted: bool) -> Vec<u8> {
u
}
// Removes its directory on drop, so a panicking fixture build doesn't leak it.
struct FixtureDir(std::path::PathBuf);
impl Drop for FixtureDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
thread_local! {
// The most recent fixture dir made on this thread, so tests can observe it.
static LAST_FIXTURE_DIR: std::cell::RefCell<Option<std::path::PathBuf>> =
const { std::cell::RefCell::new(None) };
}
// A fresh, empty directory for one fixture (removed once the image is read).
fn fixture_dir() -> std::path::PathBuf {
static NEXT: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
@@ -268,6 +287,7 @@ fn fixture_dir() -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!("libfreemkv-test-util-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("fixture dir");
LAST_FIXTURE_DIR.with(|d| *d.borrow_mut() = Some(dir.clone()));
dir
}
+20 -5
View File
@@ -310,10 +310,11 @@ impl FakeTransport {
}
// Block until released (a new `release` generation) or `limit` passes.
fn stall(&self, limit: Duration) -> bool {
// `gen0` is the release generation read when the CDB was logged, so a release
// between the log and this wait still counts.
fn stall(&self, limit: Duration, gen0: u64) -> bool {
let end = Instant::now() + limit;
let mut st = self.shared.lock();
let gen0 = st.released;
while st.released == gen0 {
let left = end.saturating_duration_since(Instant::now());
if left.is_zero() {
@@ -339,7 +340,7 @@ impl ScsiTransport for FakeTransport {
timeout_ms: u32,
) -> Result<ScsiResult> {
self.last_progress = None;
let (mode, limit) = {
let (mode, limit, gen0) = {
let mut st = self.shared.lock();
let after_cancel = st.watch.as_ref().is_some_and(Halt::is_cancelled);
st.log.push(FakeCdb {
@@ -369,7 +370,7 @@ impl ScsiTransport for FakeTransport {
break;
}
}
(mode, limit)
(mode, limit, st.released)
};
let r = match mode {
None => self.serve(cdb, dir, data, timeout_ms),
@@ -381,7 +382,7 @@ impl ScsiTransport for FakeTransport {
std::thread::sleep(d);
self.serve(cdb, dir, data, timeout_ms)
}
Some(FakeMode::Stall) => match self.stall(limit) {
Some(FakeMode::Stall) => match self.stall(limit, gen0) {
true => self.serve(cdb, dir, data, timeout_ms),
false => Err(transport_failure(cdb[0])),
},
@@ -420,3 +421,17 @@ fn note_ledger(l: &mut Ledger, cdb: &[u8], resp: &[u8], ok: bool) {
_ => {}
}
}
#[cfg(test)]
mod stall_tests {
use super::*;
// A release after the generation was read must end the stall even with a zero limit.
#[test]
fn stall_returns_true_when_released_after_gen_read() {
let (t, h) = FakeTransport::new();
let g = h.0.lock().released;
h.release();
assert!(t.stall(Duration::ZERO, g));
}
}
+31
View File
@@ -156,3 +156,34 @@ fn decrypt_unit_inverts_encrypt_unit() {
crate::test_util::decrypt_unit(&mut u, &K1);
assert_eq!(u, plain);
}
#[test]
fn fixture_dir_removed_when_build_panics() {
let bad = [BdFile {
path: "AACS/Unit_Key_RO.inf/x".into(),
sectors: 3,
key: None,
}];
let r = std::panic::catch_unwind(|| encrypted_bd_image(&bad, &[0u8; 4]));
assert!(r.is_err());
let dir = crate::test_util::LAST_FIXTURE_DIR
.with(|d| d.borrow().clone())
.expect("dir");
assert!(!dir.exists(), "fixture dir leaked: {}", dir.display());
}
#[test]
fn encrypted_bd_image_pads_a_partial_final_unit() {
let inf = unit_key_ro(AacsVersion::V10, &[[0u8; 16]; 1], &[1]);
let img = encrypted_bd_image(&[BdFile::new("BDMV/STREAM/00001.m2ts", 7, Some(K1))], &inf);
let (start, sectors) = img.files[0];
assert_eq!(sectors, 9, "7 sectors round up to 3 whole units");
for u in 0..3 {
let lba = start + u * 3;
assert!(
aacs_unit_encrypted(&unit(&img.image, lba), ContentFormat::BdTs),
"unit {u}"
);
assert_ne!(unit(&img.image, lba), unit(&img.plain, lba), "unit {u}");
}
}
+1 -2
View File
@@ -122,7 +122,7 @@ fn install() {
}
// Run `f` with every `tracing` event it emits on this thread captured; returns `f`'s value
// alongside the events, in emission order. One global subscriber, not scoped `with_default`
// alongside the events, in emission order. One global subscriber (see `install`), not scoped `with_default`.
pub(crate) fn capture<T>(f: impl FnOnce() -> T) -> (T, Vec<CapturedEvent>) {
install();
let sink: Sink = Arc::default();
@@ -134,7 +134,6 @@ pub(crate) fn capture<T>(f: impl FnOnce() -> T) -> (T, Vec<CapturedEvent>) {
(out, events)
}
#[cfg(test)]
mod tests {
use super::*;