CSS crypto tests + DVD pipeline fully wired
- CSS roundtrip tests: decrypt_key determinism, descramble XOR roundtrip - CSS table verification: TAB1 is permutation, TAB4 is bit-reversal involution - DVD scan pipeline confirmed: scan_dvd_titles, CSS crack, ContentReader descramble - 229 tests, all passing
This commit is contained in:
+138
@@ -194,4 +194,142 @@ mod tests {
|
|||||||
assert_ne!(result, key);
|
assert_ne!(result, key);
|
||||||
assert_ne!(result, [0u8; 5]);
|
assert_ne!(result, [0u8; 5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test 1: css_decrypt_key_roundtrip
|
||||||
|
///
|
||||||
|
/// decrypt_key is not a simple encrypt/decrypt pair — it is a one-way mangling
|
||||||
|
/// function. However, we can verify consistency: calling it twice with the same
|
||||||
|
/// parameters produces the same output, and varying the invert byte changes
|
||||||
|
/// the LFSR0 contribution predictably.
|
||||||
|
#[test]
|
||||||
|
fn css_decrypt_key_roundtrip() {
|
||||||
|
let keys: &[[u8; 5]] = &[
|
||||||
|
[0x12, 0x34, 0x56, 0x78, 0x9A],
|
||||||
|
[0x00, 0x00, 0x00, 0x00, 0x00],
|
||||||
|
[0xFF, 0xFF, 0xFF, 0xFF, 0xFF],
|
||||||
|
[0xAB, 0xCD, 0xEF, 0x01, 0x23],
|
||||||
|
];
|
||||||
|
let crypted_inputs: &[[u8; 5]] = &[
|
||||||
|
[0x11, 0x22, 0x33, 0x44, 0x55],
|
||||||
|
[0xAA, 0xBB, 0xCC, 0xDD, 0xEE],
|
||||||
|
[0x00, 0x00, 0x00, 0x00, 0x00],
|
||||||
|
];
|
||||||
|
|
||||||
|
for key in keys {
|
||||||
|
for crypted in crypted_inputs {
|
||||||
|
// decrypt_key with invert=0x00 and invert=0xFF should give different results
|
||||||
|
let r0 = decrypt_key(0x00, key, crypted);
|
||||||
|
let rff = decrypt_key(0xFF, key, crypted);
|
||||||
|
|
||||||
|
// The two results differ because the invert byte XORs the LFSR0 output
|
||||||
|
// They should not be equal (except by extreme coincidence)
|
||||||
|
// More importantly, both should be deterministic
|
||||||
|
let r0_again = decrypt_key(0x00, key, crypted);
|
||||||
|
let rff_again = decrypt_key(0xFF, key, crypted);
|
||||||
|
assert_eq!(r0, r0_again, "decrypt_key(0x00) not deterministic");
|
||||||
|
assert_eq!(rff, rff_again, "decrypt_key(0xFF) not deterministic");
|
||||||
|
|
||||||
|
// With different invert values, the keystream differs
|
||||||
|
assert_ne!(r0, rff, "invert=0x00 and 0xFF gave same result for key {:?}", key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test 2: css_descramble_produces_valid_mpeg2
|
||||||
|
///
|
||||||
|
/// descramble_sector XORs a keystream into bytes 128..2048. Calling it
|
||||||
|
/// twice with the same key and restored scramble flag should roundtrip,
|
||||||
|
/// since XOR is its own inverse.
|
||||||
|
#[test]
|
||||||
|
fn css_descramble_produces_valid_mpeg2() {
|
||||||
|
let title_key = [0x42, 0x13, 0x37, 0xBE, 0xEF];
|
||||||
|
|
||||||
|
// Build a sector with MPEG-2 pack header and PES header
|
||||||
|
let mut sector = vec![0x00u8; 2048];
|
||||||
|
// Pack header at byte 0
|
||||||
|
sector[0] = 0x00;
|
||||||
|
sector[1] = 0x00;
|
||||||
|
sector[2] = 0x01;
|
||||||
|
sector[3] = 0xBA;
|
||||||
|
// Scramble flag at byte 0x14
|
||||||
|
sector[0x14] = 0x30;
|
||||||
|
// Sector seed at bytes 0x54-0x58
|
||||||
|
sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
|
||||||
|
// PES header at byte 128
|
||||||
|
sector[0x80] = 0x00;
|
||||||
|
sector[0x81] = 0x00;
|
||||||
|
sector[0x82] = 0x01;
|
||||||
|
sector[0x83] = 0xE0;
|
||||||
|
// Fill some content in the encrypted region
|
||||||
|
for i in 0x84..2048 {
|
||||||
|
sector[i] = (i & 0xFF) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
let original = sector.clone();
|
||||||
|
|
||||||
|
// First descramble: "encrypts" by XORing keystream
|
||||||
|
descramble_sector(&title_key, &mut sector);
|
||||||
|
// Flag should be cleared
|
||||||
|
assert_eq!(sector[0x14] & 0x30, 0x00, "scramble flag not cleared after first descramble");
|
||||||
|
// Encrypted region should differ
|
||||||
|
assert_ne!(§or[0x80..0x84], &original[0x80..0x84],
|
||||||
|
"encrypted region unchanged after descramble");
|
||||||
|
|
||||||
|
// Restore the scramble flag and sector seed for second pass
|
||||||
|
sector[0x14] = 0x30;
|
||||||
|
|
||||||
|
// Second descramble: XOR again = roundtrip
|
||||||
|
descramble_sector(&title_key, &mut sector);
|
||||||
|
// Now the encrypted region should match original
|
||||||
|
assert_eq!(§or[0x80..2048], &original[0x80..2048],
|
||||||
|
"double descramble did not roundtrip");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test 4: css_tab1_relationship
|
||||||
|
///
|
||||||
|
/// Verify the structure of TAB1: it is a substitution table used in
|
||||||
|
/// key mangling. Check that no two inputs map to the same output
|
||||||
|
/// (TAB1 is a permutation of 0..255).
|
||||||
|
#[test]
|
||||||
|
fn css_tab1_is_permutation() {
|
||||||
|
let mut seen = [false; 256];
|
||||||
|
for i in 0..256 {
|
||||||
|
let v = TAB1[i] as usize;
|
||||||
|
assert!(!seen[v], "TAB1 maps two inputs to {:#04x}", v);
|
||||||
|
seen[v] = true;
|
||||||
|
}
|
||||||
|
// Check involution property: TAB1[TAB1[x]] should map back predictably
|
||||||
|
// TAB1 is not necessarily a strict involution, but we verify the
|
||||||
|
// composition TAB1[TAB1[x]] is also a permutation
|
||||||
|
let mut seen2 = [false; 256];
|
||||||
|
for i in 0..256 {
|
||||||
|
let v = TAB1[TAB1[i] as usize] as usize;
|
||||||
|
assert!(!seen2[v], "TAB1[TAB1[x]] maps two inputs to {:#04x}", v);
|
||||||
|
seen2[v] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test 5: css_tab4_is_bit_reversal
|
||||||
|
///
|
||||||
|
/// TAB4 reverses the bits of each byte: TAB4[0x01] = 0x80, TAB4[0x80] = 0x01, etc.
|
||||||
|
#[test]
|
||||||
|
fn css_tab4_is_bit_reversal() {
|
||||||
|
for i in 0u16..256 {
|
||||||
|
let expected = (0..8).fold(0u8, |acc, bit| {
|
||||||
|
acc | (((i as u8 >> bit) & 1) << (7 - bit))
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
TAB4[i as usize], expected,
|
||||||
|
"TAB4[{:#04x}] = {:#04x}, expected {:#04x} (bit reversal)",
|
||||||
|
i, TAB4[i as usize], expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Also verify TAB4 is an involution: TAB4[TAB4[x]] == x
|
||||||
|
for i in 0..256 {
|
||||||
|
assert_eq!(
|
||||||
|
TAB4[TAB4[i] as usize], i as u8,
|
||||||
|
"TAB4 is not an involution at {:#04x}", i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+180
-23
@@ -11,6 +11,7 @@
|
|||||||
use crate::clpi;
|
use crate::clpi;
|
||||||
use crate::drive::DriveSession;
|
use crate::drive::DriveSession;
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
use crate::ifo;
|
||||||
use crate::mpls;
|
use crate::mpls;
|
||||||
use crate::sector::SectorReader;
|
use crate::sector::SectorReader;
|
||||||
use crate::speed::DriveSpeed;
|
use crate::speed::DriveSpeed;
|
||||||
@@ -43,6 +44,17 @@ pub struct Disc {
|
|||||||
pub css: Option<crate::css::CssState>,
|
pub css: Option<crate::css::CssState>,
|
||||||
/// Whether this disc requires decryption (AACS or CSS)
|
/// Whether this disc requires decryption (AACS or CSS)
|
||||||
pub encrypted: bool,
|
pub encrypted: bool,
|
||||||
|
/// Content format (BD transport stream vs DVD program stream)
|
||||||
|
pub content_format: ContentFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Content format — determines how sectors are interpreted downstream.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum ContentFormat {
|
||||||
|
/// Blu-ray BD Transport Stream (192-byte packets)
|
||||||
|
BdTs,
|
||||||
|
/// DVD MPEG-2 Program Stream (VOB)
|
||||||
|
MpegPs,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disc format.
|
/// Disc format.
|
||||||
@@ -97,6 +109,8 @@ pub struct DiscTitle {
|
|||||||
pub streams: Vec<Stream>,
|
pub streams: Vec<Stream>,
|
||||||
/// Sector extents for ripping (clip LBA ranges)
|
/// Sector extents for ripping (clip LBA ranges)
|
||||||
pub extents: Vec<Extent>,
|
pub extents: Vec<Extent>,
|
||||||
|
/// Content format for this title
|
||||||
|
pub content_format: ContentFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A clip reference within a title.
|
/// A clip reference within a title.
|
||||||
@@ -292,6 +306,7 @@ impl DiscTitle {
|
|||||||
clips: Vec::new(),
|
clips: Vec::new(),
|
||||||
streams: Vec::new(),
|
streams: Vec::new(),
|
||||||
extents: Vec::new(),
|
extents: Vec::new(),
|
||||||
|
content_format: ContentFormat::BdTs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,22 +565,14 @@ impl Disc {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Playlists
|
// 3. Titles — BD (MPLS playlists) or DVD (IFO title sets)
|
||||||
let mut titles = Vec::new();
|
let (mut titles, content_format) = if udf_fs.find_dir("/BDMV").is_some() {
|
||||||
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
|
(Self::scan_bluray_titles(reader, &udf_fs), ContentFormat::BdTs)
|
||||||
for entry in &playlist_dir.entries {
|
} else if udf_fs.find_dir("/VIDEO_TS").is_some() {
|
||||||
if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") {
|
(Self::scan_dvd_titles(reader, &udf_fs), ContentFormat::MpegPs)
|
||||||
let path = format!("/BDMV/PLAYLIST/{}", entry.name);
|
} else {
|
||||||
if let Ok(mpls_data) = udf_fs.read_file(reader, &path) {
|
(Vec::new(), ContentFormat::BdTs)
|
||||||
if let Some(title) =
|
};
|
||||||
Self::parse_playlist(reader, &udf_fs, &entry.name, &mpls_data)
|
|
||||||
{
|
|
||||||
titles.push(title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
titles.sort_by(|a, b| {
|
titles.sort_by(|a, b| {
|
||||||
b.duration_secs
|
b.duration_secs
|
||||||
.partial_cmp(&a.duration_secs)
|
.partial_cmp(&a.duration_secs)
|
||||||
@@ -581,9 +588,8 @@ impl Disc {
|
|||||||
let layers = if capacity > 24_000_000 { 2 } else { 1 };
|
let layers = if capacity > 24_000_000 { 2 } else { 1 };
|
||||||
let region = DiscRegion::Free;
|
let region = DiscRegion::Free;
|
||||||
|
|
||||||
// 6. CSS detection for DVDs (VIDEO_TS directory = DVD structure)
|
// 6. CSS detection for DVDs
|
||||||
let is_dvd = udf_fs.find_dir("/VIDEO_TS").is_some();
|
let css = if content_format == ContentFormat::MpegPs && !titles.is_empty() {
|
||||||
let css = if is_dvd && !titles.is_empty() {
|
|
||||||
crate::css::crack_key(reader, &titles[0].extents)
|
crate::css::crack_key(reader, &titles[0].extents)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -602,6 +608,7 @@ impl Disc {
|
|||||||
aacs,
|
aacs,
|
||||||
css,
|
css,
|
||||||
encrypted,
|
encrypted,
|
||||||
|
content_format,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,8 +944,139 @@ impl Disc {
|
|||||||
clips,
|
clips,
|
||||||
streams,
|
streams,
|
||||||
extents,
|
extents,
|
||||||
|
content_format: ContentFormat::BdTs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scan Blu-ray titles from MPLS playlists.
|
||||||
|
fn scan_bluray_titles(reader: &mut dyn SectorReader, udf_fs: &udf::UdfFs) -> Vec<DiscTitle> {
|
||||||
|
let mut titles = Vec::new();
|
||||||
|
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
|
||||||
|
for entry in &playlist_dir.entries {
|
||||||
|
if !entry.is_dir && entry.name.to_lowercase().ends_with(".mpls") {
|
||||||
|
let path = format!("/BDMV/PLAYLIST/{}", entry.name);
|
||||||
|
if let Ok(mpls_data) = udf_fs.read_file(reader, &path) {
|
||||||
|
if let Some(title) =
|
||||||
|
Self::parse_playlist(reader, udf_fs, &entry.name, &mpls_data)
|
||||||
|
{
|
||||||
|
titles.push(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
titles
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan DVD titles from IFO files (VIDEO_TS.IFO + VTS_XX_0.IFO).
|
||||||
|
fn scan_dvd_titles(reader: &mut dyn SectorReader, udf_fs: &udf::UdfFs) -> Vec<DiscTitle> {
|
||||||
|
let dvd_info = match ifo::parse_vmg(reader, udf_fs) {
|
||||||
|
Ok(info) => info,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut titles = Vec::new();
|
||||||
|
let mut title_number: u16 = 0;
|
||||||
|
|
||||||
|
for ts in &dvd_info.title_sets {
|
||||||
|
// Map DvdVideoAttr to Stream::Video
|
||||||
|
let video_codec = match ts.video.codec.as_str() {
|
||||||
|
"mpeg2" => Codec::Mpeg2,
|
||||||
|
"mpeg1" => Codec::Mpeg2, // treat MPEG-1 as MPEG-2 for container purposes
|
||||||
|
_ => Codec::Mpeg2,
|
||||||
|
};
|
||||||
|
|
||||||
|
let video_stream = Stream::Video(VideoStream {
|
||||||
|
pid: 0xE0, // DVD video PID (standard MPEG PS video stream)
|
||||||
|
codec: video_codec,
|
||||||
|
resolution: ts.video.resolution.clone(),
|
||||||
|
frame_rate: match ts.video.standard.as_str() {
|
||||||
|
"PAL" => "25".to_string(),
|
||||||
|
_ => "29.97".to_string(),
|
||||||
|
},
|
||||||
|
hdr: HdrFormat::Sdr,
|
||||||
|
color_space: ColorSpace::Bt709,
|
||||||
|
secondary: false,
|
||||||
|
label: String::new(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Map DvdAudioAttr to Stream::Audio
|
||||||
|
let audio_streams: Vec<Stream> = ts
|
||||||
|
.audio_streams
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, a)| {
|
||||||
|
let codec = match a.codec.as_str() {
|
||||||
|
"ac3" => Codec::Ac3,
|
||||||
|
"dts" => Codec::Dts,
|
||||||
|
"lpcm" => Codec::Lpcm,
|
||||||
|
"mpeg1" | "mpeg2" => Codec::Mpeg2,
|
||||||
|
_ => Codec::Unknown(0),
|
||||||
|
};
|
||||||
|
let channels = match a.channels {
|
||||||
|
1 => "mono".to_string(),
|
||||||
|
2 => "stereo".to_string(),
|
||||||
|
6 => "5.1".to_string(),
|
||||||
|
8 => "7.1".to_string(),
|
||||||
|
n => format!("{}ch", n),
|
||||||
|
};
|
||||||
|
let sample_rate = match a.sample_rate {
|
||||||
|
48000 => "48kHz".to_string(),
|
||||||
|
96000 => "96kHz".to_string(),
|
||||||
|
sr => format!("{}kHz", sr / 1000),
|
||||||
|
};
|
||||||
|
Stream::Audio(AudioStream {
|
||||||
|
pid: 0xBD00 + i as u16, // DVD private stream 1 sub-IDs
|
||||||
|
codec,
|
||||||
|
channels,
|
||||||
|
language: a.language.clone(),
|
||||||
|
sample_rate,
|
||||||
|
secondary: false,
|
||||||
|
label: String::new(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for dvd_title in &ts.titles {
|
||||||
|
title_number += 1;
|
||||||
|
|
||||||
|
// Build extents from cell sector ranges (absolute = vob_start + cell offset)
|
||||||
|
let extents: Vec<Extent> = dvd_title
|
||||||
|
.cells
|
||||||
|
.iter()
|
||||||
|
.map(|cell| {
|
||||||
|
let start = ts.vob_start_sector + cell.first_sector;
|
||||||
|
let count = cell.last_sector.saturating_sub(cell.first_sector) + 1;
|
||||||
|
Extent {
|
||||||
|
start_lba: start,
|
||||||
|
sector_count: count,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let size_bytes: u64 = extents
|
||||||
|
.iter()
|
||||||
|
.map(|e| e.sector_count as u64 * 2048)
|
||||||
|
.sum();
|
||||||
|
|
||||||
|
let mut streams = vec![video_stream.clone()];
|
||||||
|
streams.extend(audio_streams.iter().cloned());
|
||||||
|
|
||||||
|
titles.push(DiscTitle {
|
||||||
|
playlist: format!("VTS_{:02}_{}.VOB", ts.vts_number, title_number),
|
||||||
|
playlist_id: title_number,
|
||||||
|
duration_secs: dvd_title.duration_secs,
|
||||||
|
size_bytes,
|
||||||
|
clips: Vec::new(),
|
||||||
|
streams,
|
||||||
|
extents,
|
||||||
|
content_format: ContentFormat::MpegPs,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
titles
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Decrypted reader ──────────────────────────────────────────────────────
|
// ─── Decrypted reader ──────────────────────────────────────────────────────
|
||||||
@@ -954,6 +1092,8 @@ impl Disc {
|
|||||||
pub struct ContentReader<'a> {
|
pub struct ContentReader<'a> {
|
||||||
session: &'a mut DriveSession,
|
session: &'a mut DriveSession,
|
||||||
aacs: Option<&'a AacsState>,
|
aacs: Option<&'a AacsState>,
|
||||||
|
css: Option<&'a crate::css::CssState>,
|
||||||
|
content_format: ContentFormat,
|
||||||
extents: Vec<Extent>,
|
extents: Vec<Extent>,
|
||||||
current_extent: usize,
|
current_extent: usize,
|
||||||
current_offset: u32,
|
current_offset: u32,
|
||||||
@@ -1003,6 +1143,8 @@ impl Disc {
|
|||||||
Ok(ContentReader {
|
Ok(ContentReader {
|
||||||
session,
|
session,
|
||||||
aacs: self.aacs.as_ref(),
|
aacs: self.aacs.as_ref(),
|
||||||
|
css: self.css.as_ref(),
|
||||||
|
content_format: title.content_format,
|
||||||
extents: title.extents.clone(),
|
extents: title.extents.clone(),
|
||||||
current_extent: 0,
|
current_extent: 0,
|
||||||
current_offset: 0,
|
current_offset: 0,
|
||||||
@@ -1109,6 +1251,7 @@ impl<'a> ContentReader<'a> {
|
|||||||
// Decrypt all units in the buffer in-place
|
// Decrypt all units in the buffer in-place
|
||||||
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
||||||
if let Some(aacs) = &self.aacs {
|
if let Some(aacs) = &self.aacs {
|
||||||
|
// AACS unit decryption (BD/UHD)
|
||||||
let uk = aacs
|
let uk = aacs
|
||||||
.unit_keys
|
.unit_keys
|
||||||
.get(self.unit_key_idx)
|
.get(self.unit_key_idx)
|
||||||
@@ -1124,11 +1267,25 @@ impl<'a> ContentReader<'a> {
|
|||||||
crate::aacs::decrypt_unit_full(unit, &uk, rdk);
|
crate::aacs::decrypt_unit_full(unit, &uk, rdk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let total_bytes = self.buf_len * unit_len;
|
let total_bytes = self.buf_len * unit_len;
|
||||||
self.buf_pos = self.buf_len; // mark fully consumed
|
self.buf_pos = self.buf_len;
|
||||||
Ok(Some(&self.read_buf[..total_bytes]))
|
Ok(Some(&self.read_buf[..total_bytes]))
|
||||||
|
} else if let Some(css) = &self.css {
|
||||||
|
// CSS per-sector descrambling (DVD)
|
||||||
|
let total_bytes = self.buf_len * unit_len;
|
||||||
|
for chunk in self.read_buf[..total_bytes].chunks_mut(2048) {
|
||||||
|
crate::css::lfsr::descramble_sector(&css.title_key, chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.buf_pos = self.buf_len;
|
||||||
|
Ok(Some(&self.read_buf[..total_bytes]))
|
||||||
|
} else {
|
||||||
|
// No encryption
|
||||||
|
let total_bytes = self.buf_len * unit_len;
|
||||||
|
self.buf_pos = self.buf_len;
|
||||||
|
Ok(Some(&self.read_buf[..total_bytes]))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt a single aligned unit in-place if needed.
|
/// Decrypt a single aligned unit in-place if needed.
|
||||||
|
|||||||
+3
-2
@@ -94,8 +94,9 @@ pub use identity::DriveId;
|
|||||||
pub use profile::DriveProfile;
|
pub use profile::DriveProfile;
|
||||||
// Platform trait is pub(crate) -- callers use DriveSession, not Platform directly
|
// Platform trait is pub(crate) -- callers use DriveSession, not Platform directly
|
||||||
pub use disc::{
|
pub use disc::{
|
||||||
AacsState, AudioStream, Clip, Codec, ColorSpace, ContentReader, Disc, DiscFormat, DiscTitle,
|
AacsState, AudioStream, Clip, Codec, ColorSpace, ContentFormat, ContentReader, Disc,
|
||||||
Extent, HdrFormat, KeySource, ScanOptions, Stream, SubtitleStream, VideoStream,
|
DiscFormat, DiscTitle, Extent, HdrFormat, KeySource, ScanOptions, Stream, SubtitleStream,
|
||||||
|
VideoStream,
|
||||||
};
|
};
|
||||||
pub use mux::DiscOptions;
|
pub use mux::DiscOptions;
|
||||||
pub use mux::DiscStream;
|
pub use mux::DiscStream;
|
||||||
|
|||||||
@@ -180,6 +180,7 @@ impl M2tsMeta {
|
|||||||
clips: Vec::new(),
|
clips: Vec::new(),
|
||||||
streams,
|
streams,
|
||||||
extents: Vec::new(),
|
extents: Vec::new(),
|
||||||
|
content_format: crate::disc::ContentFormat::BdTs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ fn sample_disc_title() -> DiscTitle {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
extents: Vec::new(),
|
extents: Vec::new(),
|
||||||
|
content_format: ContentFormat::BdTs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,6 +447,7 @@ fn meta_codec_roundtrip() {
|
|||||||
clips: Vec::new(),
|
clips: Vec::new(),
|
||||||
streams,
|
streams,
|
||||||
extents: Vec::new(),
|
extents: Vec::new(),
|
||||||
|
content_format: ContentFormat::BdTs,
|
||||||
};
|
};
|
||||||
|
|
||||||
let meta = M2tsMeta::from_title(&dt);
|
let meta = M2tsMeta::from_title(&dt);
|
||||||
@@ -478,6 +480,7 @@ fn meta_empty_streams() {
|
|||||||
clips: Vec::new(),
|
clips: Vec::new(),
|
||||||
streams: Vec::new(),
|
streams: Vec::new(),
|
||||||
extents: Vec::new(),
|
extents: Vec::new(),
|
||||||
|
content_format: ContentFormat::BdTs,
|
||||||
};
|
};
|
||||||
|
|
||||||
let meta = M2tsMeta::from_title(&dt);
|
let meta = M2tsMeta::from_title(&dt);
|
||||||
@@ -495,6 +498,7 @@ fn meta_all_stream_types() {
|
|||||||
duration_secs: 3600.0,
|
duration_secs: 3600.0,
|
||||||
size_bytes: 0,
|
size_bytes: 0,
|
||||||
clips: Vec::new(),
|
clips: Vec::new(),
|
||||||
|
content_format: ContentFormat::BdTs,
|
||||||
streams: vec![
|
streams: vec![
|
||||||
Stream::Video(VideoStream {
|
Stream::Video(VideoStream {
|
||||||
pid: 0x1011,
|
pid: 0x1011,
|
||||||
|
|||||||
Reference in New Issue
Block a user