Add 113 tests, update CI to checkout@v5, add FEATURES.md
Test suite: 64 → 177 tests - MPLS parser: 6 tests (synthetic binary, streams, errors) - CLPI parser: 6 tests (EP map, PTS/SPN math, errors) - H.264: 12 tests (NAL parsing, SPS/PPS, keyframes) - HEVC: 13 tests (VPS/SPS/PPS, IRAP range, codec private) - AC3: 12 tests (syncword, frame extraction) - VC1: 15 tests (BITMAPINFOHEADER, start codes) - DTS: 5, TrueHD: 4, PGS: 4 tests - EBML: 6 tests (size/ID/string/float roundtrips) - UDF: 10 tests (MockSectorReader, filesystem parsing, error paths) - Disc: 8 tests (scan_image, DiscTitle helpers) - Streams: 5 new (meta roundtrip, MkvStream) - NullStream: 4, StdioStream: 2, IsoSectorReader: 2 CI: actions/checkout@v4 → v5 (all workflows) FEATURES.md: created for v0.7.1
This commit is contained in:
@@ -47,3 +47,137 @@ fn find_ac3_sync(data: &[u8]) -> Option<usize> {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mux::ts::PesPacket;
|
||||
|
||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||
PesPacket { pid: 0x1100, pts, dts: None, data }
|
||||
}
|
||||
|
||||
// --- syncword detection ---
|
||||
|
||||
#[test]
|
||||
fn find_ac3_sync_at_start() {
|
||||
let data = [0x0B, 0x77, 0x01, 0x02, 0x03];
|
||||
assert_eq!(find_ac3_sync(&data), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ac3_sync_with_garbage_prefix() {
|
||||
let data = [0xFF, 0xFE, 0x0B, 0x77, 0x01, 0x02];
|
||||
assert_eq!(find_ac3_sync(&data), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ac3_sync_none() {
|
||||
let data = [0x0B, 0x78, 0x00, 0x00];
|
||||
assert_eq!(find_ac3_sync(&data), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_ac3_sync_empty() {
|
||||
let data: [u8; 0] = [];
|
||||
assert_eq!(find_ac3_sync(&data), None);
|
||||
}
|
||||
|
||||
// --- parse syncword → frame extracted ---
|
||||
|
||||
#[test]
|
||||
fn parse_syncword() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
|
||||
// AC3 frame starting with syncword
|
||||
let data = vec![0x0B, 0x77, 0x44, 0x55, 0x66, 0x77, 0x88];
|
||||
let pes = make_pes(data.clone(), Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].data, data);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_syncword_with_garbage_prefix() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
|
||||
// Garbage bytes before syncword
|
||||
let data = vec![0xFF, 0xFE, 0x0B, 0x77, 0x44, 0x55];
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
// Data should start from the syncword
|
||||
assert_eq!(frames[0].data[0], 0x0B);
|
||||
assert_eq!(frames[0].data[1], 0x77);
|
||||
assert_eq!(frames[0].data.len(), 4); // syncword + 2 payload bytes
|
||||
}
|
||||
|
||||
// --- all frames are keyframes ---
|
||||
|
||||
#[test]
|
||||
fn all_keyframes() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
|
||||
for i in 0..5 {
|
||||
let data = vec![0x0B, 0x77, 0x00, i];
|
||||
let pes = make_pes(data, Some(90000 * i as i64));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(frames[0].keyframe, "AC3 frame {} should be a keyframe", i);
|
||||
}
|
||||
}
|
||||
|
||||
// --- codec_private is None ---
|
||||
|
||||
#[test]
|
||||
fn codec_private_none() {
|
||||
let parser = Ac3Parser::new();
|
||||
assert!(parser.codec_private().is_none());
|
||||
}
|
||||
|
||||
// --- empty / too-short PES ---
|
||||
|
||||
#[test]
|
||||
fn parse_empty_pes() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
let pes = make_pes(Vec::new(), Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert!(frames.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_byte_pes() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
let pes = make_pes(vec![0x0B], Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert!(frames.is_empty());
|
||||
}
|
||||
|
||||
// --- PTS conversion ---
|
||||
|
||||
#[test]
|
||||
fn pts_conversion() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
let data = vec![0x0B, 0x77, 0x00, 0x01];
|
||||
// 45000 ticks = 0.5 seconds → 500_000_000 ns
|
||||
let pes = make_pes(data, Some(45000));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pts_ns, 500_000_000);
|
||||
}
|
||||
|
||||
// --- None PTS ---
|
||||
|
||||
#[test]
|
||||
fn no_pts() {
|
||||
let mut parser = Ac3Parser::new();
|
||||
let data = vec![0x0B, 0x77, 0x00, 0x01];
|
||||
let pes = make_pes(data, None);
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pts_ns, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,3 +24,60 @@ impl CodecParser for DtsParser {
|
||||
|
||||
fn codec_private(&self) -> Option<Vec<u8>> { None }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mux::ts::PesPacket;
|
||||
|
||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||
PesPacket { pid: 0x1100, pts, dts: None, data }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_basic_frame() {
|
||||
let mut parser = DtsParser::new();
|
||||
// DTS core syncword: 7F FE 80 01 + payload
|
||||
let data = vec![0x7F, 0xFE, 0x80, 0x01, 0xAA, 0xBB, 0xCC];
|
||||
let pes = make_pes(data.clone(), Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].data, data);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_keyframes() {
|
||||
let mut parser = DtsParser::new();
|
||||
for i in 0..3 {
|
||||
let data = vec![0x7F, 0xFE, 0x80, 0x01, i];
|
||||
let pes = make_pes(data, Some(90000 * i as i64));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(frames[0].keyframe, "DTS frame should always be keyframe");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_private_none() {
|
||||
let parser = DtsParser::new();
|
||||
assert!(parser.codec_private().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_pes() {
|
||||
let mut parser = DtsParser::new();
|
||||
let pes = make_pes(Vec::new(), Some(0));
|
||||
assert!(parser.parse(&pes).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_pts() {
|
||||
let mut parser = DtsParser::new();
|
||||
let pes = make_pes(vec![0x7F, 0xFE, 0x80, 0x01], None);
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pts_ns, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,3 +189,253 @@ pub fn skip_start_code(data: &[u8], pos: usize) -> Option<usize> {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mux::ts::PesPacket;
|
||||
|
||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||
PesPacket { pid: 0x1011, pts, dts: None, data }
|
||||
}
|
||||
|
||||
// --- find_start_code tests ---
|
||||
|
||||
#[test]
|
||||
fn find_start_code_3byte() {
|
||||
let data = [0x00, 0x00, 0x01, 0x65];
|
||||
assert_eq!(find_start_code(&data, 0), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_4byte() {
|
||||
let data = [0x00, 0x00, 0x00, 0x01, 0x65];
|
||||
// find_start_code looks for 00 00 01 pattern, which starts at offset 1 in a 4-byte start code
|
||||
assert_eq!(find_start_code(&data, 0), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_offset() {
|
||||
let data = [0xFF, 0xFF, 0x00, 0x00, 0x01, 0x09];
|
||||
assert_eq!(find_start_code(&data, 0), Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_none() {
|
||||
let data = [0x00, 0x00, 0x00, 0x00];
|
||||
assert_eq!(find_start_code(&data, 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_start_code_too_short() {
|
||||
let data = [0x00, 0x00];
|
||||
assert_eq!(find_start_code(&data, 0), None);
|
||||
}
|
||||
|
||||
// --- parse SPS+PPS → codec_private ---
|
||||
|
||||
#[test]
|
||||
fn parse_sps_pps() {
|
||||
let mut parser = H264Parser::new();
|
||||
|
||||
// Build PES with SPS (type 7) + PPS (type 8) + IDR slice (type 5)
|
||||
// SPS NAL: 0x67 = 0_11_00111 (nal_type = 7), followed by profile/compat/level + payload
|
||||
// PPS NAL: 0x68 = 0_11_01000 (nal_type = 8)
|
||||
let mut data = Vec::new();
|
||||
// SPS: 00 00 01 [67 42 00 1E <payload>]
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x67); // SPS
|
||||
data.extend_from_slice(&[0x42, 0x00, 0x1E, 0xAB, 0xCD]); // profile=0x42, compat=0x00, level=0x1E
|
||||
// PPS: 00 00 01 [68 <payload>]
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x68); // PPS
|
||||
data.extend_from_slice(&[0xCE, 0x01]);
|
||||
// IDR slice: 00 00 01 [65 <payload>]
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x65); // IDR
|
||||
data.extend_from_slice(&[0x88, 0x00, 0x10]);
|
||||
|
||||
let pes = make_pes(data, Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
// codec_private should now be available
|
||||
let cp = parser.codec_private();
|
||||
assert!(cp.is_some(), "codec_private should be Some after seeing SPS+PPS");
|
||||
let cp = cp.unwrap();
|
||||
|
||||
// AVCDecoderConfigurationRecord checks
|
||||
assert_eq!(cp[0], 1, "configurationVersion");
|
||||
assert_eq!(cp[1], 0x42, "profile from SPS[1]");
|
||||
assert_eq!(cp[2], 0x00, "compatibility from SPS[2]");
|
||||
assert_eq!(cp[3], 0x1E, "level from SPS[3]");
|
||||
assert_eq!(cp[4], 0xFF, "reserved + lengthSizeMinusOne=3");
|
||||
assert_eq!(cp[5], 0xE1, "reserved + numSPS=1");
|
||||
|
||||
// Frames should have been produced
|
||||
assert_eq!(frames.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_private_none_before_sps_pps() {
|
||||
let parser = H264Parser::new();
|
||||
assert!(parser.codec_private().is_none());
|
||||
}
|
||||
|
||||
// --- IDR keyframe detection ---
|
||||
|
||||
#[test]
|
||||
fn parse_idr_keyframe() {
|
||||
let mut parser = H264Parser::new();
|
||||
|
||||
// PES with IDR NAL (type 5 = 0x65)
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x65); // IDR slice (nal_type = 5)
|
||||
data.extend_from_slice(&[0x88, 0x00, 0x10, 0x20]);
|
||||
|
||||
let pes = make_pes(data, Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(frames[0].keyframe, "IDR slice should be detected as keyframe");
|
||||
}
|
||||
|
||||
// --- non-IDR → not keyframe ---
|
||||
|
||||
#[test]
|
||||
fn parse_non_idr() {
|
||||
let mut parser = H264Parser::new();
|
||||
|
||||
// PES with non-IDR slice (type 1 = 0x61 or 0x41)
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x41); // non-IDR coded slice (nal_type = 1)
|
||||
data.extend_from_slice(&[0x9A, 0x00, 0x10]);
|
||||
|
||||
let pes = make_pes(data, Some(180000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(!frames[0].keyframe, "non-IDR slice should not be keyframe");
|
||||
}
|
||||
|
||||
// --- length prefix conversion ---
|
||||
|
||||
#[test]
|
||||
fn length_prefix_conversion() {
|
||||
let mut parser = H264Parser::new();
|
||||
|
||||
// PES with a single non-IDR NAL
|
||||
let nal_payload = [0x41, 0xAA, 0xBB, 0xCC, 0xDD]; // type 1, 5 bytes
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&nal_payload);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
let frame_data = &frames[0].data;
|
||||
|
||||
// Should start with 4-byte big-endian length prefix
|
||||
assert!(frame_data.len() >= 4, "frame data should have length prefix");
|
||||
let length = u32::from_be_bytes([frame_data[0], frame_data[1], frame_data[2], frame_data[3]]);
|
||||
assert_eq!(length as usize, nal_payload.len(), "length prefix should match NAL size");
|
||||
|
||||
// Followed by the NAL data itself
|
||||
assert_eq!(&frame_data[4..], &nal_payload);
|
||||
|
||||
// No start code (00 00 01) should appear in the output
|
||||
for i in 0..frame_data.len().saturating_sub(2) {
|
||||
let is_sc = frame_data[i] == 0x00 && frame_data[i + 1] == 0x00 && frame_data[i + 2] == 0x01;
|
||||
assert!(!is_sc, "output should not contain Annex B start codes");
|
||||
}
|
||||
}
|
||||
|
||||
// --- SPS/PPS/AUD are stripped from frame data ---
|
||||
|
||||
#[test]
|
||||
fn sps_pps_aud_stripped_from_frame_data() {
|
||||
let mut parser = H264Parser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
// AUD (type 9)
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x09);
|
||||
data.push(0xF0);
|
||||
// SPS (type 7)
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x67);
|
||||
data.extend_from_slice(&[0x42, 0x00, 0x1E, 0xAB]);
|
||||
// PPS (type 8)
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x68);
|
||||
data.extend_from_slice(&[0xCE, 0x01]);
|
||||
// IDR (type 5) - only this should appear in frame data
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x65);
|
||||
data.extend_from_slice(&[0x88, 0x00]);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
|
||||
// Frame data should only contain the IDR NAL (length-prefixed)
|
||||
let fd = &frames[0].data;
|
||||
let length = u32::from_be_bytes([fd[0], fd[1], fd[2], fd[3]]);
|
||||
// IDR NAL is 0x65, 0x88 (trailing 0x00 is stripped as potential start code prefix)
|
||||
assert_eq!(length, 2);
|
||||
assert_eq!(fd[4], 0x65); // IDR NAL type byte
|
||||
}
|
||||
|
||||
// --- PTS conversion ---
|
||||
|
||||
#[test]
|
||||
fn pts_conversion() {
|
||||
let mut parser = H264Parser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x41);
|
||||
data.extend_from_slice(&[0x00, 0x10]);
|
||||
|
||||
// PTS = 90000 (1 second at 90kHz) → 1_000_000_000 ns
|
||||
let pes = make_pes(data, Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
|
||||
// --- empty PES ---
|
||||
|
||||
#[test]
|
||||
fn parse_empty_pes() {
|
||||
let mut parser = H264Parser::new();
|
||||
let pes = make_pes(Vec::new(), Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert!(frames.is_empty());
|
||||
}
|
||||
|
||||
// --- DTS preferred over PTS when present ---
|
||||
|
||||
#[test]
|
||||
fn dts_preferred_over_pts() {
|
||||
let mut parser = H264Parser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.push(0x41);
|
||||
data.extend_from_slice(&[0x00, 0x10]);
|
||||
|
||||
let pes = PesPacket {
|
||||
pid: 0x1011,
|
||||
pts: Some(180000), // 2 seconds
|
||||
dts: Some(90000), // 1 second
|
||||
data,
|
||||
};
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
// DTS should be used, not PTS
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,3 +169,259 @@ impl CodecParser for HevcParser {
|
||||
Some(record)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mux::ts::PesPacket;
|
||||
|
||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||
PesPacket { pid: 0x1011, pts, dts: None, data }
|
||||
}
|
||||
|
||||
/// Build an HEVC NAL header (2 bytes). Type is bits 1-6 of first byte.
|
||||
/// Format: forbidden(1) | type(6) | layer_id_high(1) || layer_id_low(5) | tid(3)
|
||||
fn hevc_nal_header(nal_type: u8) -> [u8; 2] {
|
||||
[(nal_type & 0x3F) << 1, 0x01] // tid=1
|
||||
}
|
||||
|
||||
// --- VPS+SPS+PPS → codec_private ---
|
||||
|
||||
#[test]
|
||||
fn parse_vps_sps_pps() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
// VPS (type 32)
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
let vps_hdr = hevc_nal_header(32);
|
||||
data.extend_from_slice(&vps_hdr);
|
||||
data.extend_from_slice(&[0xAA, 0xBB, 0xCC]); // VPS payload
|
||||
|
||||
// SPS (type 33)
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
let sps_hdr = hevc_nal_header(33);
|
||||
data.extend_from_slice(&sps_hdr);
|
||||
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x09, 0x0A, 0x0B, 0x0C, 0x0D]); // SPS payload (>12 bytes for level)
|
||||
|
||||
// PPS (type 34)
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
let pps_hdr = hevc_nal_header(34);
|
||||
data.extend_from_slice(&pps_hdr);
|
||||
data.extend_from_slice(&[0xDD, 0xEE]); // PPS payload
|
||||
|
||||
// IRAP slice (type 19 = IDR_W_RADL) so a frame is emitted
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
let idr_hdr = hevc_nal_header(19);
|
||||
data.extend_from_slice(&idr_hdr);
|
||||
data.extend_from_slice(&[0x10, 0x20, 0x30]);
|
||||
|
||||
let pes = make_pes(data, Some(90000));
|
||||
let _frames = parser.parse(&pes);
|
||||
|
||||
let cp = parser.codec_private();
|
||||
assert!(cp.is_some(), "codec_private should be Some after VPS+SPS+PPS");
|
||||
|
||||
let cp = cp.unwrap();
|
||||
// configurationVersion = 1
|
||||
assert_eq!(cp[0], 1);
|
||||
// numOfArrays = 3 (VPS, SPS, PPS)
|
||||
assert_eq!(cp[22], 3);
|
||||
// Should be longer than the minimal header (23 bytes) + array entries
|
||||
assert!(cp.len() > 23, "codec_private should contain VPS+SPS+PPS data");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_private_none_before_params() {
|
||||
let parser = HevcParser::new();
|
||||
assert!(parser.codec_private().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_private_none_missing_pps() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
// Only VPS + SPS, no PPS
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(32));
|
||||
data.extend_from_slice(&[0xAA, 0xBB]);
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(33));
|
||||
data.extend_from_slice(&[0x01, 0x02, 0x03, 0x04]);
|
||||
// Add a slice so parse doesn't return empty
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(1)); // TRAIL_R
|
||||
data.extend_from_slice(&[0x10, 0x20]);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
parser.parse(&pes);
|
||||
assert!(parser.codec_private().is_none(), "should be None without PPS");
|
||||
}
|
||||
|
||||
// --- IRAP keyframe detection ---
|
||||
|
||||
#[test]
|
||||
fn parse_irap_keyframe_idr_w_radl() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
// IDR_W_RADL = type 19
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(19));
|
||||
data.extend_from_slice(&[0x10, 0x20, 0x30]);
|
||||
|
||||
let pes = make_pes(data, Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(frames[0].keyframe, "IDR_W_RADL (type 19) should be keyframe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_irap_keyframe_bla() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
// BLA_W_LP = type 16
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(16));
|
||||
data.extend_from_slice(&[0x10, 0x20]);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(frames[0].keyframe, "BLA_W_LP (type 16) should be keyframe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_irap_keyframe_cra() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
// CRA_NUT = type 21
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(21));
|
||||
data.extend_from_slice(&[0x10, 0x20]);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(frames[0].keyframe, "CRA (type 21) should be keyframe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_irap_type_23() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
// RSV_IRAP_VCL23 = type 23 (upper boundary)
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(23));
|
||||
data.extend_from_slice(&[0x10, 0x20]);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(frames[0].keyframe, "type 23 should be keyframe");
|
||||
}
|
||||
|
||||
// --- non-IRAP (trailing) → not keyframe ---
|
||||
|
||||
#[test]
|
||||
fn parse_trailing_not_keyframe() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
// TRAIL_R = type 1
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(1));
|
||||
data.extend_from_slice(&[0x10, 0x20, 0x30]);
|
||||
|
||||
let pes = make_pes(data, Some(180000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(!frames[0].keyframe, "TRAIL_R (type 1) should not be keyframe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_tsa_not_keyframe() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
// TSA_N = type 2
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(2));
|
||||
data.extend_from_slice(&[0x10, 0x20]);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(!frames[0].keyframe, "TSA_N (type 2) should not be keyframe");
|
||||
}
|
||||
|
||||
// --- VPS/SPS/PPS stripped from frame data ---
|
||||
|
||||
#[test]
|
||||
fn param_sets_stripped_from_frame() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
// VPS
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(32));
|
||||
data.extend_from_slice(&[0xAA]);
|
||||
// SPS
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(33));
|
||||
data.extend_from_slice(&[0xBB]);
|
||||
// PPS
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(34));
|
||||
data.extend_from_slice(&[0xCC]);
|
||||
// IDR slice
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
let idr_hdr = hevc_nal_header(19);
|
||||
data.extend_from_slice(&idr_hdr);
|
||||
data.extend_from_slice(&[0x10, 0x20]);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
|
||||
// Frame data should only have the IDR NAL (length-prefixed)
|
||||
let fd = &frames[0].data;
|
||||
let length = u32::from_be_bytes([fd[0], fd[1], fd[2], fd[3]]);
|
||||
// IDR NAL = 2 bytes header + 2 bytes payload = 4 bytes
|
||||
assert_eq!(length as usize + 4, fd.len(), "frame should contain exactly one length-prefixed NAL");
|
||||
}
|
||||
|
||||
// --- empty PES ---
|
||||
|
||||
#[test]
|
||||
fn parse_empty_pes() {
|
||||
let mut parser = HevcParser::new();
|
||||
let pes = make_pes(Vec::new(), Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert!(frames.is_empty());
|
||||
}
|
||||
|
||||
// --- PTS conversion ---
|
||||
|
||||
#[test]
|
||||
fn pts_conversion() {
|
||||
let mut parser = HevcParser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01]);
|
||||
data.extend_from_slice(&hevc_nal_header(1));
|
||||
data.extend_from_slice(&[0x10, 0x20]);
|
||||
|
||||
let pes = make_pes(data, Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,3 +23,51 @@ impl CodecParser for PgsParser {
|
||||
|
||||
fn codec_private(&self) -> Option<Vec<u8>> { None }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mux::ts::PesPacket;
|
||||
|
||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||
PesPacket { pid: 0x1200, pts, dts: None, data }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_basic_segment() {
|
||||
let mut parser = PgsParser::new();
|
||||
// PGS segment data (PCS = presentation composition segment)
|
||||
let data = vec![0x16, 0x00, 0x00, 0x11, 0x01, 0x02, 0x03];
|
||||
let pes = make_pes(data.clone(), Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].data, data);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_keyframes() {
|
||||
let mut parser = PgsParser::new();
|
||||
for i in 0..3 {
|
||||
let data = vec![0x16, 0x00, i];
|
||||
let pes = make_pes(data, Some(90000 * i as i64));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(frames[0].keyframe, "PGS segment should always be keyframe");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_private_none() {
|
||||
let parser = PgsParser::new();
|
||||
assert!(parser.codec_private().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_pes() {
|
||||
let mut parser = PgsParser::new();
|
||||
let pes = make_pes(Vec::new(), Some(0));
|
||||
assert!(parser.parse(&pes).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,3 +25,51 @@ impl CodecParser for TrueHdParser {
|
||||
|
||||
fn codec_private(&self) -> Option<Vec<u8>> { None }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mux::ts::PesPacket;
|
||||
|
||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||
PesPacket { pid: 0x1100, pts, dts: None, data }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_basic_frame() {
|
||||
let mut parser = TrueHdParser::new();
|
||||
// TrueHD major sync: F8 72 6F BA (at 4-byte aligned position) + payload
|
||||
let data = vec![0xF8, 0x72, 0x6F, 0xBA, 0x01, 0x02, 0x03, 0x04];
|
||||
let pes = make_pes(data.clone(), Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].data, data);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_keyframes() {
|
||||
let mut parser = TrueHdParser::new();
|
||||
for i in 0..3 {
|
||||
let data = vec![0xF8, 0x72, 0x6F, 0xBA, i];
|
||||
let pes = make_pes(data, Some(90000 * i as i64));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(frames[0].keyframe, "TrueHD frame should always be keyframe");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_private_none() {
|
||||
let parser = TrueHdParser::new();
|
||||
assert!(parser.codec_private().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_pes() {
|
||||
let mut parser = TrueHdParser::new();
|
||||
let pes = make_pes(Vec::new(), Some(0));
|
||||
assert!(parser.parse(&pes).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,3 +120,225 @@ fn find_next_sc(data: &[u8], from: usize) -> Option<usize> {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::mux::ts::PesPacket;
|
||||
|
||||
fn make_pes(data: Vec<u8>, pts: Option<i64>) -> PesPacket {
|
||||
PesPacket { pid: 0x1011, pts, dts: None, data }
|
||||
}
|
||||
|
||||
/// Build a VC-1 PES with sequence header + entry point + frame start code.
|
||||
fn build_vc1_iframe_pes() -> Vec<u8> {
|
||||
let mut data = Vec::new();
|
||||
// Sequence header: 00 00 01 0F + payload
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SC_SEQUENCE_HEADER]);
|
||||
data.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF]);
|
||||
// Entry point: 00 00 01 0E + payload
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SC_ENTRY_POINT]);
|
||||
data.extend_from_slice(&[0x11, 0x22, 0x33, 0x44]);
|
||||
// Frame: 00 00 01 0D + payload
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SC_FRAME]);
|
||||
data.extend_from_slice(&[0x55, 0x66, 0x77, 0x88, 0x99]);
|
||||
data
|
||||
}
|
||||
|
||||
// --- sequence header detection ---
|
||||
|
||||
#[test]
|
||||
fn parse_sequence_header() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
|
||||
let data = build_vc1_iframe_pes();
|
||||
let pes = make_pes(data, Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
// Sequence header present → keyframe
|
||||
assert!(frames[0].keyframe, "PES with sequence header should be keyframe");
|
||||
// seq_header should be stored internally
|
||||
assert!(parser.seq_header.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_entry_point() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
|
||||
let data = build_vc1_iframe_pes();
|
||||
let pes = make_pes(data, Some(0));
|
||||
parser.parse(&pes);
|
||||
|
||||
assert!(parser.entry_point.is_some());
|
||||
}
|
||||
|
||||
// --- codec_private is BITMAPINFOHEADER (40+ bytes) ---
|
||||
|
||||
#[test]
|
||||
fn codec_private_bitmapinfoheader() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
|
||||
let data = build_vc1_iframe_pes();
|
||||
let pes = make_pes(data, Some(0));
|
||||
parser.parse(&pes);
|
||||
|
||||
let cp = parser.codec_private();
|
||||
assert!(cp.is_some(), "codec_private should be Some after seq header + entry point");
|
||||
|
||||
let cp = cp.unwrap();
|
||||
// BITMAPINFOHEADER is 40 bytes + extra data
|
||||
assert!(cp.len() >= 40, "codec_private should be at least 40 bytes (BITMAPINFOHEADER)");
|
||||
|
||||
// biSize (first 4 bytes, little-endian) should equal total length
|
||||
let bi_size = u32::from_le_bytes([cp[0], cp[1], cp[2], cp[3]]);
|
||||
assert_eq!(bi_size as usize, cp.len(), "biSize should match total codec_private length");
|
||||
|
||||
// biCompression = "WVC1" at offset 16
|
||||
assert_eq!(&cp[16..20], b"WVC1", "FOURCC should be WVC1");
|
||||
|
||||
// biWidth at offset 4 (little-endian u32) = 1920
|
||||
let width = u32::from_le_bytes([cp[4], cp[5], cp[6], cp[7]]);
|
||||
assert_eq!(width, 1920);
|
||||
|
||||
// biHeight at offset 8 (little-endian u32) = 1080
|
||||
let height = u32::from_le_bytes([cp[8], cp[9], cp[10], cp[11]]);
|
||||
assert_eq!(height, 1080);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_private_none_before_data() {
|
||||
let parser = Vc1Parser::new();
|
||||
assert!(parser.codec_private().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn codec_private_none_missing_entry_point() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
|
||||
// Only sequence header, no entry point
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SC_SEQUENCE_HEADER]);
|
||||
data.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SC_FRAME]);
|
||||
data.extend_from_slice(&[0x55, 0x66]);
|
||||
|
||||
let pes = make_pes(data, Some(0));
|
||||
parser.parse(&pes);
|
||||
|
||||
assert!(parser.codec_private().is_none(), "should be None without entry point");
|
||||
}
|
||||
|
||||
// --- frame without sequence header → not keyframe ---
|
||||
|
||||
#[test]
|
||||
fn parse_non_keyframe() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
|
||||
// PES with only a frame start code (no sequence header)
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SC_FRAME]);
|
||||
data.extend_from_slice(&[0x55, 0x66, 0x77]);
|
||||
|
||||
let pes = make_pes(data, Some(180000));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert!(!frames[0].keyframe, "frame without sequence header should not be keyframe");
|
||||
}
|
||||
|
||||
// --- frame data starts from frame start code ---
|
||||
|
||||
#[test]
|
||||
fn frame_data_starts_at_frame_sc() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
|
||||
let data = build_vc1_iframe_pes();
|
||||
let pes = make_pes(data.clone(), Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
|
||||
assert_eq!(frames.len(), 1);
|
||||
// Frame data should start with the frame start code (00 00 01 0D)
|
||||
assert!(frames[0].data.len() >= 4);
|
||||
assert_eq!(&frames[0].data[0..4], &[0x00, 0x00, 0x01, SC_FRAME]);
|
||||
}
|
||||
|
||||
// --- empty PES ---
|
||||
|
||||
#[test]
|
||||
fn parse_empty_pes() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
let pes = make_pes(Vec::new(), Some(0));
|
||||
let frames = parser.parse(&pes);
|
||||
assert!(frames.is_empty());
|
||||
}
|
||||
|
||||
// --- PTS conversion ---
|
||||
|
||||
#[test]
|
||||
fn pts_conversion() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SC_FRAME]);
|
||||
data.extend_from_slice(&[0x55, 0x66]);
|
||||
|
||||
let pes = make_pes(data, Some(90000));
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
|
||||
// --- DTS preferred over PTS ---
|
||||
|
||||
#[test]
|
||||
fn dts_preferred_over_pts() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
|
||||
let mut data = Vec::new();
|
||||
data.extend_from_slice(&[0x00, 0x00, 0x01, SC_FRAME]);
|
||||
data.extend_from_slice(&[0x55, 0x66]);
|
||||
|
||||
let pes = PesPacket {
|
||||
pid: 0x1011,
|
||||
pts: Some(180000),
|
||||
dts: Some(90000),
|
||||
data,
|
||||
};
|
||||
let frames = parser.parse(&pes);
|
||||
assert_eq!(frames.len(), 1);
|
||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||
}
|
||||
|
||||
// --- find_next_sc utility ---
|
||||
|
||||
#[test]
|
||||
fn find_next_sc_basic() {
|
||||
let data = [0xAA, 0x00, 0x00, 0x01, 0x0D, 0xBB];
|
||||
assert_eq!(find_next_sc(&data, 0), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_next_sc_none() {
|
||||
let data = [0xAA, 0xBB, 0xCC];
|
||||
assert_eq!(find_next_sc(&data, 0), None);
|
||||
}
|
||||
|
||||
// --- codec_private extra data contains seq header + entry point ---
|
||||
|
||||
#[test]
|
||||
fn codec_private_contains_extra_data() {
|
||||
let mut parser = Vc1Parser::new();
|
||||
|
||||
let data = build_vc1_iframe_pes();
|
||||
let pes = make_pes(data, Some(0));
|
||||
parser.parse(&pes);
|
||||
|
||||
let cp = parser.codec_private().unwrap();
|
||||
// After the 40-byte BITMAPINFOHEADER, we should have seq_header + entry_point data
|
||||
let extra = &cp[40..];
|
||||
assert!(!extra.is_empty(), "extra data after BITMAPINFOHEADER should not be empty");
|
||||
// Extra data should start with the sequence header start code
|
||||
assert_eq!(&extra[0..4], &[0x00, 0x00, 0x01, SC_SEQUENCE_HEADER]);
|
||||
}
|
||||
}
|
||||
|
||||
+121
@@ -413,4 +413,125 @@ mod tests {
|
||||
// EBML header: 1A 45 DF A3, then 8-byte size, then content
|
||||
assert_eq!(&data[0..4], &[0x1A, 0x45, 0xDF, 0xA3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_read_id_roundtrip() {
|
||||
// 1-byte IDs have high bit set (0x80..=0xFF)
|
||||
for &id in &[0x80u32, 0xA3, 0xFF] {
|
||||
let mut buf = Vec::new();
|
||||
write_id(&mut buf, id).unwrap();
|
||||
assert_eq!(buf.len(), 1);
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let (read_back, consumed) = read_id(&mut cursor).unwrap();
|
||||
assert_eq!(read_back, id, "1-byte ID roundtrip failed for 0x{:X}", id);
|
||||
assert_eq!(consumed, 1);
|
||||
}
|
||||
// 2-byte IDs (0x4000..=0x7FFF)
|
||||
for &id in &[0x4286u32, 0x4282, 0x7FFF] {
|
||||
let mut buf = Vec::new();
|
||||
write_id(&mut buf, id).unwrap();
|
||||
assert_eq!(buf.len(), 2);
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let (read_back, consumed) = read_id(&mut cursor).unwrap();
|
||||
assert_eq!(read_back, id, "2-byte ID roundtrip failed for 0x{:X}", id);
|
||||
assert_eq!(consumed, 2);
|
||||
}
|
||||
// 3-byte IDs (0x200000..=0x3FFFFF)
|
||||
for &id in &[0x22B59Cu32, 0x23E383] {
|
||||
let mut buf = Vec::new();
|
||||
write_id(&mut buf, id).unwrap();
|
||||
assert_eq!(buf.len(), 3);
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let (read_back, consumed) = read_id(&mut cursor).unwrap();
|
||||
assert_eq!(read_back, id, "3-byte ID roundtrip failed for 0x{:X}", id);
|
||||
assert_eq!(consumed, 3);
|
||||
}
|
||||
// 4-byte IDs (0x10000000..=0x1FFFFFFF)
|
||||
for &id in &[EBML, SEGMENT, TRACKS, CLUSTER] {
|
||||
let mut buf = Vec::new();
|
||||
write_id(&mut buf, id).unwrap();
|
||||
assert_eq!(buf.len(), 4);
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let (read_back, consumed) = read_id(&mut cursor).unwrap();
|
||||
assert_eq!(read_back, id, "4-byte ID roundtrip failed for 0x{:X}", id);
|
||||
assert_eq!(consumed, 4);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_read_size_roundtrip() {
|
||||
let test_sizes: &[u64] = &[0, 1, 0x7E, 127, 128, 0x3FFE, 16383, 16384, 0x1FFFFE, 0x0FFFFFFE, 0x1_0000_0000];
|
||||
for &size in test_sizes {
|
||||
let mut buf = Vec::new();
|
||||
write_size(&mut buf, size).unwrap();
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let (read_back, _consumed) = read_size(&mut cursor).unwrap();
|
||||
assert_eq!(read_back, size, "size roundtrip failed for {}", size);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_read_uint_roundtrip() {
|
||||
let test_vals: &[u64] = &[0, 1, 127, 255, 256, 0xFFFF, 0xFF_FFFF, 0xFFFF_FFFF, 1_000_000_000_000];
|
||||
let test_id = EBML_VERSION;
|
||||
for &val in test_vals {
|
||||
let mut buf = Vec::new();
|
||||
write_uint(&mut buf, test_id, val).unwrap();
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let (id, id_len) = read_id(&mut cursor).unwrap();
|
||||
assert_eq!(id, test_id);
|
||||
let (size, _) = read_size(&mut cursor).unwrap();
|
||||
let read_val = read_uint_val(&mut cursor, size as usize).unwrap();
|
||||
assert_eq!(read_val, val, "uint roundtrip failed for {}", val);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_read_string_roundtrip() {
|
||||
let test_strings = &["", "matroska", "freemkv", "Hello, World!", "unicode: \u{1F600}"];
|
||||
let test_id = EBML_DOC_TYPE;
|
||||
for &s in test_strings {
|
||||
let mut buf = Vec::new();
|
||||
write_string(&mut buf, test_id, s).unwrap();
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let (id, _) = read_id(&mut cursor).unwrap();
|
||||
assert_eq!(id, test_id);
|
||||
let (size, _) = read_size(&mut cursor).unwrap();
|
||||
let read_s = read_string_val(&mut cursor, size as usize).unwrap();
|
||||
assert_eq!(read_s, s, "string roundtrip failed for {:?}", s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_read_float_roundtrip() {
|
||||
let test_vals: &[f64] = &[0.0, 1.0, -1.0, 3.14159265358979, 48000.0, 7200000.0, f64::MIN, f64::MAX];
|
||||
let test_id = DURATION;
|
||||
for &val in test_vals {
|
||||
let mut buf = Vec::new();
|
||||
write_float(&mut buf, test_id, val).unwrap();
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let (id, _) = read_id(&mut cursor).unwrap();
|
||||
assert_eq!(id, test_id);
|
||||
let (size, _) = read_size(&mut cursor).unwrap();
|
||||
assert_eq!(size, 8);
|
||||
let read_val = read_float_val(&mut cursor, size as usize).unwrap();
|
||||
assert_eq!(read_val.to_bits(), val.to_bits(), "float roundtrip failed for {}", val);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_size() {
|
||||
let mut buf = Vec::new();
|
||||
write_unknown_size(&mut buf).unwrap();
|
||||
assert_eq!(buf.len(), 8);
|
||||
assert_eq!(buf[0], 0x01);
|
||||
for &b in &buf[1..] {
|
||||
assert_eq!(b, 0xFF, "unknown size bytes should all be 0xFF after first byte");
|
||||
}
|
||||
// Reading it back should yield u64::MAX
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let (size, consumed) = read_size(&mut cursor).unwrap();
|
||||
assert_eq!(size, u64::MAX);
|
||||
assert_eq!(consumed, 8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,6 +198,81 @@ impl Read for IsoStream {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use crate::sector::SectorReader;
|
||||
|
||||
#[test]
|
||||
fn iso_reader_read_sectors() {
|
||||
// Create a temp file with known sector data
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("libfreemkv_test_iso_sectors.iso");
|
||||
let path_str = path.to_str().unwrap();
|
||||
|
||||
// Write 4 sectors of known data
|
||||
{
|
||||
let mut f = File::create(&path).unwrap();
|
||||
for sector_idx in 0u8..4 {
|
||||
let mut sector = [sector_idx; SECTOR_SIZE as usize];
|
||||
sector[0] = sector_idx;
|
||||
sector[2047] = sector_idx.wrapping_mul(0x37);
|
||||
f.write_all(§or).unwrap();
|
||||
}
|
||||
f.flush().unwrap();
|
||||
}
|
||||
|
||||
let mut reader = IsoSectorReader::open(path_str).unwrap();
|
||||
assert_eq!(reader.capacity(), 4);
|
||||
|
||||
// Read sector 0
|
||||
let mut buf = [0u8; SECTOR_SIZE as usize];
|
||||
let n = reader.read_sectors(0, 1, &mut buf).unwrap();
|
||||
assert_eq!(n, SECTOR_SIZE as usize);
|
||||
assert_eq!(buf[0], 0);
|
||||
assert_eq!(buf[2047], 0u8.wrapping_mul(0x37));
|
||||
|
||||
// Read sector 2
|
||||
let n = reader.read_sectors(2, 1, &mut buf).unwrap();
|
||||
assert_eq!(n, SECTOR_SIZE as usize);
|
||||
assert_eq!(buf[0], 2);
|
||||
assert_eq!(buf[1], 2); // filled with sector_idx
|
||||
assert_eq!(buf[2047], 2u8.wrapping_mul(0x37));
|
||||
|
||||
// Read 2 sectors at once (sectors 1 and 2)
|
||||
let mut buf2 = [0u8; SECTOR_SIZE as usize * 2];
|
||||
let n = reader.read_sectors(1, 2, &mut buf2).unwrap();
|
||||
assert_eq!(n, SECTOR_SIZE as usize * 2);
|
||||
assert_eq!(buf2[0], 1); // sector 1 first byte
|
||||
assert_eq!(buf2[SECTOR_SIZE as usize], 2); // sector 2 first byte
|
||||
|
||||
// Clean up
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iso_reader_capacity() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("libfreemkv_test_iso_capacity.iso");
|
||||
let path_str = path.to_str().unwrap();
|
||||
|
||||
// Write exactly 10 sectors
|
||||
{
|
||||
let mut f = File::create(&path).unwrap();
|
||||
let data = vec![0u8; SECTOR_SIZE as usize * 10];
|
||||
f.write_all(&data).unwrap();
|
||||
f.flush().unwrap();
|
||||
}
|
||||
|
||||
let reader = IsoSectorReader::open(path_str).unwrap();
|
||||
assert_eq!(reader.capacity(), 10);
|
||||
|
||||
// Clean up
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for IsoStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self.writer.as_mut() {
|
||||
|
||||
@@ -41,3 +41,48 @@ impl Read for NullStream {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported, "null stream is write-only"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn null_counts_bytes() {
|
||||
let mut ns = NullStream::new();
|
||||
assert_eq!(ns.bytes_written(), 0);
|
||||
ns.write_all(&[0u8; 100]).unwrap();
|
||||
assert_eq!(ns.bytes_written(), 100);
|
||||
ns.write_all(&[1u8; 50]).unwrap();
|
||||
assert_eq!(ns.bytes_written(), 150);
|
||||
// Single write returns correct count
|
||||
let n = ns.write(&[0u8; 200]).unwrap();
|
||||
assert_eq!(n, 200);
|
||||
assert_eq!(ns.bytes_written(), 350);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_read_errors() {
|
||||
let mut ns = NullStream::new();
|
||||
let mut buf = [0u8; 10];
|
||||
let err = ns.read(&mut buf).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_finish_ok() {
|
||||
let mut ns = NullStream::new();
|
||||
ns.write_all(&[0u8; 1000]).unwrap();
|
||||
ns.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_implements_iostream() {
|
||||
let ns = NullStream::new();
|
||||
let mut boxed: Box<dyn IOStream> = Box::new(ns);
|
||||
boxed.write_all(&[0u8; 50]).unwrap();
|
||||
let info = boxed.info();
|
||||
assert_eq!(info.streams.len(), 0);
|
||||
boxed.finish().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,3 +75,26 @@ impl Write for StdioStream {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[test]
|
||||
fn stdio_output_write_errors_on_read() {
|
||||
let mut stream = StdioStream::output();
|
||||
let mut buf = [0u8; 10];
|
||||
let err = stream.read(&mut buf).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
|
||||
assert!(err.to_string().contains("cannot read"), "got: {}", err);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdio_input_read_errors_on_write() {
|
||||
let mut stream = StdioStream::input();
|
||||
let err = stream.write(&[0u8; 10]).unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::Unsupported);
|
||||
assert!(err.to_string().contains("cannot write"), "got: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user