libfreemkv 0.31.2: comprehensive spec-grounded test suite (~950 tests)
Test-hardening release, no runtime changes. Adds spec-grounded unit tests across the silent-corruption surfaces — UDF/MPLS/CLPI/IFO parsing, BD/DVD title + extent assembly, AACS/CSS key handling, TS/PS demux + codec parsers, MKV/EBML container output, the mux pipeline, sector prefetch + decrypt decorator, drive/SCSI sense decoding, label extraction, and core I/O. Each test is grounded in the format spec or real on-disc behavior and verified to fail under a targeted source mutation. No behavior changed.
This commit is contained in:
@@ -284,4 +284,169 @@ mod tests {
|
||||
let result = NetworkStream::connect("127.0.0.1");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
/// Spawn an accepting reader and return (its address, join handle that
|
||||
/// yields all frames read after the FMKV header).
|
||||
fn spawn_reader() -> (
|
||||
std::net::SocketAddr,
|
||||
std::thread::JoinHandle<(DiscTitle, Vec<crate::pes::PesFrame>)>,
|
||||
) {
|
||||
use crate::pes;
|
||||
// Bind BEFORE spawning so the port is live when connect() runs — no
|
||||
// channel handshake needed (the listener already owns the socket).
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let handle = std::thread::spawn(move || {
|
||||
let mut ns = NetworkStream::accept_from(listener).unwrap();
|
||||
let info = pes::Stream::info(&ns).clone();
|
||||
let mut frames = Vec::new();
|
||||
while let Ok(Some(f)) = pes::Stream::read(&mut ns) {
|
||||
frames.push(f);
|
||||
}
|
||||
(info, frames)
|
||||
});
|
||||
(addr, handle)
|
||||
}
|
||||
|
||||
/// write() on a listen()/accept-constructed (READ) stream must return
|
||||
/// StreamReadOnly — the read side has no writer. (Returning Ok would let
|
||||
/// a caller silently lose frames written into a receive-only socket.)
|
||||
#[test]
|
||||
fn write_on_read_side_is_read_only_error() {
|
||||
use crate::pes;
|
||||
let (addr, handle) = spawn_reader();
|
||||
|
||||
// Sender connects, sends header (zero frames), finishes — so the
|
||||
// reader's accept_from() returns. We test the reader's write guard.
|
||||
let dt = sample_title();
|
||||
let mut writer = NetworkStream::connect(&addr.to_string()).unwrap().meta(&dt);
|
||||
pes::Stream::finish(&mut writer).unwrap();
|
||||
let (_info, _frames) = handle.join().unwrap();
|
||||
|
||||
// Now build a fresh read-side stream and confirm its write() errors.
|
||||
// (Re-bind, accept once, then immediately try to write to it.)
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr2 = listener.local_addr().unwrap();
|
||||
let h = std::thread::spawn(move || {
|
||||
let mut ns = NetworkStream::accept_from(listener).unwrap();
|
||||
let frame = pes::PesFrame {
|
||||
track: 0,
|
||||
pts: 0,
|
||||
keyframe: true,
|
||||
data: vec![0u8; 8],
|
||||
duration_ns: None,
|
||||
};
|
||||
// Read side: writing must be a typed StreamReadOnly error.
|
||||
let err = pes::Stream::write(&mut ns, &frame).expect_err("read side write must error");
|
||||
err.kind()
|
||||
});
|
||||
// Drive the accept: connect + send header so accept_from completes.
|
||||
let mut w2 = NetworkStream::connect(&addr2.to_string())
|
||||
.unwrap()
|
||||
.meta(&dt);
|
||||
pes::Stream::finish(&mut w2).unwrap();
|
||||
let kind = h.join().unwrap();
|
||||
// E_STREAM_READ_ONLY (9000) maps to Unsupported.
|
||||
assert_eq!(kind, io::ErrorKind::Unsupported);
|
||||
}
|
||||
|
||||
/// read() on a connect()-constructed (WRITE) stream must return
|
||||
/// StreamWriteOnly — never Ok(None), which a caller would read as a
|
||||
/// legitimately empty stream.
|
||||
#[test]
|
||||
fn read_on_write_side_is_write_only_error() {
|
||||
use crate::pes;
|
||||
let (addr, handle) = spawn_reader();
|
||||
let dt = sample_title();
|
||||
let mut writer = NetworkStream::connect(&addr.to_string()).unwrap().meta(&dt);
|
||||
let err = pes::Stream::read(&mut writer).expect_err("write side read must error");
|
||||
// E_STREAM_WRITE_ONLY (9001) maps to Unsupported.
|
||||
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
|
||||
pes::Stream::finish(&mut writer).unwrap();
|
||||
let _ = handle.join().unwrap();
|
||||
}
|
||||
|
||||
/// The FMKV header must be written exactly once, before the first frame,
|
||||
/// even across many frames. The receiver must therefore reconstruct the
|
||||
/// title exactly once and read every frame after it — a header re-emitted
|
||||
/// between frames would desync PesFrame::deserialize and corrupt frame N.
|
||||
#[test]
|
||||
fn header_written_once_then_all_frames_roundtrip() {
|
||||
use crate::pes;
|
||||
let (addr, handle) = spawn_reader();
|
||||
let dt = sample_title();
|
||||
let mut writer = NetworkStream::connect(&addr.to_string()).unwrap().meta(&dt);
|
||||
for i in 0..5u8 {
|
||||
let frame = pes::PesFrame {
|
||||
track: (i % 2) as usize,
|
||||
pts: i as i64 * 90_000,
|
||||
keyframe: i == 0,
|
||||
data: vec![i; 100 + i as usize],
|
||||
duration_ns: None,
|
||||
};
|
||||
pes::Stream::write(&mut writer, &frame).unwrap();
|
||||
}
|
||||
pes::Stream::finish(&mut writer).unwrap();
|
||||
let (info, frames) = handle.join().unwrap();
|
||||
// Title parsed once and intact.
|
||||
assert_eq!(info.streams.len(), 2);
|
||||
// Every frame survived in order with exact payloads — no desync from
|
||||
// a duplicated header.
|
||||
assert_eq!(frames.len(), 5);
|
||||
for (i, f) in frames.iter().enumerate() {
|
||||
assert_eq!(f.pts, i as i64 * 90_000, "frame {i} pts");
|
||||
assert_eq!(f.data.len(), 100 + i, "frame {i} payload length");
|
||||
assert!(
|
||||
f.data.iter().all(|&b| b == i as u8),
|
||||
"frame {i} payload bytes"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The receiver's title comes strictly from the SENDER's FMKV header:
|
||||
/// the sender's meta() title is what accept_from() reconstructs, proving
|
||||
/// the metadata flows sender→receiver over the header (not from the
|
||||
/// receiver's empty default). Distinct sender title confirms the source.
|
||||
#[test]
|
||||
fn receiver_title_comes_from_sender_header() {
|
||||
use crate::pes;
|
||||
let (addr, handle) = spawn_reader();
|
||||
let mut dt = sample_title();
|
||||
dt.playlist = "SenderControlled".into();
|
||||
dt.playlist_id = 42;
|
||||
let mut writer = NetworkStream::connect(&addr.to_string()).unwrap().meta(&dt);
|
||||
pes::Stream::finish(&mut writer).unwrap();
|
||||
let (info, _frames) = handle.join().unwrap();
|
||||
// The receiver default title is empty (playlist ""); it must have
|
||||
// been replaced by the sender's header-carried title.
|
||||
assert_eq!(info.playlist, "SenderControlled");
|
||||
assert_eq!(
|
||||
info.streams.len(),
|
||||
2,
|
||||
"stream descriptors round-trip via header"
|
||||
);
|
||||
}
|
||||
|
||||
/// accept_from() must reject a connection whose first bytes are NOT the
|
||||
/// FMKV magic — there is no metadata to drive muxing, so it surfaces
|
||||
/// NoMetadata rather than proceeding with an empty/garbage title.
|
||||
#[test]
|
||||
fn accept_from_rejects_stream_without_fmkv_header() {
|
||||
use std::io::Write as _;
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let handle = std::thread::spawn(move || {
|
||||
// Raw non-FMKV bytes (not starting with 'F') then close.
|
||||
let mut s = TcpStream::connect(addr).unwrap();
|
||||
s.write_all(&[0x47u8; 64]).unwrap(); // TS sync bytes, no FMKV magic
|
||||
s.shutdown(std::net::Shutdown::Both).unwrap();
|
||||
});
|
||||
let err = match NetworkStream::accept_from(listener) {
|
||||
Ok(_) => panic!("missing FMKV header must error, not silently accept"),
|
||||
Err(e) => e,
|
||||
};
|
||||
// E_NO_METADATA (9008) maps to InvalidInput.
|
||||
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
|
||||
handle.join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user