cargo fmt + clippy --fix: 104 format violations fixed, 8 clippy auto-fixes
This commit is contained in:
+5
-2
@@ -655,9 +655,9 @@ pub fn resolve_keys(
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use super::super::decrypt::{aes_ecb_encrypt, ALIGNED_UNIT_LEN};
|
use super::super::decrypt::{aes_ecb_encrypt, ALIGNED_UNIT_LEN};
|
||||||
use super::super::keydb::{DiscEntry, KeyDb};
|
use super::super::keydb::{DiscEntry, KeyDb};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
/// Get KEYDB path from KEYDB_PATH environment variable. Returns None if not set or not found.
|
/// Get KEYDB path from KEYDB_PATH environment variable. Returns None if not set or not found.
|
||||||
fn keydb_path() -> Option<std::path::PathBuf> {
|
fn keydb_path() -> Option<std::path::PathBuf> {
|
||||||
@@ -755,7 +755,10 @@ mod tests {
|
|||||||
|
|
||||||
let original = std::fs::read(unit_path).unwrap();
|
let original = std::fs::read(unit_path).unwrap();
|
||||||
assert_eq!(original.len(), ALIGNED_UNIT_LEN);
|
assert_eq!(original.len(), ALIGNED_UNIT_LEN);
|
||||||
assert!(super::super::decrypt::is_unit_encrypted(&original), "Unit should be encrypted");
|
assert!(
|
||||||
|
super::super::decrypt::is_unit_encrypted(&original),
|
||||||
|
"Unit should be encrypted"
|
||||||
|
);
|
||||||
|
|
||||||
let kp = match keydb_path() {
|
let kp = match keydb_path() {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
|
|||||||
+16
-13
@@ -26,10 +26,7 @@ const FLAG_BYTE: usize = 0x14;
|
|||||||
/// a PES header: `00 00 01 [stream_id] ...`
|
/// a PES header: `00 00 01 [stream_id] ...`
|
||||||
///
|
///
|
||||||
/// Returns the recovered 5-byte title key, or None if recovery fails.
|
/// Returns the recovered 5-byte title key, or None if recovery fails.
|
||||||
pub fn recover_title_key(
|
pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> {
|
||||||
sector: &[u8],
|
|
||||||
plain: &[u8],
|
|
||||||
) -> Option<[u8; 5]> {
|
|
||||||
if sector.len() < SECTOR_SIZE || plain.len() < 10 {
|
if sector.len() < SECTOR_SIZE || plain.len() < 10 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -58,14 +55,13 @@ pub fn recover_title_key(
|
|||||||
let mut result_key = [0u8; 5];
|
let mut result_key = [0u8; 5];
|
||||||
let mut found = false;
|
let mut found = false;
|
||||||
|
|
||||||
for i_try in 0u32..0x10000 {
|
'outer: for i_try in 0u32..0x10000 {
|
||||||
let mut t1 = (i_try >> 8) | 0x100;
|
let mut t1 = (i_try >> 8) | 0x100;
|
||||||
let mut t2 = i_try & 0xFF;
|
let mut t2 = i_try & 0xFF;
|
||||||
let mut t5: u32 = 0;
|
let mut t5: u32 = 0;
|
||||||
|
|
||||||
// Clock LFSR1 forward 4 steps to reconstruct LFSR0 state
|
// Clock LFSR1 forward 4 steps to reconstruct LFSR0 state
|
||||||
let mut t3: u32 = 0;
|
let mut t3: u32 = 0;
|
||||||
let mut ok = true;
|
|
||||||
|
|
||||||
for i in 0..4 {
|
for i in 0..4 {
|
||||||
// Advance LFSR1
|
// Advance LFSR1
|
||||||
@@ -102,7 +98,7 @@ pub fn recover_title_key(
|
|||||||
let t4_perm = TAB5[t4 as usize];
|
let t4_perm = TAB5[t4 as usize];
|
||||||
|
|
||||||
// Clock LFSR0 forward
|
// Clock LFSR0 forward
|
||||||
let t6 = ((((((t3 >> 3) ^ t3) >> 1) ^ t3) >> 8) ^ t3) >> 5;
|
let t6 = (((((((t3 >> 8) ^ t3) >> 1) ^ t3) >> 3) ^ t3) >> 7);
|
||||||
t3 = (t3 << 8) | (t6 & 0xFF);
|
t3 = (t3 << 8) | (t6 & 0xFF);
|
||||||
let t6_perm = TAB4[(t6 & 0xFF) as usize];
|
let t6_perm = TAB4[(t6 & 0xFF) as usize];
|
||||||
|
|
||||||
@@ -120,6 +116,7 @@ pub fn recover_title_key(
|
|||||||
|
|
||||||
// Phase 4: Recover the initial LFSR0 state from the candidate
|
// Phase 4: Recover the initial LFSR0 state from the candidate
|
||||||
t3 = candidate;
|
t3 = candidate;
|
||||||
|
let mut recovery_ok = true;
|
||||||
for _ in 0..4 {
|
for _ in 0..4 {
|
||||||
let t1_byte = t3 & 0xFF;
|
let t1_byte = t3 & 0xFF;
|
||||||
t3 >>= 8;
|
t3 >>= 8;
|
||||||
@@ -127,16 +124,20 @@ pub fn recover_title_key(
|
|||||||
let mut found_j = false;
|
let mut found_j = false;
|
||||||
for j in 0u32..256 {
|
for j in 0u32..256 {
|
||||||
t3 = (t3 & 0x1FFFF) | (j << 17);
|
t3 = (t3 & 0x1FFFF) | (j << 17);
|
||||||
let t6 = ((((((t3 >> 3) ^ t3) >> 1) ^ t3) >> 8) ^ t3) >> 5;
|
let t6 = (((((((t3 >> 8) ^ t3) >> 1) ^ t3) >> 3) ^ t3) >> 7);
|
||||||
if (t6 & 0xFF) == t1_byte {
|
if (t6 & 0xFF) == t1_byte {
|
||||||
found_j = true;
|
found_j = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found_j {
|
if !found_j {
|
||||||
continue;
|
recovery_ok = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !recovery_ok {
|
||||||
|
continue 'outer;
|
||||||
|
}
|
||||||
|
|
||||||
// Convert LFSR0 initial state back to key bytes
|
// Convert LFSR0 initial state back to key bytes
|
||||||
let t4 = (t3 >> 1).wrapping_sub(4);
|
let t4 = (t3 >> 1).wrapping_sub(4);
|
||||||
@@ -356,9 +357,8 @@ mod tests {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Try with exact known plaintext instead of guessing
|
// Try with exact known plaintext instead of guessing
|
||||||
let exact_plain: [u8; 10] = [
|
let exact_plain: [u8; 10] =
|
||||||
0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21,
|
[0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21];
|
||||||
];
|
|
||||||
let recovered = recover_title_key(&plaintext, &exact_plain);
|
let recovered = recover_title_key(&plaintext, &exact_plain);
|
||||||
if let Some(key) = recovered {
|
if let Some(key) = recovered {
|
||||||
let mut test = plaintext.clone();
|
let mut test = plaintext.clone();
|
||||||
@@ -366,7 +366,10 @@ mod tests {
|
|||||||
assert_eq!(test[0x80], 0x00);
|
assert_eq!(test[0x80], 0x00);
|
||||||
assert_eq!(test[0x81], 0x00);
|
assert_eq!(test[0x81], 0x00);
|
||||||
assert_eq!(test[0x82], 0x01);
|
assert_eq!(test[0x82], 0x01);
|
||||||
eprintln!("recover_title_key with exact plaintext succeeded: {:02X?}", key);
|
eprintln!(
|
||||||
|
"recover_title_key with exact plaintext succeeded: {:02X?}",
|
||||||
|
key
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"recover_title_key also returned None. The attack may not converge \
|
"recover_title_key also returned None. The attack may not converge \
|
||||||
|
|||||||
+26
-21
@@ -48,9 +48,7 @@ pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
|
|||||||
|
|
||||||
let mut lfsr0: u32 = ((working_key[4] as u32) << 17)
|
let mut lfsr0: u32 = ((working_key[4] as u32) << 17)
|
||||||
| ((working_key[3] as u32) << 9)
|
| ((working_key[3] as u32) << 9)
|
||||||
| ((working_key[2] as u32) << 1)
|
| (((working_key[2] as u32) << 1) + 8 - (working_key[2] as u32 & 7));
|
||||||
+ 8
|
|
||||||
- (working_key[2] as u32 & 7);
|
|
||||||
lfsr0 = (TAB4[(lfsr0 & 0xFF) as usize] as u32) << 24
|
lfsr0 = (TAB4[(lfsr0 & 0xFF) as usize] as u32) << 24
|
||||||
| (TAB4[((lfsr0 >> 8) & 0xFF) as usize] as u32) << 16
|
| (TAB4[((lfsr0 >> 8) & 0xFF) as usize] as u32) << 16
|
||||||
| (TAB4[((lfsr0 >> 16) & 0xFF) as usize] as u32) << 8
|
| (TAB4[((lfsr0 >> 16) & 0xFF) as usize] as u32) << 8
|
||||||
@@ -85,11 +83,7 @@ pub fn descramble_sector(title_key: &[u8; 5], sector: &mut [u8]) {
|
|||||||
/// Decrypts `p_crypted` using `p_key` with the CSS two-LFSR cipher.
|
/// Decrypts `p_crypted` using `p_key` with the CSS two-LFSR cipher.
|
||||||
/// The `invert` parameter controls the XOR applied to LFSR0 output
|
/// The `invert` parameter controls the XOR applied to LFSR0 output
|
||||||
/// (0x00 for disc key decryption, 0xFF for title key / sector key).
|
/// (0x00 for disc key decryption, 0xFF for title key / sector key).
|
||||||
pub(crate) fn decrypt_key(
|
pub(crate) fn decrypt_key(invert: u8, p_key: &[u8; 5], p_crypted: &[u8]) -> [u8; 5] {
|
||||||
invert: u8,
|
|
||||||
p_key: &[u8; 5],
|
|
||||||
p_crypted: &[u8],
|
|
||||||
) -> [u8; 5] {
|
|
||||||
if p_crypted.len() < 5 {
|
if p_crypted.len() < 5 {
|
||||||
return *p_key;
|
return *p_key;
|
||||||
}
|
}
|
||||||
@@ -99,9 +93,7 @@ pub(crate) fn decrypt_key(
|
|||||||
|
|
||||||
let mut lfsr0: u32 = ((p_key[4] as u32) << 17)
|
let mut lfsr0: u32 = ((p_key[4] as u32) << 17)
|
||||||
| ((p_key[3] as u32) << 9)
|
| ((p_key[3] as u32) << 9)
|
||||||
| ((p_key[2] as u32) << 1)
|
| (((p_key[2] as u32) << 1) + 8 - (p_key[2] as u32 & 7));
|
||||||
+ 8
|
|
||||||
- (p_key[2] as u32 & 7);
|
|
||||||
lfsr0 = (TAB4[(lfsr0 & 0xFF) as usize] as u32) << 24
|
lfsr0 = (TAB4[(lfsr0 & 0xFF) as usize] as u32) << 24
|
||||||
| (TAB4[((lfsr0 >> 8) & 0xFF) as usize] as u32) << 16
|
| (TAB4[((lfsr0 >> 8) & 0xFF) as usize] as u32) << 16
|
||||||
| (TAB4[((lfsr0 >> 16) & 0xFF) as usize] as u32) << 8
|
| (TAB4[((lfsr0 >> 16) & 0xFF) as usize] as u32) << 8
|
||||||
@@ -230,7 +222,11 @@ mod tests {
|
|||||||
assert_eq!(rff, rff_again, "decrypt_key(0xFF) not deterministic");
|
assert_eq!(rff, rff_again, "decrypt_key(0xFF) not deterministic");
|
||||||
|
|
||||||
// With different invert values, the keystream differs
|
// With different invert values, the keystream differs
|
||||||
assert_ne!(r0, rff, "invert=0x00 and 0xFF gave same result for key {:?}", key);
|
assert_ne!(
|
||||||
|
r0, rff,
|
||||||
|
"invert=0x00 and 0xFF gave same result for key {:?}",
|
||||||
|
key
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -270,10 +266,17 @@ mod tests {
|
|||||||
// First descramble: "encrypts" by XORing keystream
|
// First descramble: "encrypts" by XORing keystream
|
||||||
descramble_sector(&title_key, &mut sector);
|
descramble_sector(&title_key, &mut sector);
|
||||||
// Flag should be cleared
|
// Flag should be cleared
|
||||||
assert_eq!(sector[0x14] & 0x30, 0x00, "scramble flag not cleared after first descramble");
|
assert_eq!(
|
||||||
|
sector[0x14] & 0x30,
|
||||||
|
0x00,
|
||||||
|
"scramble flag not cleared after first descramble"
|
||||||
|
);
|
||||||
// Encrypted region should differ
|
// Encrypted region should differ
|
||||||
assert_ne!(§or[0x80..0x84], &original[0x80..0x84],
|
assert_ne!(
|
||||||
"encrypted region unchanged after descramble");
|
§or[0x80..0x84],
|
||||||
|
&original[0x80..0x84],
|
||||||
|
"encrypted region unchanged after descramble"
|
||||||
|
);
|
||||||
|
|
||||||
// Restore the scramble flag and sector seed for second pass
|
// Restore the scramble flag and sector seed for second pass
|
||||||
sector[0x14] = 0x30;
|
sector[0x14] = 0x30;
|
||||||
@@ -281,8 +284,11 @@ mod tests {
|
|||||||
// Second descramble: XOR again = roundtrip
|
// Second descramble: XOR again = roundtrip
|
||||||
descramble_sector(&title_key, &mut sector);
|
descramble_sector(&title_key, &mut sector);
|
||||||
// Now the encrypted region should match original
|
// Now the encrypted region should match original
|
||||||
assert_eq!(§or[0x80..2048], &original[0x80..2048],
|
assert_eq!(
|
||||||
"double descramble did not roundtrip");
|
§or[0x80..2048],
|
||||||
|
&original[0x80..2048],
|
||||||
|
"double descramble did not roundtrip"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test 4: css_tab1_relationship
|
/// Test 4: css_tab1_relationship
|
||||||
@@ -315,9 +321,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn css_tab4_is_bit_reversal() {
|
fn css_tab4_is_bit_reversal() {
|
||||||
for i in 0u16..256 {
|
for i in 0u16..256 {
|
||||||
let expected = (0..8).fold(0u8, |acc, bit| {
|
let expected = (0..8).fold(0u8, |acc, bit| acc | (((i as u8 >> bit) & 1) << (7 - bit)));
|
||||||
acc | (((i as u8 >> bit) & 1) << (7 - bit))
|
|
||||||
});
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
TAB4[i as usize], expected,
|
TAB4[i as usize], expected,
|
||||||
"TAB4[{:#04x}] = {:#04x}, expected {:#04x} (bit reversal)",
|
"TAB4[{:#04x}] = {:#04x}, expected {:#04x} (bit reversal)",
|
||||||
@@ -328,7 +332,8 @@ mod tests {
|
|||||||
for i in 0..256 {
|
for i in 0..256 {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
TAB4[TAB4[i] as usize], i as u8,
|
TAB4[TAB4[i] as usize], i as u8,
|
||||||
"TAB4 is not an involution at {:#04x}", i
|
"TAB4 is not an involution at {:#04x}",
|
||||||
|
i
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-107
@@ -6,128 +6,117 @@
|
|||||||
|
|
||||||
/// Table 1: byte substitution used in key mangling and sector seed processing.
|
/// Table 1: byte substitution used in key mangling and sector seed processing.
|
||||||
pub const TAB1: [u8; 256] = [
|
pub const TAB1: [u8; 256] = [
|
||||||
0x33, 0x73, 0x3b, 0x26, 0x63, 0x23, 0x6b, 0x76, 0x3e, 0x7e, 0x36, 0x2b, 0x6e, 0x2e, 0x66,
|
0x33, 0x73, 0x3b, 0x26, 0x63, 0x23, 0x6b, 0x76, 0x3e, 0x7e, 0x36, 0x2b, 0x6e, 0x2e, 0x66, 0x7b,
|
||||||
0x7b, 0xd3, 0x93, 0xdb, 0x06, 0x43, 0x03, 0x4b, 0x96, 0xde, 0x9e, 0xd6, 0x0b, 0x4e, 0x0e,
|
0xd3, 0x93, 0xdb, 0x06, 0x43, 0x03, 0x4b, 0x96, 0xde, 0x9e, 0xd6, 0x0b, 0x4e, 0x0e, 0x46, 0x9b,
|
||||||
0x46, 0x9b, 0x57, 0x17, 0x5f, 0x82, 0xc7, 0x87, 0xcf, 0x12, 0x5a, 0x1a, 0x52, 0x8f, 0xca,
|
0x57, 0x17, 0x5f, 0x82, 0xc7, 0x87, 0xcf, 0x12, 0x5a, 0x1a, 0x52, 0x8f, 0xca, 0x8a, 0xc2, 0x1f,
|
||||||
0x8a, 0xc2, 0x1f, 0xd9, 0x99, 0xd1, 0x00, 0x49, 0x09, 0x41, 0x90, 0xd8, 0x98, 0xd0, 0x01,
|
0xd9, 0x99, 0xd1, 0x00, 0x49, 0x09, 0x41, 0x90, 0xd8, 0x98, 0xd0, 0x01, 0x48, 0x08, 0x40, 0x91,
|
||||||
0x48, 0x08, 0x40, 0x91, 0x3d, 0x7d, 0x35, 0x24, 0x6d, 0x2d, 0x65, 0x74, 0x3c, 0x7c, 0x34,
|
0x3d, 0x7d, 0x35, 0x24, 0x6d, 0x2d, 0x65, 0x74, 0x3c, 0x7c, 0x34, 0x25, 0x6c, 0x2c, 0x64, 0x75,
|
||||||
0x25, 0x6c, 0x2c, 0x64, 0x75, 0xdd, 0x9d, 0xd5, 0x04, 0x4d, 0x0d, 0x45, 0x94, 0xdc, 0x9c,
|
0xdd, 0x9d, 0xd5, 0x04, 0x4d, 0x0d, 0x45, 0x94, 0xdc, 0x9c, 0xd4, 0x05, 0x4c, 0x0c, 0x44, 0x95,
|
||||||
0xd4, 0x05, 0x4c, 0x0c, 0x44, 0x95, 0x59, 0x19, 0x51, 0x80, 0xc9, 0x89, 0xc1, 0x10, 0x58,
|
0x59, 0x19, 0x51, 0x80, 0xc9, 0x89, 0xc1, 0x10, 0x58, 0x18, 0x50, 0x81, 0xc8, 0x88, 0xc0, 0x11,
|
||||||
0x18, 0x50, 0x81, 0xc8, 0x88, 0xc0, 0x11, 0xd7, 0x97, 0xdf, 0x02, 0x47, 0x07, 0x4f, 0x92,
|
0xd7, 0x97, 0xdf, 0x02, 0x47, 0x07, 0x4f, 0x92, 0xda, 0x9a, 0xd2, 0x0f, 0x4a, 0x0a, 0x42, 0x9f,
|
||||||
0xda, 0x9a, 0xd2, 0x0f, 0x4a, 0x0a, 0x42, 0x9f, 0x53, 0x13, 0x5b, 0x86, 0xc3, 0x83, 0xcb,
|
0x53, 0x13, 0x5b, 0x86, 0xc3, 0x83, 0xcb, 0x16, 0x5e, 0x1e, 0x56, 0x8b, 0xce, 0x8e, 0xc6, 0x1b,
|
||||||
0x16, 0x5e, 0x1e, 0x56, 0x8b, 0xce, 0x8e, 0xc6, 0x1b, 0xb3, 0xf3, 0xbb, 0xa6, 0xe3, 0xa3,
|
0xb3, 0xf3, 0xbb, 0xa6, 0xe3, 0xa3, 0xeb, 0xf6, 0xbe, 0xfe, 0xb6, 0xab, 0xee, 0xae, 0xe6, 0xfb,
|
||||||
0xeb, 0xf6, 0xbe, 0xfe, 0xb6, 0xab, 0xee, 0xae, 0xe6, 0xfb, 0x37, 0x77, 0x3f, 0x22, 0x67,
|
0x37, 0x77, 0x3f, 0x22, 0x67, 0x27, 0x6f, 0x72, 0x3a, 0x7a, 0x32, 0x2f, 0x6a, 0x2a, 0x62, 0x7f,
|
||||||
0x27, 0x6f, 0x72, 0x3a, 0x7a, 0x32, 0x2f, 0x6a, 0x2a, 0x62, 0x7f, 0xb9, 0xf9, 0xb1, 0xa0,
|
0xb9, 0xf9, 0xb1, 0xa0, 0xe9, 0xa9, 0xe1, 0xf0, 0xb8, 0xf8, 0xb0, 0xa1, 0xe8, 0xa8, 0xe0, 0xf1,
|
||||||
0xe9, 0xa9, 0xe1, 0xf0, 0xb8, 0xf8, 0xb0, 0xa1, 0xe8, 0xa8, 0xe0, 0xf1, 0x5d, 0x1d, 0x55,
|
0x5d, 0x1d, 0x55, 0x84, 0xcd, 0x8d, 0xc5, 0x14, 0x5c, 0x1c, 0x54, 0x85, 0xcc, 0x8c, 0xc4, 0x15,
|
||||||
0x84, 0xcd, 0x8d, 0xc5, 0x14, 0x5c, 0x1c, 0x54, 0x85, 0xcc, 0x8c, 0xc4, 0x15, 0xbd, 0xfd,
|
0xbd, 0xfd, 0xb5, 0xa4, 0xed, 0xad, 0xe5, 0xf4, 0xbc, 0xfc, 0xb4, 0xa5, 0xec, 0xac, 0xe4, 0xf5,
|
||||||
0xb5, 0xa4, 0xed, 0xad, 0xe5, 0xf4, 0xbc, 0xfc, 0xb4, 0xa5, 0xec, 0xac, 0xe4, 0xf5, 0x39,
|
0x39, 0x79, 0x31, 0x20, 0x69, 0x29, 0x61, 0x70, 0x38, 0x78, 0x30, 0x21, 0x68, 0x28, 0x60, 0x71,
|
||||||
0x79, 0x31, 0x20, 0x69, 0x29, 0x61, 0x70, 0x38, 0x78, 0x30, 0x21, 0x68, 0x28, 0x60, 0x71,
|
0xb7, 0xf7, 0xbf, 0xa2, 0xe7, 0xa7, 0xef, 0xf2, 0xba, 0xfa, 0xb2, 0xaf, 0xea, 0xaa, 0xe2, 0xff,
|
||||||
0xb7, 0xf7, 0xbf, 0xa2, 0xe7, 0xa7, 0xef, 0xf2, 0xba, 0xfa, 0xb2, 0xaf, 0xea, 0xaa, 0xe2,
|
|
||||||
0xff,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Table 2: LFSR1 high-byte feedback permutation.
|
/// Table 2: LFSR1 high-byte feedback permutation.
|
||||||
pub const TAB2: [u8; 256] = [
|
pub const TAB2: [u8; 256] = [
|
||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x09, 0x08, 0x0b, 0x0a, 0x0d, 0x0c, 0x0f,
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x09, 0x08, 0x0b, 0x0a, 0x0d, 0x0c, 0x0f, 0x0e,
|
||||||
0x0e, 0x12, 0x13, 0x10, 0x11, 0x16, 0x17, 0x14, 0x15, 0x1b, 0x1a, 0x19, 0x18, 0x1f, 0x1e,
|
0x12, 0x13, 0x10, 0x11, 0x16, 0x17, 0x14, 0x15, 0x1b, 0x1a, 0x19, 0x18, 0x1f, 0x1e, 0x1d, 0x1c,
|
||||||
0x1d, 0x1c, 0x24, 0x25, 0x26, 0x27, 0x20, 0x21, 0x22, 0x23, 0x2d, 0x2c, 0x2f, 0x2e, 0x29,
|
0x24, 0x25, 0x26, 0x27, 0x20, 0x21, 0x22, 0x23, 0x2d, 0x2c, 0x2f, 0x2e, 0x29, 0x28, 0x2b, 0x2a,
|
||||||
0x28, 0x2b, 0x2a, 0x36, 0x37, 0x34, 0x35, 0x32, 0x33, 0x30, 0x31, 0x3f, 0x3e, 0x3d, 0x3c,
|
0x36, 0x37, 0x34, 0x35, 0x32, 0x33, 0x30, 0x31, 0x3f, 0x3e, 0x3d, 0x3c, 0x3b, 0x3a, 0x39, 0x38,
|
||||||
0x3b, 0x3a, 0x39, 0x38, 0x49, 0x48, 0x4b, 0x4a, 0x4d, 0x4c, 0x4f, 0x4e, 0x40, 0x41, 0x42,
|
0x49, 0x48, 0x4b, 0x4a, 0x4d, 0x4c, 0x4f, 0x4e, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
|
||||||
0x43, 0x44, 0x45, 0x46, 0x47, 0x5b, 0x5a, 0x59, 0x58, 0x5f, 0x5e, 0x5d, 0x5c, 0x52, 0x53,
|
0x5b, 0x5a, 0x59, 0x58, 0x5f, 0x5e, 0x5d, 0x5c, 0x52, 0x53, 0x50, 0x51, 0x56, 0x57, 0x54, 0x55,
|
||||||
0x50, 0x51, 0x56, 0x57, 0x54, 0x55, 0x6d, 0x6c, 0x6f, 0x6e, 0x69, 0x68, 0x6b, 0x6a, 0x64,
|
0x6d, 0x6c, 0x6f, 0x6e, 0x69, 0x68, 0x6b, 0x6a, 0x64, 0x65, 0x66, 0x67, 0x60, 0x61, 0x62, 0x63,
|
||||||
0x65, 0x66, 0x67, 0x60, 0x61, 0x62, 0x63, 0x7f, 0x7e, 0x7d, 0x7c, 0x7b, 0x7a, 0x79, 0x78,
|
0x7f, 0x7e, 0x7d, 0x7c, 0x7b, 0x7a, 0x79, 0x78, 0x76, 0x77, 0x74, 0x75, 0x72, 0x73, 0x70, 0x71,
|
||||||
0x76, 0x77, 0x74, 0x75, 0x72, 0x73, 0x70, 0x71, 0x92, 0x93, 0x90, 0x91, 0x96, 0x97, 0x94,
|
0x92, 0x93, 0x90, 0x91, 0x96, 0x97, 0x94, 0x95, 0x9b, 0x9a, 0x99, 0x98, 0x9f, 0x9e, 0x9d, 0x9c,
|
||||||
0x95, 0x9b, 0x9a, 0x99, 0x98, 0x9f, 0x9e, 0x9d, 0x9c, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85,
|
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x89, 0x88, 0x8b, 0x8a, 0x8d, 0x8c, 0x8f, 0x8e,
|
||||||
0x86, 0x87, 0x89, 0x88, 0x8b, 0x8a, 0x8d, 0x8c, 0x8f, 0x8e, 0xb6, 0xb7, 0xb4, 0xb5, 0xb2,
|
0xb6, 0xb7, 0xb4, 0xb5, 0xb2, 0xb3, 0xb0, 0xb1, 0xbf, 0xbe, 0xbd, 0xbc, 0xbb, 0xba, 0xb9, 0xb8,
|
||||||
0xb3, 0xb0, 0xb1, 0xbf, 0xbe, 0xbd, 0xbc, 0xbb, 0xba, 0xb9, 0xb8, 0xa4, 0xa5, 0xa6, 0xa7,
|
0xa4, 0xa5, 0xa6, 0xa7, 0xa0, 0xa1, 0xa2, 0xa3, 0xad, 0xac, 0xaf, 0xae, 0xa9, 0xa8, 0xab, 0xaa,
|
||||||
0xa0, 0xa1, 0xa2, 0xa3, 0xad, 0xac, 0xaf, 0xae, 0xa9, 0xa8, 0xab, 0xaa, 0xdb, 0xda, 0xd9,
|
0xdb, 0xda, 0xd9, 0xd8, 0xdf, 0xde, 0xdd, 0xdc, 0xd2, 0xd3, 0xd0, 0xd1, 0xd6, 0xd7, 0xd4, 0xd5,
|
||||||
0xd8, 0xdf, 0xde, 0xdd, 0xdc, 0xd2, 0xd3, 0xd0, 0xd1, 0xd6, 0xd7, 0xd4, 0xd5, 0xc9, 0xc8,
|
0xc9, 0xc8, 0xcb, 0xca, 0xcd, 0xcc, 0xcf, 0xce, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
|
||||||
0xcb, 0xca, 0xcd, 0xcc, 0xcf, 0xce, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xed,
|
0xed, 0xec, 0xef, 0xee, 0xe9, 0xe8, 0xeb, 0xea, 0xe4, 0xe5, 0xe6, 0xe7, 0xe0, 0xe1, 0xe2, 0xe3,
|
||||||
0xec, 0xef, 0xee, 0xe9, 0xe8, 0xeb, 0xea, 0xe4, 0xe5, 0xe6, 0xe7, 0xe0, 0xe1, 0xe2, 0xe3,
|
0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf6, 0xf7, 0xf4, 0xf5, 0xf2, 0xf3, 0xf0, 0xf1,
|
||||||
0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf6, 0xf7, 0xf4, 0xf5, 0xf2, 0xf3, 0xf0,
|
|
||||||
0xf1,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Table 3: LFSR1 low-byte feedback permutation.
|
/// Table 3: LFSR1 low-byte feedback permutation.
|
||||||
pub const TAB3: [u8; 512] = [
|
pub const TAB3: [u8; 512] = [
|
||||||
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb,
|
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||||
0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6,
|
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||||
0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92,
|
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||||
0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d,
|
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||||
0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49,
|
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||||
0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24,
|
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||||
0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00,
|
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||||
0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff,
|
||||||
0x00, 0x24, 0x49, 0x6d, 0x92, 0xb6, 0xdb, 0xff, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda,
|
0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe,
|
||||||
0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7,
|
0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe,
|
||||||
0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93,
|
0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe,
|
||||||
0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c,
|
0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe,
|
||||||
0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48,
|
0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe,
|
||||||
0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25,
|
0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe,
|
||||||
0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01,
|
0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe,
|
||||||
0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe,
|
0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe,
|
||||||
0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda, 0xfe, 0x01, 0x25, 0x48, 0x6c, 0x93, 0xb7, 0xda,
|
0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd,
|
||||||
0xfe, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4,
|
0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd,
|
||||||
0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90,
|
0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd,
|
||||||
0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f,
|
0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd,
|
||||||
0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b,
|
0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd,
|
||||||
0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26,
|
0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd,
|
||||||
0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02,
|
0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd,
|
||||||
0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd,
|
0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd,
|
||||||
0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9,
|
0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc,
|
||||||
0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5,
|
0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc,
|
||||||
0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91,
|
0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc,
|
||||||
0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e,
|
0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc,
|
||||||
0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a,
|
0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc,
|
||||||
0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27,
|
0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc,
|
||||||
0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03,
|
0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc,
|
||||||
0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc,
|
0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc,
|
||||||
0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8,
|
|
||||||
0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5,
|
|
||||||
0xd8, 0xfc,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Table 4: LFSR0 byte permutation (used in initialization and output).
|
/// Table 4: LFSR0 byte permutation (used in initialization and output).
|
||||||
pub const TAB4: [u8; 256] = [
|
pub const TAB4: [u8; 256] = [
|
||||||
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70,
|
0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0,
|
||||||
0xf0, 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8,
|
0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8,
|
||||||
0x78, 0xf8, 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 0x14, 0x94, 0x54, 0xd4, 0x34,
|
0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4,
|
||||||
0xb4, 0x74, 0xf4, 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 0x1c, 0x9c, 0x5c, 0xdc,
|
0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc,
|
||||||
0x3c, 0xbc, 0x7c, 0xfc, 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 0x12, 0x92, 0x52,
|
0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2,
|
||||||
0xd2, 0x32, 0xb2, 0x72, 0xf2, 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 0x1a, 0x9a,
|
0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa,
|
||||||
0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 0x16,
|
0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6,
|
||||||
0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee,
|
0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe,
|
||||||
0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61,
|
0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1,
|
||||||
0xe1, 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9,
|
0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9,
|
||||||
0x69, 0xe9, 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, 0x05, 0x85, 0x45, 0xc5, 0x25,
|
0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5,
|
||||||
0xa5, 0x65, 0xe5, 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, 0x0d, 0x8d, 0x4d, 0xcd,
|
0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd,
|
||||||
0x2d, 0xad, 0x6d, 0xed, 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, 0x03, 0x83, 0x43,
|
0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3,
|
||||||
0xc3, 0x23, 0xa3, 0x63, 0xe3, 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, 0x0b, 0x8b,
|
0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb,
|
||||||
0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, 0x07,
|
0x07, 0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
|
||||||
0x87, 0x47, 0xc7, 0x27, 0xa7, 0x67, 0xe7, 0x17, 0x97, 0x57, 0xd7, 0x37, 0xb7, 0x77, 0xf7,
|
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f, 0xff,
|
||||||
0x0f, 0x8f, 0x4f, 0xcf, 0x2f, 0xaf, 0x6f, 0xef, 0x1f, 0x9f, 0x5f, 0xdf, 0x3f, 0xbf, 0x7f,
|
|
||||||
0xff,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Table 5: LFSR1 output permutation for the Stevenson attack.
|
/// Table 5: LFSR1 output permutation for the Stevenson attack.
|
||||||
/// This is the inverse byte-reversal of TAB4.
|
/// This is the inverse byte-reversal of TAB4.
|
||||||
pub const TAB5: [u8; 256] = [
|
pub const TAB5: [u8; 256] = [
|
||||||
0xff, 0x7f, 0xbf, 0x3f, 0xdf, 0x5f, 0x9f, 0x1f, 0xef, 0x6f, 0xaf, 0x2f, 0xcf, 0x4f, 0x8f,
|
0xff, 0x7f, 0xbf, 0x3f, 0xdf, 0x5f, 0x9f, 0x1f, 0xef, 0x6f, 0xaf, 0x2f, 0xcf, 0x4f, 0x8f, 0x0f,
|
||||||
0x0f, 0xf7, 0x77, 0xb7, 0x37, 0xd7, 0x57, 0x97, 0x17, 0xe7, 0x67, 0xa7, 0x27, 0xc7, 0x47,
|
0xf7, 0x77, 0xb7, 0x37, 0xd7, 0x57, 0x97, 0x17, 0xe7, 0x67, 0xa7, 0x27, 0xc7, 0x47, 0x87, 0x07,
|
||||||
0x87, 0x07, 0xfb, 0x7b, 0xbb, 0x3b, 0xdb, 0x5b, 0x9b, 0x1b, 0xeb, 0x6b, 0xab, 0x2b, 0xcb,
|
0xfb, 0x7b, 0xbb, 0x3b, 0xdb, 0x5b, 0x9b, 0x1b, 0xeb, 0x6b, 0xab, 0x2b, 0xcb, 0x4b, 0x8b, 0x0b,
|
||||||
0x4b, 0x8b, 0x0b, 0xf3, 0x73, 0xb3, 0x33, 0xd3, 0x53, 0x93, 0x13, 0xe3, 0x63, 0xa3, 0x23,
|
0xf3, 0x73, 0xb3, 0x33, 0xd3, 0x53, 0x93, 0x13, 0xe3, 0x63, 0xa3, 0x23, 0xc3, 0x43, 0x83, 0x03,
|
||||||
0xc3, 0x43, 0x83, 0x03, 0xfd, 0x7d, 0xbd, 0x3d, 0xdd, 0x5d, 0x9d, 0x1d, 0xed, 0x6d, 0xad,
|
0xfd, 0x7d, 0xbd, 0x3d, 0xdd, 0x5d, 0x9d, 0x1d, 0xed, 0x6d, 0xad, 0x2d, 0xcd, 0x4d, 0x8d, 0x0d,
|
||||||
0x2d, 0xcd, 0x4d, 0x8d, 0x0d, 0xf5, 0x75, 0xb5, 0x35, 0xd5, 0x55, 0x95, 0x15, 0xe5, 0x65,
|
0xf5, 0x75, 0xb5, 0x35, 0xd5, 0x55, 0x95, 0x15, 0xe5, 0x65, 0xa5, 0x25, 0xc5, 0x45, 0x85, 0x05,
|
||||||
0xa5, 0x25, 0xc5, 0x45, 0x85, 0x05, 0xf9, 0x79, 0xb9, 0x39, 0xd9, 0x59, 0x99, 0x19, 0xe9,
|
0xf9, 0x79, 0xb9, 0x39, 0xd9, 0x59, 0x99, 0x19, 0xe9, 0x69, 0xa9, 0x29, 0xc9, 0x49, 0x89, 0x09,
|
||||||
0x69, 0xa9, 0x29, 0xc9, 0x49, 0x89, 0x09, 0xf1, 0x71, 0xb1, 0x31, 0xd1, 0x51, 0x91, 0x11,
|
0xf1, 0x71, 0xb1, 0x31, 0xd1, 0x51, 0x91, 0x11, 0xe1, 0x61, 0xa1, 0x21, 0xc1, 0x41, 0x81, 0x01,
|
||||||
0xe1, 0x61, 0xa1, 0x21, 0xc1, 0x41, 0x81, 0x01, 0xfe, 0x7e, 0xbe, 0x3e, 0xde, 0x5e, 0x9e,
|
0xfe, 0x7e, 0xbe, 0x3e, 0xde, 0x5e, 0x9e, 0x1e, 0xee, 0x6e, 0xae, 0x2e, 0xce, 0x4e, 0x8e, 0x0e,
|
||||||
0x1e, 0xee, 0x6e, 0xae, 0x2e, 0xce, 0x4e, 0x8e, 0x0e, 0xf6, 0x76, 0xb6, 0x36, 0xd6, 0x56,
|
0xf6, 0x76, 0xb6, 0x36, 0xd6, 0x56, 0x96, 0x16, 0xe6, 0x66, 0xa6, 0x26, 0xc6, 0x46, 0x86, 0x06,
|
||||||
0x96, 0x16, 0xe6, 0x66, 0xa6, 0x26, 0xc6, 0x46, 0x86, 0x06, 0xfa, 0x7a, 0xba, 0x3a, 0xda,
|
0xfa, 0x7a, 0xba, 0x3a, 0xda, 0x5a, 0x9a, 0x1a, 0xea, 0x6a, 0xaa, 0x2a, 0xca, 0x4a, 0x8a, 0x0a,
|
||||||
0x5a, 0x9a, 0x1a, 0xea, 0x6a, 0xaa, 0x2a, 0xca, 0x4a, 0x8a, 0x0a, 0xf2, 0x72, 0xb2, 0x32,
|
0xf2, 0x72, 0xb2, 0x32, 0xd2, 0x52, 0x92, 0x12, 0xe2, 0x62, 0xa2, 0x22, 0xc2, 0x42, 0x82, 0x02,
|
||||||
0xd2, 0x52, 0x92, 0x12, 0xe2, 0x62, 0xa2, 0x22, 0xc2, 0x42, 0x82, 0x02, 0xfc, 0x7c, 0xbc,
|
0xfc, 0x7c, 0xbc, 0x3c, 0xdc, 0x5c, 0x9c, 0x1c, 0xec, 0x6c, 0xac, 0x2c, 0xcc, 0x4c, 0x8c, 0x0c,
|
||||||
0x3c, 0xdc, 0x5c, 0x9c, 0x1c, 0xec, 0x6c, 0xac, 0x2c, 0xcc, 0x4c, 0x8c, 0x0c, 0xf4, 0x74,
|
0xf4, 0x74, 0xb4, 0x34, 0xd4, 0x54, 0x94, 0x14, 0xe4, 0x64, 0xa4, 0x24, 0xc4, 0x44, 0x84, 0x04,
|
||||||
0xb4, 0x34, 0xd4, 0x54, 0x94, 0x14, 0xe4, 0x64, 0xa4, 0x24, 0xc4, 0x44, 0x84, 0x04, 0xf8,
|
0xf8, 0x78, 0xb8, 0x38, 0xd8, 0x58, 0x98, 0x18, 0xe8, 0x68, 0xa8, 0x28, 0xc8, 0x48, 0x88, 0x08,
|
||||||
0x78, 0xb8, 0x38, 0xd8, 0x58, 0x98, 0x18, 0xe8, 0x68, 0xa8, 0x28, 0xc8, 0x48, 0x88, 0x08,
|
0xf0, 0x70, 0xb0, 0x30, 0xd0, 0x50, 0x90, 0x10, 0xe0, 0x60, 0xa0, 0x20, 0xc0, 0x40, 0x80, 0x00,
|
||||||
0xf0, 0x70, 0xb0, 0x30, 0xd0, 0x50, 0x90, 0x10, 0xe0, 0x60, 0xa0, 0x20, 0xc0, 0x40, 0x80,
|
|
||||||
0x00,
|
|
||||||
];
|
];
|
||||||
|
|||||||
+8
-2
@@ -8,7 +8,10 @@ use crate::udf;
|
|||||||
|
|
||||||
impl Disc {
|
impl Disc {
|
||||||
/// Scan Blu-ray titles from MPLS playlists.
|
/// Scan Blu-ray titles from MPLS playlists.
|
||||||
pub(super) fn scan_bluray_titles(reader: &mut dyn SectorReader, udf_fs: &udf::UdfFs) -> Vec<DiscTitle> {
|
pub(super) fn scan_bluray_titles(
|
||||||
|
reader: &mut dyn SectorReader,
|
||||||
|
udf_fs: &udf::UdfFs,
|
||||||
|
) -> Vec<DiscTitle> {
|
||||||
let mut titles = Vec::new();
|
let mut titles = Vec::new();
|
||||||
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
|
if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") {
|
||||||
for entry in &playlist_dir.entries {
|
for entry in &playlist_dir.entries {
|
||||||
@@ -190,7 +193,10 @@ impl Disc {
|
|||||||
/// Read disc title from META/DL/bdmt_eng.xml (Blu-ray Disc Meta Table).
|
/// Read disc title from META/DL/bdmt_eng.xml (Blu-ray Disc Meta Table).
|
||||||
/// Prefers English, falls back to first available language.
|
/// Prefers English, falls back to first available language.
|
||||||
/// Returns None if META directory is empty or XML has no usable title.
|
/// Returns None if META directory is empty or XML has no usable title.
|
||||||
pub(super) fn read_meta_title(reader: &mut dyn SectorReader, udf_fs: &udf::UdfFs) -> Option<String> {
|
pub(super) fn read_meta_title(
|
||||||
|
reader: &mut dyn SectorReader,
|
||||||
|
udf_fs: &udf::UdfFs,
|
||||||
|
) -> Option<String> {
|
||||||
let meta_dir = udf_fs.find_dir("/BDMV/META")?;
|
let meta_dir = udf_fs.find_dir("/BDMV/META")?;
|
||||||
for sub in &meta_dir.entries {
|
for sub in &meta_dir.entries {
|
||||||
if !sub.is_dir {
|
if !sub.is_dir {
|
||||||
|
|||||||
+7
-7
@@ -7,7 +7,10 @@ use crate::udf;
|
|||||||
|
|
||||||
impl Disc {
|
impl Disc {
|
||||||
/// Scan DVD titles from IFO files (VIDEO_TS.IFO + VTS_XX_0.IFO).
|
/// Scan DVD titles from IFO files (VIDEO_TS.IFO + VTS_XX_0.IFO).
|
||||||
pub(super) fn scan_dvd_titles(reader: &mut dyn SectorReader, udf_fs: &udf::UdfFs) -> Vec<DiscTitle> {
|
pub(super) fn scan_dvd_titles(
|
||||||
|
reader: &mut dyn SectorReader,
|
||||||
|
udf_fs: &udf::UdfFs,
|
||||||
|
) -> Vec<DiscTitle> {
|
||||||
let dvd_info = match ifo::parse_vmg(reader, udf_fs) {
|
let dvd_info = match ifo::parse_vmg(reader, udf_fs) {
|
||||||
Ok(info) => info,
|
Ok(info) => info,
|
||||||
Err(_) => return Vec::new(),
|
Err(_) => return Vec::new(),
|
||||||
@@ -83,8 +86,8 @@ impl Disc {
|
|||||||
.cells
|
.cells
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cell| {
|
.map(|cell| {
|
||||||
let start = ts.vob_start_sector + cell.first_sector;
|
let start = ts.vob_start_sector.saturating_add(cell.first_sector);
|
||||||
let count = cell.last_sector.saturating_sub(cell.first_sector) + 1;
|
let count = cell.last_sector.saturating_sub(cell.first_sector).saturating_add(1);
|
||||||
Extent {
|
Extent {
|
||||||
start_lba: start,
|
start_lba: start,
|
||||||
sector_count: count,
|
sector_count: count,
|
||||||
@@ -92,10 +95,7 @@ impl Disc {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let size_bytes: u64 = extents
|
let size_bytes: u64 = extents.iter().map(|e| e.sector_count as u64 * 2048).sum();
|
||||||
.iter()
|
|
||||||
.map(|e| e.sector_count as u64 * 2048)
|
|
||||||
.sum();
|
|
||||||
|
|
||||||
// Build pre-formatted palette codec_data for VobSub subtitle streams
|
// Build pre-formatted palette codec_data for VobSub subtitle streams
|
||||||
let codec_data = dvd_title
|
let codec_data = dvd_title
|
||||||
|
|||||||
+5
-7
@@ -11,13 +11,15 @@ use crate::udf;
|
|||||||
pub(super) struct HandshakeResult {
|
pub(super) struct HandshakeResult {
|
||||||
pub volume_id: [u8; 16],
|
pub volume_id: [u8; 16],
|
||||||
pub read_data_key: Option<[u8; 16]>,
|
pub read_data_key: Option<[u8; 16]>,
|
||||||
pub error: Option<crate::error::Error>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Disc {
|
impl Disc {
|
||||||
/// SCSI handshake result — volume ID and bus keys from ECDH authentication.
|
/// SCSI handshake result — volume ID and bus keys from ECDH authentication.
|
||||||
/// Only available when scanning from a real drive (not ISO images).
|
/// Only available when scanning from a real drive (not ISO images).
|
||||||
pub(super) fn do_handshake(session: &mut crate::drive::DriveSession, opts: &ScanOptions) -> Option<HandshakeResult> {
|
pub(super) fn do_handshake(
|
||||||
|
session: &mut crate::drive::DriveSession,
|
||||||
|
opts: &ScanOptions,
|
||||||
|
) -> Option<HandshakeResult> {
|
||||||
use crate::aacs::{self, KeyDb};
|
use crate::aacs::{self, KeyDb};
|
||||||
|
|
||||||
let keydb_path = opts.resolve_keydb()?;
|
let keydb_path = opts.resolve_keydb()?;
|
||||||
@@ -35,7 +37,6 @@ impl Disc {
|
|||||||
return Some(HandshakeResult {
|
return Some(HandshakeResult {
|
||||||
volume_id,
|
volume_id,
|
||||||
read_data_key,
|
read_data_key,
|
||||||
error: None,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -45,10 +46,9 @@ impl Disc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
last_error.map(|e| HandshakeResult {
|
last_error.map(|_e| HandshakeResult {
|
||||||
volume_id: [0u8; 16],
|
volume_id: [0u8; 16],
|
||||||
read_data_key: None,
|
read_data_key: None,
|
||||||
error: Some(e),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +90,6 @@ impl Disc {
|
|||||||
// (KEYDB VUK lookup by disc hash works without volume ID)
|
// (KEYDB VUK lookup by disc hash works without volume ID)
|
||||||
let volume_id = handshake.map(|h| h.volume_id).unwrap_or([0u8; 16]);
|
let volume_id = handshake.map(|h| h.volume_id).unwrap_or([0u8; 16]);
|
||||||
let read_data_key = handshake.and_then(|h| h.read_data_key);
|
let read_data_key = handshake.and_then(|h| h.read_data_key);
|
||||||
let handshake_error = None;
|
|
||||||
|
|
||||||
// Resolve: tries all available paths — KEYDB VUK, media key, processing key, device key
|
// Resolve: tries all available paths — KEYDB VUK, media key, processing key, device key
|
||||||
let resolved = aacs::resolve_keys(
|
let resolved = aacs::resolve_keys(
|
||||||
@@ -118,7 +117,6 @@ impl Disc {
|
|||||||
unit_keys: resolved.unit_keys,
|
unit_keys: resolved.unit_keys,
|
||||||
read_data_key,
|
read_data_key,
|
||||||
volume_id,
|
volume_id,
|
||||||
handshake_error,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-21
@@ -370,8 +370,6 @@ pub struct AacsState {
|
|||||||
pub read_data_key: Option<[u8; 16]>,
|
pub read_data_key: Option<[u8; 16]>,
|
||||||
/// Volume ID (16 bytes) -- from SCSI handshake
|
/// Volume ID (16 bytes) -- from SCSI handshake
|
||||||
pub volume_id: [u8; 16],
|
pub volume_id: [u8; 16],
|
||||||
/// Handshake error code if authentication failed (None = no HC, or success)
|
|
||||||
pub handshake_error: Option<crate::error::Error>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How AACS keys were resolved.
|
/// How AACS keys were resolved.
|
||||||
@@ -414,7 +412,6 @@ pub struct ScanOptions {
|
|||||||
pub keydb_path: Option<std::path::PathBuf>,
|
pub keydb_path: Option<std::path::PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl ScanOptions {
|
impl ScanOptions {
|
||||||
/// Create options with a specific KEYDB path.
|
/// Create options with a specific KEYDB path.
|
||||||
pub fn with_keydb(path: impl Into<std::path::PathBuf>) -> Self {
|
pub fn with_keydb(path: impl Into<std::path::PathBuf>) -> Self {
|
||||||
@@ -577,9 +574,15 @@ impl Disc {
|
|||||||
|
|
||||||
// 3. Titles — BD (MPLS playlists) or DVD (IFO title sets)
|
// 3. Titles — BD (MPLS playlists) or DVD (IFO title sets)
|
||||||
let (mut titles, content_format) = if udf_fs.find_dir("/BDMV").is_some() {
|
let (mut titles, content_format) = if udf_fs.find_dir("/BDMV").is_some() {
|
||||||
(Self::scan_bluray_titles(reader, &udf_fs), ContentFormat::BdTs)
|
(
|
||||||
|
Self::scan_bluray_titles(reader, &udf_fs),
|
||||||
|
ContentFormat::BdTs,
|
||||||
|
)
|
||||||
} else if udf_fs.find_dir("/VIDEO_TS").is_some() {
|
} else if udf_fs.find_dir("/VIDEO_TS").is_some() {
|
||||||
(Self::scan_dvd_titles(reader, &udf_fs), ContentFormat::MpegPs)
|
(
|
||||||
|
Self::scan_dvd_titles(reader, &udf_fs),
|
||||||
|
ContentFormat::MpegPs,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
(Vec::new(), ContentFormat::BdTs)
|
(Vec::new(), ContentFormat::BdTs)
|
||||||
};
|
};
|
||||||
@@ -667,7 +670,6 @@ impl Disc {
|
|||||||
let lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
|
let lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
|
||||||
Ok(lba + 1)
|
Ok(lba + 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Decrypted reader ──────────────────────────────────────────────────────
|
// ─── Decrypted reader ──────────────────────────────────────────────────────
|
||||||
@@ -684,7 +686,6 @@ 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>,
|
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,
|
||||||
@@ -716,10 +717,7 @@ impl Disc {
|
|||||||
session: &'a mut DriveSession,
|
session: &'a mut DriveSession,
|
||||||
title_idx: usize,
|
title_idx: usize,
|
||||||
) -> Result<ContentReader<'a>> {
|
) -> Result<ContentReader<'a>> {
|
||||||
let title = self
|
let title = self.titles.get(title_idx).ok_or(Error::DiscTitleRange {
|
||||||
.titles
|
|
||||||
.get(title_idx)
|
|
||||||
.ok_or(Error::DiscTitleRange {
|
|
||||||
index: title_idx,
|
index: title_idx,
|
||||||
count: self.titles.len(),
|
count: self.titles.len(),
|
||||||
})?;
|
})?;
|
||||||
@@ -735,7 +733,6 @@ impl Disc {
|
|||||||
session,
|
session,
|
||||||
aacs: self.aacs.as_ref(),
|
aacs: self.aacs.as_ref(),
|
||||||
css: self.css.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,
|
||||||
@@ -756,7 +753,7 @@ impl Disc {
|
|||||||
/// Reads /sys/block/<dev>/queue/max_hw_sectors_kb on Linux.
|
/// Reads /sys/block/<dev>/queue/max_hw_sectors_kb on Linux.
|
||||||
/// For sg devices, resolves the corresponding block device via sysfs.
|
/// For sg devices, resolves the corresponding block device via sysfs.
|
||||||
/// Returns a value aligned to 3 sectors (one aligned unit).
|
/// Returns a value aligned to 3 sectors (one aligned unit).
|
||||||
fn detect_max_batch_sectors(device_path: &str) -> u16 {
|
pub(crate) fn detect_max_batch_sectors(device_path: &str) -> u16 {
|
||||||
let dev_name = device_path.rsplit('/').next().unwrap_or("");
|
let dev_name = device_path.rsplit('/').next().unwrap_or("");
|
||||||
if dev_name.is_empty() {
|
if dev_name.is_empty() {
|
||||||
return DEFAULT_BATCH_SECTORS;
|
return DEFAULT_BATCH_SECTORS;
|
||||||
@@ -793,12 +790,12 @@ fn detect_max_batch_sectors(device_path: &str) -> u16 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Read strategy constants
|
/// Read strategy constants
|
||||||
const MAX_BATCH_SECTORS: u16 = 510; // absolute max (170 aligned units ≈ 1MB)
|
pub(crate) const MAX_BATCH_SECTORS: u16 = 510; // absolute max (170 aligned units ≈ 1MB)
|
||||||
const DEFAULT_BATCH_SECTORS: u16 = 60; // fallback: typical kernel limit (120KB = 60 sectors)
|
pub(crate) const DEFAULT_BATCH_SECTORS: u16 = 60; // fallback: typical kernel limit (120KB = 60 sectors)
|
||||||
const MIN_BATCH_SECTORS: u16 = 3; // 1 aligned unit = 6KB (error recovery)
|
pub(crate) const MIN_BATCH_SECTORS: u16 = 3; // 1 aligned unit = 6KB (error recovery)
|
||||||
const RAMP_BATCH_AFTER: u32 = 5; // successes before doubling batch size
|
pub(crate) const RAMP_BATCH_AFTER: u32 = 5; // successes before doubling batch size
|
||||||
const RAMP_SPEED_AFTER: u32 = 50; // successes at max batch before restoring speed
|
pub(crate) const RAMP_SPEED_AFTER: u32 = 50; // successes at max batch before restoring speed
|
||||||
const SLOW_SPEED_AFTER: u32 = 3; // consecutive errors before reducing disc speed
|
pub(crate) const SLOW_SPEED_AFTER: u32 = 3; // consecutive errors before reducing disc speed
|
||||||
|
|
||||||
impl<'a> ContentReader<'a> {
|
impl<'a> ContentReader<'a> {
|
||||||
/// Total bytes across all extents (for progress display).
|
/// Total bytes across all extents (for progress display).
|
||||||
@@ -814,8 +811,7 @@ impl<'a> ContentReader<'a> {
|
|||||||
/// Returns None when all extents are exhausted.
|
/// Returns None when all extents are exhausted.
|
||||||
pub fn read_unit(&mut self) -> Result<Option<Vec<u8>>> {
|
pub fn read_unit(&mut self) -> Result<Option<Vec<u8>>> {
|
||||||
// Refill buffer if empty
|
// Refill buffer if empty
|
||||||
if self.buf_pos >= self.buf_len
|
if self.buf_pos >= self.buf_len && !self.fill_buffer()? {
|
||||||
&& !self.fill_buffer()? {
|
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-8
@@ -200,7 +200,9 @@ pub fn parse_vmg(reader: &mut dyn SectorReader, udf: &UdfFs) -> Result<DvdInfo>
|
|||||||
|
|
||||||
// Read TT_SRPT — it's at the given sector offset relative to the start of VIDEO_TS.IFO.
|
// Read TT_SRPT — it's at the given sector offset relative to the start of VIDEO_TS.IFO.
|
||||||
// In the IFO file data we already have, sector offsets are relative to the IFO start.
|
// In the IFO file data we already have, sector offsets are relative to the IFO start.
|
||||||
let tt_srpt_offset = (tt_srpt_sector as usize).checked_mul(SECTOR_SIZE).ok_or(Error::IfoParse)?;
|
let tt_srpt_offset = (tt_srpt_sector as usize)
|
||||||
|
.checked_mul(SECTOR_SIZE)
|
||||||
|
.ok_or(Error::IfoParse)?;
|
||||||
|
|
||||||
// TT_SRPT may be beyond what we read; if so, it's embedded in the file data
|
// TT_SRPT may be beyond what we read; if so, it's embedded in the file data
|
||||||
// (IFO files are typically small, a few sectors). Check bounds.
|
// (IFO files are typically small, a few sectors). Check bounds.
|
||||||
@@ -312,7 +314,9 @@ fn parse_vts(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse PGC information table
|
// Parse PGC information table
|
||||||
let pgcit_offset = (pgcit_sector as usize).checked_mul(SECTOR_SIZE).ok_or(Error::IfoParse)?;
|
let pgcit_offset = (pgcit_sector as usize)
|
||||||
|
.checked_mul(SECTOR_SIZE)
|
||||||
|
.ok_or(Error::IfoParse)?;
|
||||||
let titles = parse_pgcit(&vts_data, pgcit_offset, titles_info)?;
|
let titles = parse_pgcit(&vts_data, pgcit_offset, titles_info)?;
|
||||||
|
|
||||||
Ok(DvdTitleSet {
|
Ok(DvdTitleSet {
|
||||||
@@ -409,8 +413,10 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result<DvdAudioAttr> {
|
|||||||
|
|
||||||
// Language code: bytes 2-3 as ISO 639
|
// Language code: bytes 2-3 as ISO 639
|
||||||
let lang_bytes = sub_slice(data, offset + 2, 2)?;
|
let lang_bytes = sub_slice(data, offset + 2, 2)?;
|
||||||
let language = if lang_bytes[0] >= b'a' && lang_bytes[0] <= b'z'
|
let language = if lang_bytes[0] >= b'a'
|
||||||
&& lang_bytes[1] >= b'a' && lang_bytes[1] <= b'z'
|
&& lang_bytes[0] <= b'z'
|
||||||
|
&& lang_bytes[1] >= b'a'
|
||||||
|
&& lang_bytes[1] <= b'z'
|
||||||
{
|
{
|
||||||
String::from_utf8_lossy(lang_bytes).to_string()
|
String::from_utf8_lossy(lang_bytes).to_string()
|
||||||
} else if lang_bytes[0] == 0 && lang_bytes[1] == 0 {
|
} else if lang_bytes[0] == 0 && lang_bytes[1] == 0 {
|
||||||
@@ -437,8 +443,10 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result<DvdAudioAttr> {
|
|||||||
fn parse_subtitle_attr(data: &[u8], offset: usize) -> Result<DvdSubtitleAttr> {
|
fn parse_subtitle_attr(data: &[u8], offset: usize) -> Result<DvdSubtitleAttr> {
|
||||||
// Language code: bytes 2-3 as ISO 639
|
// Language code: bytes 2-3 as ISO 639
|
||||||
let lang_bytes = sub_slice(data, offset + 2, 2)?;
|
let lang_bytes = sub_slice(data, offset + 2, 2)?;
|
||||||
let language = if lang_bytes[0] >= b'a' && lang_bytes[0] <= b'z'
|
let language = if lang_bytes[0] >= b'a'
|
||||||
&& lang_bytes[1] >= b'a' && lang_bytes[1] <= b'z'
|
&& lang_bytes[0] <= b'z'
|
||||||
|
&& lang_bytes[1] >= b'a'
|
||||||
|
&& lang_bytes[1] <= b'z'
|
||||||
{
|
{
|
||||||
String::from_utf8_lossy(lang_bytes).to_string()
|
String::from_utf8_lossy(lang_bytes).to_string()
|
||||||
} else if lang_bytes[0] == 0 && lang_bytes[1] == 0 {
|
} else if lang_bytes[0] == 0 && lang_bytes[1] == 0 {
|
||||||
@@ -488,7 +496,9 @@ fn parse_pgcit(
|
|||||||
|
|
||||||
// PGC byte offset relative to VTS_PGCIT start
|
// PGC byte offset relative to VTS_PGCIT start
|
||||||
let pgc_byte_offset = be_u32(data, entry_offset + 4)? as usize;
|
let pgc_byte_offset = be_u32(data, entry_offset + 4)? as usize;
|
||||||
let pgc_abs = pgcit_offset.checked_add(pgc_byte_offset).ok_or(Error::IfoParse)?;
|
let pgc_abs = pgcit_offset
|
||||||
|
.checked_add(pgc_byte_offset)
|
||||||
|
.ok_or(Error::IfoParse)?;
|
||||||
|
|
||||||
match parse_pgc(data, pgc_abs, chapter_count) {
|
match parse_pgc(data, pgc_abs, chapter_count) {
|
||||||
Ok(title) => titles.push(title),
|
Ok(title) => titles.push(title),
|
||||||
@@ -527,7 +537,9 @@ fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result<DvdTitle>
|
|||||||
// Parse cells
|
// Parse cells
|
||||||
let mut cells = Vec::with_capacity(num_cells);
|
let mut cells = Vec::with_capacity(num_cells);
|
||||||
if cell_playback_offset > 0 && num_cells > 0 {
|
if cell_playback_offset > 0 && num_cells > 0 {
|
||||||
let cell_base = pgc_offset.checked_add(cell_playback_offset).ok_or(Error::IfoParse)?;
|
let cell_base = pgc_offset
|
||||||
|
.checked_add(cell_playback_offset)
|
||||||
|
.ok_or(Error::IfoParse)?;
|
||||||
for i in 0..num_cells {
|
for i in 0..num_cells {
|
||||||
let co = cell_base + i * 24;
|
let co = cell_base + i * 24;
|
||||||
if co + 24 > data.len() {
|
if co + 24 > data.len() {
|
||||||
|
|||||||
@@ -143,7 +143,9 @@ fn parse_playback_config(xml: &str, map: &mut HashMap<String, u16>) {
|
|||||||
while pos < xml.len() {
|
while pos < xml.len() {
|
||||||
let tag_start = if let Some(p) = xml[pos..].find("<AudioStreams>") {
|
let tag_start = if let Some(p) = xml[pos..].find("<AudioStreams>") {
|
||||||
Some(p + pos)
|
Some(p + pos)
|
||||||
} else { xml[pos..].find("<SubtitlesStreams>").map(|p| p + pos) };
|
} else {
|
||||||
|
xml[pos..].find("<SubtitlesStreams>").map(|p| p + pos)
|
||||||
|
};
|
||||||
|
|
||||||
let tag_start = match tag_start {
|
let tag_start = match tag_start {
|
||||||
Some(p) => p,
|
Some(p) => p,
|
||||||
|
|||||||
+1
-1
@@ -73,9 +73,9 @@ pub mod css;
|
|||||||
pub mod disc;
|
pub mod disc;
|
||||||
pub mod drive;
|
pub mod drive;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod ifo;
|
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
|
pub mod ifo;
|
||||||
pub mod keydb;
|
pub mod keydb;
|
||||||
pub mod labels;
|
pub mod labels;
|
||||||
pub mod mpls;
|
pub mod mpls;
|
||||||
|
|||||||
+37
-8
@@ -264,7 +264,8 @@ pub fn parse(data: &[u8]) -> Result<Playlist> {
|
|||||||
}
|
}
|
||||||
let mark_type = ms[mpos];
|
let mark_type = ms[mpos];
|
||||||
let play_item_ref = u16::from_be_bytes([ms[mpos + 2], ms[mpos + 3]]);
|
let play_item_ref = u16::from_be_bytes([ms[mpos + 2], ms[mpos + 3]]);
|
||||||
let timestamp = u32::from_be_bytes([ms[mpos + 4], ms[mpos + 5], ms[mpos + 6], ms[mpos + 7]]);
|
let timestamp =
|
||||||
|
u32::from_be_bytes([ms[mpos + 4], ms[mpos + 5], ms[mpos + 6], ms[mpos + 7]]);
|
||||||
marks.push(PlaylistMark {
|
marks.push(PlaylistMark {
|
||||||
mark_type,
|
mark_type,
|
||||||
play_item_ref,
|
play_item_ref,
|
||||||
@@ -771,9 +772,21 @@ mod tests {
|
|||||||
fn parse_marks_chapter_entries() {
|
fn parse_marks_chapter_entries() {
|
||||||
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
|
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
|
||||||
let marks = vec![
|
let marks = vec![
|
||||||
TestMark { mark_type: 1, play_item_ref: 0, timestamp: 90000 },
|
TestMark {
|
||||||
TestMark { mark_type: 1, play_item_ref: 0, timestamp: 4500000 },
|
mark_type: 1,
|
||||||
TestMark { mark_type: 1, play_item_ref: 0, timestamp: 9000000 },
|
play_item_ref: 0,
|
||||||
|
timestamp: 90000,
|
||||||
|
},
|
||||||
|
TestMark {
|
||||||
|
mark_type: 1,
|
||||||
|
play_item_ref: 0,
|
||||||
|
timestamp: 4500000,
|
||||||
|
},
|
||||||
|
TestMark {
|
||||||
|
mark_type: 1,
|
||||||
|
play_item_ref: 0,
|
||||||
|
timestamp: 9000000,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let data = build_mpls_with_marks(
|
let data = build_mpls_with_marks(
|
||||||
@@ -799,10 +812,26 @@ mod tests {
|
|||||||
|
|
||||||
// Chapters at 0s, 100s, 200s relative to in_time
|
// Chapters at 0s, 100s, 200s relative to in_time
|
||||||
let marks = vec![
|
let marks = vec![
|
||||||
TestMark { mark_type: 1, play_item_ref: 0, timestamp: in_time },
|
TestMark {
|
||||||
TestMark { mark_type: 1, play_item_ref: 0, timestamp: in_time + 45000 * 100 },
|
mark_type: 1,
|
||||||
TestMark { mark_type: 1, play_item_ref: 0, timestamp: in_time + 45000 * 200 },
|
play_item_ref: 0,
|
||||||
TestMark { mark_type: 2, play_item_ref: 0, timestamp: in_time + 45000 * 50 }, // non-chapter mark
|
timestamp: in_time,
|
||||||
|
},
|
||||||
|
TestMark {
|
||||||
|
mark_type: 1,
|
||||||
|
play_item_ref: 0,
|
||||||
|
timestamp: in_time + 45000 * 100,
|
||||||
|
},
|
||||||
|
TestMark {
|
||||||
|
mark_type: 1,
|
||||||
|
play_item_ref: 0,
|
||||||
|
timestamp: in_time + 45000 * 200,
|
||||||
|
},
|
||||||
|
TestMark {
|
||||||
|
mark_type: 2,
|
||||||
|
play_item_ref: 0,
|
||||||
|
timestamp: in_time + 45000 * 50,
|
||||||
|
}, // non-chapter mark
|
||||||
];
|
];
|
||||||
|
|
||||||
let data = build_mpls_with_marks(
|
let data = build_mpls_with_marks(
|
||||||
|
|||||||
@@ -87,9 +87,8 @@ pub fn find_dts_hd_ext_sync(data: &[u8]) -> Option<usize> {
|
|||||||
/// ((ext[6] & 0x1F) << 11) | (ext[7] << 3) | (ext[8] >> 5) + 1
|
/// ((ext[6] & 0x1F) << 11) | (ext[7] << 3) | (ext[8] >> 5) + 1
|
||||||
pub fn dts_hd_ext_frame_size(ext: &[u8]) -> usize {
|
pub fn dts_hd_ext_frame_size(ext: &[u8]) -> usize {
|
||||||
debug_assert!(ext.len() >= 9);
|
debug_assert!(ext.len() >= 9);
|
||||||
let raw = ((ext[6] as usize & 0x1F) << 11)
|
let raw =
|
||||||
| ((ext[7] as usize) << 3)
|
((ext[6] as usize & 0x1F) << 11) | ((ext[7] as usize) << 3) | ((ext[8] as usize) >> 5);
|
||||||
| ((ext[8] as usize) >> 5);
|
|
||||||
raw + 1
|
raw + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-9
@@ -115,7 +115,10 @@ mod tests {
|
|||||||
let frames = parser.parse(&pes);
|
let frames = parser.parse(&pes);
|
||||||
|
|
||||||
assert_eq!(frames.len(), 1);
|
assert_eq!(frames.len(), 1);
|
||||||
assert_eq!(frames[0].data, sub_data, "VobSub data should pass through unmodified");
|
assert_eq!(
|
||||||
|
frames[0].data, sub_data,
|
||||||
|
"VobSub data should pass through unmodified"
|
||||||
|
);
|
||||||
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
assert_eq!(frames[0].pts_ns, 1_000_000_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +130,10 @@ mod tests {
|
|||||||
let pes = make_pes(data, Some(90000 * i as i64));
|
let pes = make_pes(data, Some(90000 * i as i64));
|
||||||
let frames = parser.parse(&pes);
|
let frames = parser.parse(&pes);
|
||||||
assert_eq!(frames.len(), 1);
|
assert_eq!(frames.len(), 1);
|
||||||
assert!(frames[0].keyframe, "DVD subtitle frames should always be keyframes");
|
assert!(
|
||||||
|
frames[0].keyframe,
|
||||||
|
"DVD subtitle frames should always be keyframes"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,24 +230,37 @@ mod tests {
|
|||||||
];
|
];
|
||||||
let result = format_palette(&palette);
|
let result = format_palette(&palette);
|
||||||
let text = String::from_utf8(result).unwrap();
|
let text = String::from_utf8(result).unwrap();
|
||||||
assert!(text.starts_with("palette: "), "should start with 'palette: '");
|
assert!(
|
||||||
|
text.starts_with("palette: "),
|
||||||
|
"should start with 'palette: '"
|
||||||
|
);
|
||||||
assert!(text.ends_with('\n'), "should end with newline");
|
assert!(text.ends_with('\n'), "should end with newline");
|
||||||
// First color: 000000
|
// First color: 000000
|
||||||
assert!(text.contains("000000"), "black should be 000000, got: {}", text);
|
assert!(
|
||||||
|
text.contains("000000"),
|
||||||
|
"black should be 000000, got: {}",
|
||||||
|
text
|
||||||
|
);
|
||||||
// Second color: ffffff
|
// Second color: ffffff
|
||||||
assert!(text.contains("ffffff"), "white should be ffffff, got: {}", text);
|
assert!(
|
||||||
|
text.contains("ffffff"),
|
||||||
|
"white should be ffffff, got: {}",
|
||||||
|
text
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_palette_16_colors() {
|
fn format_palette_16_colors() {
|
||||||
let palette: Vec<[u8; 4]> = (0..16)
|
let palette: Vec<[u8; 4]> = (0..16).map(|i| [0x00, (i * 16) as u8, 128, 128]).collect();
|
||||||
.map(|i| [0x00, (i * 16) as u8, 128, 128])
|
|
||||||
.collect();
|
|
||||||
let result = format_palette(&palette);
|
let result = format_palette(&palette);
|
||||||
let text = String::from_utf8(result).unwrap();
|
let text = String::from_utf8(result).unwrap();
|
||||||
// Should have exactly 15 commas (16 colors separated by ", ")
|
// Should have exactly 15 commas (16 colors separated by ", ")
|
||||||
let comma_count = text.matches(", ").count();
|
let comma_count = text.matches(", ").count();
|
||||||
assert_eq!(comma_count, 15, "16 colors should have 15 separators, got {}", comma_count);
|
assert_eq!(
|
||||||
|
comma_count, 15,
|
||||||
|
"16 colors should have 15 separators, got {}",
|
||||||
|
comma_count
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -525,7 +525,9 @@ mod tests {
|
|||||||
let mut nal_types = Vec::new();
|
let mut nal_types = Vec::new();
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
while offset + 4 <= fd.len() {
|
while offset + 4 <= fd.len() {
|
||||||
let length = u32::from_be_bytes([fd[offset], fd[offset + 1], fd[offset + 2], fd[offset + 3]]) as usize;
|
let length =
|
||||||
|
u32::from_be_bytes([fd[offset], fd[offset + 1], fd[offset + 2], fd[offset + 3]])
|
||||||
|
as usize;
|
||||||
offset += 4;
|
offset += 4;
|
||||||
assert!(offset + length <= fd.len(), "NAL length exceeds frame data");
|
assert!(offset + length <= fd.len(), "NAL length exceeds frame data");
|
||||||
let nal_type = (fd[offset] >> 1) & 0x3F;
|
let nal_type = (fd[offset] >> 1) & 0x3F;
|
||||||
@@ -553,7 +555,9 @@ mod tests {
|
|||||||
// Verify RPU payload is intact
|
// Verify RPU payload is intact
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
while offset + 4 <= fd.len() {
|
while offset + 4 <= fd.len() {
|
||||||
let length = u32::from_be_bytes([fd[offset], fd[offset + 1], fd[offset + 2], fd[offset + 3]]) as usize;
|
let length =
|
||||||
|
u32::from_be_bytes([fd[offset], fd[offset + 1], fd[offset + 2], fd[offset + 3]])
|
||||||
|
as usize;
|
||||||
offset += 4;
|
offset += 4;
|
||||||
let nal_type = (fd[offset] >> 1) & 0x3F;
|
let nal_type = (fd[offset] >> 1) & 0x3F;
|
||||||
if nal_type == 62 {
|
if nal_type == 62 {
|
||||||
|
|||||||
@@ -114,8 +114,7 @@ impl CodecParser for Mpeg2Parser {
|
|||||||
|
|
||||||
// Check if sequence extension follows immediately.
|
// Check if sequence extension follows immediately.
|
||||||
if hdr_end + 3 < data.len() && data[hdr_end + 3] == SEQ_EXT_CODE {
|
if hdr_end + 3 < data.len() && data[hdr_end + 3] == SEQ_EXT_CODE {
|
||||||
let ext_end =
|
let ext_end = find_start_code(data, hdr_end + 4).unwrap_or(data.len());
|
||||||
find_start_code(data, hdr_end + 4).unwrap_or(data.len());
|
|
||||||
seq_data.extend_from_slice(&data[hdr_end..ext_end]);
|
seq_data.extend_from_slice(&data[hdr_end..ext_end]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +343,10 @@ mod tests {
|
|||||||
let _frames = parser.parse(&pes);
|
let _frames = parser.parse(&pes);
|
||||||
|
|
||||||
let cp = parser.codec_private();
|
let cp = parser.codec_private();
|
||||||
assert!(cp.is_some(), "codec_private should be available after sequence header");
|
assert!(
|
||||||
|
cp.is_some(),
|
||||||
|
"codec_private should be available after sequence header"
|
||||||
|
);
|
||||||
let cp = cp.unwrap();
|
let cp = cp.unwrap();
|
||||||
// Should start with the sequence header start code.
|
// Should start with the sequence header start code.
|
||||||
assert_eq!(&cp[..4], &[0x00, 0x00, 0x01, SEQ_HEADER_CODE]);
|
assert_eq!(&cp[..4], &[0x00, 0x00, 0x01, SEQ_HEADER_CODE]);
|
||||||
|
|||||||
+369
-34
@@ -2,14 +2,30 @@
|
|||||||
//!
|
//!
|
||||||
//! Read-only stream. Wraps DriveSession + Disc.
|
//! Read-only stream. Wraps DriveSession + Disc.
|
||||||
//! Handles drive init, AACS decryption, and sector reading.
|
//! Handles drive init, AACS decryption, and sector reading.
|
||||||
|
//!
|
||||||
|
//! Reading state (extent index, offset, batch size, error recovery) is stored
|
||||||
|
//! directly on the struct so that successive `read()` calls advance through
|
||||||
|
//! the disc instead of restarting from byte 0.
|
||||||
|
|
||||||
use super::IOStream;
|
use super::IOStream;
|
||||||
use crate::disc::{Disc, DiscTitle};
|
use crate::disc::{
|
||||||
|
ContentFormat, Disc, DiscTitle, Extent,
|
||||||
|
DEFAULT_BATCH_SECTORS, MIN_BATCH_SECTORS, RAMP_BATCH_AFTER, RAMP_SPEED_AFTER,
|
||||||
|
SLOW_SPEED_AFTER, detect_max_batch_sectors,
|
||||||
|
};
|
||||||
use crate::drive::DriveSession;
|
use crate::drive::DriveSession;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
use crate::speed::DriveSpeed;
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// AACS decryption parameters needed at read time.
|
||||||
|
/// Extracted from `AacsState` so we don't need `Clone` on the full struct.
|
||||||
|
struct AacsDecrypt {
|
||||||
|
unit_keys: Vec<(u32, [u8; 16])>,
|
||||||
|
read_data_key: Option<[u8; 16]>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Options for opening a disc stream.
|
/// Options for opening a disc stream.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct DiscOptions {
|
pub struct DiscOptions {
|
||||||
@@ -21,18 +37,38 @@ pub struct DiscOptions {
|
|||||||
pub title_index: Option<usize>,
|
pub title_index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Optical disc stream. Read-only — yields decrypted BD-TS bytes.
|
/// Optical disc stream. Read-only — yields decrypted BD-TS bytes.
|
||||||
|
///
|
||||||
|
/// Embeds the reading state that `ContentReader` would normally hold, so that
|
||||||
|
/// successive `read()` calls advance through the disc correctly.
|
||||||
pub struct DiscStream {
|
pub struct DiscStream {
|
||||||
disc_title: DiscTitle,
|
disc_title: DiscTitle,
|
||||||
disc: Disc,
|
disc: Disc,
|
||||||
session: DriveSession,
|
session: DriveSession,
|
||||||
title_index: usize,
|
// Read buffer: holds one decoded batch
|
||||||
// Read buffer: holds one batch from ContentReader
|
|
||||||
batch_buf: Vec<u8>,
|
batch_buf: Vec<u8>,
|
||||||
batch_pos: usize,
|
batch_pos: usize,
|
||||||
started: bool,
|
|
||||||
eof: bool,
|
eof: bool,
|
||||||
|
|
||||||
|
// ── Reading state (replaces ContentReader) ──
|
||||||
|
extents: Vec<Extent>,
|
||||||
|
current_extent: usize,
|
||||||
|
current_offset: u32,
|
||||||
|
content_format: ContentFormat,
|
||||||
|
aacs: Option<AacsDecrypt>,
|
||||||
|
css: Option<crate::css::CssState>,
|
||||||
|
unit_key_idx: usize,
|
||||||
|
read_buf: Vec<u8>,
|
||||||
|
/// Current batch size in sectors (adapts on errors)
|
||||||
|
batch_sectors: u16,
|
||||||
|
/// Maximum batch size detected from kernel limits
|
||||||
|
max_batch_sectors: u16,
|
||||||
|
/// Consecutive successful batch reads
|
||||||
|
ok_streak: u32,
|
||||||
|
/// Consecutive errors at current position
|
||||||
|
error_streak: u32,
|
||||||
|
/// Total read errors encountered
|
||||||
|
pub errors: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiscStream {
|
impl DiscStream {
|
||||||
@@ -64,16 +100,36 @@ impl DiscStream {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
let disc_title = disc.titles[title_index].clone();
|
let disc_title = disc.titles[title_index].clone();
|
||||||
|
let extents = disc_title.extents.clone();
|
||||||
|
let content_format = disc_title.content_format;
|
||||||
|
let aacs = disc.aacs.as_ref().map(|a| AacsDecrypt {
|
||||||
|
unit_keys: a.unit_keys.clone(),
|
||||||
|
read_data_key: a.read_data_key,
|
||||||
|
});
|
||||||
|
let css = disc.css.clone();
|
||||||
|
|
||||||
|
let max_batch = detect_max_batch_sectors(session.device_path());
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
disc_title,
|
disc_title,
|
||||||
disc,
|
disc,
|
||||||
session,
|
session,
|
||||||
title_index,
|
|
||||||
batch_buf: Vec::new(),
|
batch_buf: Vec::new(),
|
||||||
batch_pos: 0,
|
batch_pos: 0,
|
||||||
started: false,
|
|
||||||
eof: false,
|
eof: false,
|
||||||
|
extents,
|
||||||
|
current_extent: 0,
|
||||||
|
current_offset: 0,
|
||||||
|
content_format,
|
||||||
|
aacs,
|
||||||
|
css,
|
||||||
|
unit_key_idx: 0,
|
||||||
|
read_buf: Vec::with_capacity(max_batch as usize * 2048),
|
||||||
|
batch_sectors: max_batch,
|
||||||
|
max_batch_sectors: max_batch,
|
||||||
|
ok_streak: 0,
|
||||||
|
error_streak: 0,
|
||||||
|
errors: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +137,155 @@ impl DiscStream {
|
|||||||
pub fn disc(&self) -> &Disc {
|
pub fn disc(&self) -> &Disc {
|
||||||
&self.disc
|
&self.disc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read sectors from the drive into `self.read_buf`.
|
||||||
|
fn read_sectors(&mut self, lba: u32, count: u16) -> Result<(), Error> {
|
||||||
|
self.session.read_content(lba, count, &mut self.read_buf)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill the internal read buffer with the next batch of sectors,
|
||||||
|
/// handling error recovery (halve batch, slow drive, retry, skip).
|
||||||
|
///
|
||||||
|
/// Returns `true` if data was read, `false` at end-of-title.
|
||||||
|
fn fill_buffer(&mut self) -> Result<bool, Error> {
|
||||||
|
loop {
|
||||||
|
if self.current_extent >= self.extents.len() {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ext_start = self.extents[self.current_extent].start_lba;
|
||||||
|
let ext_sectors = self.extents[self.current_extent].sector_count;
|
||||||
|
let remaining = ext_sectors.saturating_sub(self.current_offset);
|
||||||
|
|
||||||
|
// Align to 3 sectors (one aligned unit)
|
||||||
|
let sectors_to_read = remaining.min(self.batch_sectors as u32) as u16;
|
||||||
|
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
|
||||||
|
if sectors_to_read == 0 {
|
||||||
|
self.current_extent += 1;
|
||||||
|
self.current_offset = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lba = ext_start + self.current_offset;
|
||||||
|
let byte_count = sectors_to_read as usize * 2048;
|
||||||
|
self.read_buf.resize(byte_count, 0);
|
||||||
|
|
||||||
|
match self.read_sectors(lba, sectors_to_read) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.current_offset += sectors_to_read as u32;
|
||||||
|
self.error_streak = 0;
|
||||||
|
|
||||||
|
if self.current_offset >= ext_sectors {
|
||||||
|
self.current_extent += 1;
|
||||||
|
self.current_offset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ramp up batch size after consecutive successes
|
||||||
|
self.ok_streak += 1;
|
||||||
|
if self.batch_sectors < self.max_batch_sectors
|
||||||
|
&& self.ok_streak >= RAMP_BATCH_AFTER
|
||||||
|
{
|
||||||
|
self.batch_sectors =
|
||||||
|
(self.batch_sectors * 2).min(self.max_batch_sectors);
|
||||||
|
self.ok_streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore max speed after sustained success at full batch
|
||||||
|
if self.batch_sectors == self.max_batch_sectors
|
||||||
|
&& self.ok_streak >= RAMP_SPEED_AFTER
|
||||||
|
{
|
||||||
|
self.session.set_speed(0xFFFF);
|
||||||
|
self.ok_streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
self.errors += 1;
|
||||||
|
self.error_streak += 1;
|
||||||
|
self.ok_streak = 0;
|
||||||
|
|
||||||
|
// First error: re-init (drive may have re-locked)
|
||||||
|
if self.error_streak == 1 {
|
||||||
|
let _ = self.session.init();
|
||||||
|
let _ = self.session.probe_disc();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeated errors: slow down
|
||||||
|
if self.error_streak >= SLOW_SPEED_AFTER {
|
||||||
|
self.session.set_speed(DriveSpeed::BD2x.to_kbps());
|
||||||
|
self.error_streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.batch_sectors > MIN_BATCH_SECTORS {
|
||||||
|
self.batch_sectors =
|
||||||
|
(self.batch_sectors / 2).max(MIN_BATCH_SECTORS);
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||||
|
} else {
|
||||||
|
// At minimum batch -- retry once with longer pause
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
|
self.read_buf
|
||||||
|
.resize(MIN_BATCH_SECTORS as usize * 2048, 0);
|
||||||
|
if self.read_sectors(lba, MIN_BATCH_SECTORS).is_ok() {
|
||||||
|
self.error_streak = 0;
|
||||||
|
self.current_offset += MIN_BATCH_SECTORS as u32;
|
||||||
|
if self.current_offset >= ext_sectors {
|
||||||
|
self.current_extent += 1;
|
||||||
|
self.current_offset = 0;
|
||||||
|
}
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
// Still failing -- skip this unit (zero-fill)
|
||||||
|
self.current_offset += 3;
|
||||||
|
if self.current_offset >= ext_sectors {
|
||||||
|
self.current_extent += 1;
|
||||||
|
self.current_offset = 0;
|
||||||
|
}
|
||||||
|
self.read_buf
|
||||||
|
.resize(crate::aacs::ALIGNED_UNIT_LEN, 0);
|
||||||
|
self.read_buf.fill(0);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrypt the contents of `self.read_buf` in-place and copy the
|
||||||
|
/// decrypted data into `self.batch_buf`.
|
||||||
|
fn decrypt_and_buffer(&mut self) {
|
||||||
|
let unit_len = crate::aacs::ALIGNED_UNIT_LEN;
|
||||||
|
let total_bytes = self.read_buf.len();
|
||||||
|
|
||||||
|
if let Some(ref aacs) = self.aacs {
|
||||||
|
let uk = aacs
|
||||||
|
.unit_keys
|
||||||
|
.get(self.unit_key_idx)
|
||||||
|
.map(|(_, k)| *k)
|
||||||
|
.unwrap_or([0u8; 16]);
|
||||||
|
let rdk = aacs.read_data_key.as_ref();
|
||||||
|
|
||||||
|
let num_units = total_bytes / unit_len;
|
||||||
|
for i in 0..num_units {
|
||||||
|
let start = i * unit_len;
|
||||||
|
let end = start + unit_len;
|
||||||
|
let unit = &mut self.read_buf[start..end];
|
||||||
|
if crate::aacs::is_unit_encrypted(unit) {
|
||||||
|
crate::aacs::decrypt_unit_full(unit, &uk, rdk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if let Some(ref css) = self.css {
|
||||||
|
for chunk in self.read_buf[..total_bytes].chunks_mut(2048) {
|
||||||
|
crate::css::lfsr::descramble_sector(&css.title_key, chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No encryption: read_buf is already plaintext
|
||||||
|
|
||||||
|
self.batch_buf.clear();
|
||||||
|
self.batch_buf.extend_from_slice(&self.read_buf[..total_bytes]);
|
||||||
|
self.batch_pos = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IOStream for DiscStream {
|
impl IOStream for DiscStream {
|
||||||
@@ -109,37 +314,24 @@ impl Read for DiscStream {
|
|||||||
return Ok(0);
|
return Ok(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open reader on first call
|
// Fill the read buffer with the next batch of sectors
|
||||||
if !self.started {
|
let has_data = self
|
||||||
self.started = true;
|
.fill_buffer()
|
||||||
}
|
|
||||||
|
|
||||||
// Read next batch via a temporary ContentReader
|
|
||||||
// ContentReader borrows session and disc, so we create it inline
|
|
||||||
let mut reader = self
|
|
||||||
.disc
|
|
||||||
.open_title(&mut self.session, self.title_index)
|
|
||||||
.map_err(|e| io::Error::other(e.to_string()))?;
|
.map_err(|e| io::Error::other(e.to_string()))?;
|
||||||
|
|
||||||
match reader.read_batch() {
|
if !has_data {
|
||||||
Ok(Some(batch)) => {
|
|
||||||
let n = batch.len().min(buf.len());
|
|
||||||
buf[..n].copy_from_slice(&batch[..n]);
|
|
||||||
if batch.len() > n {
|
|
||||||
self.batch_buf = batch.to_vec();
|
|
||||||
self.batch_pos = n;
|
|
||||||
} else {
|
|
||||||
self.batch_buf.clear();
|
|
||||||
self.batch_pos = 0;
|
|
||||||
}
|
|
||||||
Ok(n)
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
self.eof = true;
|
self.eof = true;
|
||||||
Ok(0)
|
return Ok(0);
|
||||||
}
|
|
||||||
Err(e) => Err(io::Error::other(e.to_string())),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Decrypt in-place and move to batch_buf
|
||||||
|
self.decrypt_and_buffer();
|
||||||
|
|
||||||
|
// Now drain into the caller's buffer
|
||||||
|
let n = self.batch_buf.len().min(buf.len());
|
||||||
|
buf[..n].copy_from_slice(&self.batch_buf[..n]);
|
||||||
|
self.batch_pos = n;
|
||||||
|
Ok(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,3 +346,146 @@ impl Write for DiscStream {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::disc::{ContentFormat, DiscTitle, Extent};
|
||||||
|
|
||||||
|
/// Build a minimal DiscStream with fake extents for testing state advancement.
|
||||||
|
/// We cannot call `DiscStream::open()` without a real drive, so we construct
|
||||||
|
/// one manually and then call the internal `fill_buffer` / `read` path
|
||||||
|
/// through a helper that simulates the session reads.
|
||||||
|
///
|
||||||
|
/// Instead we test the state-machine logic directly: given a set of extents
|
||||||
|
/// and a current_extent/current_offset, verify that repeated reads advance
|
||||||
|
/// through the extents correctly.
|
||||||
|
#[test]
|
||||||
|
fn state_advances_across_extents() {
|
||||||
|
// Simulate two extents of 6 sectors each (2 aligned units each).
|
||||||
|
let extents = vec![
|
||||||
|
Extent {
|
||||||
|
start_lba: 100,
|
||||||
|
sector_count: 6,
|
||||||
|
},
|
||||||
|
Extent {
|
||||||
|
start_lba: 200,
|
||||||
|
sector_count: 6,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Walk through the extents manually using the same arithmetic
|
||||||
|
// that fill_buffer uses, and verify we visit every sector.
|
||||||
|
let batch_sectors: u16 = 6;
|
||||||
|
let mut current_extent: usize = 0;
|
||||||
|
let mut current_offset: u32 = 0;
|
||||||
|
let mut lbas_read = Vec::new();
|
||||||
|
|
||||||
|
while current_extent < extents.len() {
|
||||||
|
let ext_start = extents[current_extent].start_lba;
|
||||||
|
let ext_sectors = extents[current_extent].sector_count;
|
||||||
|
let remaining = ext_sectors.saturating_sub(current_offset);
|
||||||
|
let sectors_to_read = remaining.min(batch_sectors as u32) as u16;
|
||||||
|
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
|
||||||
|
if sectors_to_read == 0 {
|
||||||
|
current_extent += 1;
|
||||||
|
current_offset = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lba = ext_start + current_offset;
|
||||||
|
lbas_read.push((lba, sectors_to_read));
|
||||||
|
current_offset += sectors_to_read as u32;
|
||||||
|
if current_offset >= ext_sectors {
|
||||||
|
current_extent += 1;
|
||||||
|
current_offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(lbas_read.len(), 2, "should read two batches");
|
||||||
|
assert_eq!(lbas_read[0], (100, 6), "first batch starts at LBA 100");
|
||||||
|
assert_eq!(lbas_read[1], (200, 6), "second batch starts at LBA 200");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify that small extents that are not aligned to 3 sectors are skipped
|
||||||
|
/// (moved past) rather than causing an infinite loop.
|
||||||
|
#[test]
|
||||||
|
fn unaligned_extent_is_skipped() {
|
||||||
|
let extents = vec![
|
||||||
|
Extent {
|
||||||
|
start_lba: 50,
|
||||||
|
sector_count: 2, // < 3, cannot form an aligned unit
|
||||||
|
},
|
||||||
|
Extent {
|
||||||
|
start_lba: 300,
|
||||||
|
sector_count: 9,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let batch_sectors: u16 = 9;
|
||||||
|
let mut current_extent: usize = 0;
|
||||||
|
let mut current_offset: u32 = 0;
|
||||||
|
let mut lbas_read = Vec::new();
|
||||||
|
|
||||||
|
while current_extent < extents.len() {
|
||||||
|
let ext_start = extents[current_extent].start_lba;
|
||||||
|
let ext_sectors = extents[current_extent].sector_count;
|
||||||
|
let remaining = ext_sectors.saturating_sub(current_offset);
|
||||||
|
let sectors_to_read = remaining.min(batch_sectors as u32) as u16;
|
||||||
|
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
|
||||||
|
if sectors_to_read == 0 {
|
||||||
|
current_extent += 1;
|
||||||
|
current_offset = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lba = ext_start + current_offset;
|
||||||
|
lbas_read.push((lba, sectors_to_read));
|
||||||
|
current_offset += sectors_to_read as u32;
|
||||||
|
if current_offset >= ext_sectors {
|
||||||
|
current_extent += 1;
|
||||||
|
current_offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(lbas_read.len(), 1, "only second extent is readable");
|
||||||
|
assert_eq!(lbas_read[0], (300, 9));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify that multiple reads from the same extent produce advancing offsets.
|
||||||
|
#[test]
|
||||||
|
fn multiple_batches_within_one_extent() {
|
||||||
|
let extents = vec![Extent {
|
||||||
|
start_lba: 1000,
|
||||||
|
sector_count: 18, // 6 aligned units = 3 batches of 6 sectors
|
||||||
|
}];
|
||||||
|
|
||||||
|
let batch_sectors: u16 = 6;
|
||||||
|
let mut current_extent: usize = 0;
|
||||||
|
let mut current_offset: u32 = 0;
|
||||||
|
let mut lbas_read = Vec::new();
|
||||||
|
|
||||||
|
while current_extent < extents.len() {
|
||||||
|
let ext_start = extents[current_extent].start_lba;
|
||||||
|
let ext_sectors = extents[current_extent].sector_count;
|
||||||
|
let remaining = ext_sectors.saturating_sub(current_offset);
|
||||||
|
let sectors_to_read = remaining.min(batch_sectors as u32) as u16;
|
||||||
|
let sectors_to_read = sectors_to_read - (sectors_to_read % 3);
|
||||||
|
if sectors_to_read == 0 {
|
||||||
|
current_extent += 1;
|
||||||
|
current_offset = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lba = ext_start + current_offset;
|
||||||
|
lbas_read.push((lba, sectors_to_read));
|
||||||
|
current_offset += sectors_to_read as u32;
|
||||||
|
if current_offset >= ext_sectors {
|
||||||
|
current_extent += 1;
|
||||||
|
current_offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(lbas_read.len(), 3, "three batches from one extent");
|
||||||
|
assert_eq!(lbas_read[0], (1000, 6));
|
||||||
|
assert_eq!(lbas_read[1], (1006, 6));
|
||||||
|
assert_eq!(lbas_read[2], (1012, 6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+19
-12
@@ -394,7 +394,12 @@ impl<W: Write + Seek> IsoWriter<W> {
|
|||||||
// Allocation: data starts at DATA_START in the physical partition
|
// Allocation: data starts at DATA_START in the physical partition
|
||||||
let data_offset = self.data_start_sector - PARTITION_START;
|
let data_offset = self.data_start_sector - PARTITION_START;
|
||||||
// Cap allocation length at u32::MAX for files >4GB (UDF short_ad limitation)
|
// Cap allocation length at u32::MAX for files >4GB (UDF short_ad limitation)
|
||||||
let ad_len = if file_size > u32::MAX as u64 { u32::MAX } else { file_size as u32 };
|
// TODO: long_ad support is needed for full BD ISO support (files >4GB)
|
||||||
|
let ad_len = if file_size > u32::MAX as u64 {
|
||||||
|
u32::MAX
|
||||||
|
} else {
|
||||||
|
file_size as u32
|
||||||
|
};
|
||||||
icb[216..220].copy_from_slice(&ad_len.to_le_bytes());
|
icb[216..220].copy_from_slice(&ad_len.to_le_bytes());
|
||||||
icb[220..224].copy_from_slice(&data_offset.to_le_bytes());
|
icb[220..224].copy_from_slice(&data_offset.to_le_bytes());
|
||||||
self.writer.write_all(&icb)?;
|
self.writer.write_all(&icb)?;
|
||||||
@@ -409,14 +414,18 @@ fn write_descriptor_tag(buf: &mut [u8], tag_id: u16, sector: u32) {
|
|||||||
buf[0..2].copy_from_slice(&tag_id.to_le_bytes());
|
buf[0..2].copy_from_slice(&tag_id.to_le_bytes());
|
||||||
// Descriptor version: 3 (UDF 2.50)
|
// Descriptor version: 3 (UDF 2.50)
|
||||||
buf[2..4].copy_from_slice(&3u16.to_le_bytes());
|
buf[2..4].copy_from_slice(&3u16.to_le_bytes());
|
||||||
// Tag serial number
|
|
||||||
buf[4] = 0;
|
|
||||||
// Descriptor CRC (simplified — set to 0, most implementations accept this)
|
// Descriptor CRC (simplified — set to 0, most implementations accept this)
|
||||||
buf[8..10].copy_from_slice(&0u16.to_le_bytes());
|
buf[8..10].copy_from_slice(&0u16.to_le_bytes());
|
||||||
// Descriptor CRC length
|
// Descriptor CRC length
|
||||||
buf[10..12].copy_from_slice(&0u16.to_le_bytes());
|
buf[10..12].copy_from_slice(&0u16.to_le_bytes());
|
||||||
// Tag location
|
// Tag location
|
||||||
buf[12..16].copy_from_slice(§or.to_le_bytes());
|
buf[12..16].copy_from_slice(§or.to_le_bytes());
|
||||||
|
// Compute tag checksum: sum of bytes 0-3, 5-15 mod 256
|
||||||
|
let checksum: u8 = buf[0..4]
|
||||||
|
.iter()
|
||||||
|
.chain(buf[5..16].iter())
|
||||||
|
.fold(0u8, |acc, &b| acc.wrapping_add(b));
|
||||||
|
buf[4] = checksum;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write a UDF d-string (compressed unicode string with length prefix).
|
/// Write a UDF d-string (compressed unicode string with length prefix).
|
||||||
@@ -520,11 +529,7 @@ mod tests {
|
|||||||
|
|
||||||
// VRS at sector 16 should contain "BEA01"
|
// VRS at sector 16 should contain "BEA01"
|
||||||
let vrs = sector(&data, VRS_START);
|
let vrs = sector(&data, VRS_START);
|
||||||
assert_eq!(
|
assert_eq!(&vrs[1..6], b"BEA01", "VRS sector 16 should contain BEA01");
|
||||||
&vrs[1..6],
|
|
||||||
b"BEA01",
|
|
||||||
"VRS sector 16 should contain BEA01"
|
|
||||||
);
|
|
||||||
|
|
||||||
// FSD at metadata sector should have tag ID = 256
|
// FSD at metadata sector should have tag ID = 256
|
||||||
let fsd = sector(&data, FSD_SECTOR);
|
let fsd = sector(&data, FSD_SECTOR);
|
||||||
@@ -578,9 +583,7 @@ mod tests {
|
|||||||
// The FID for the m2ts file should contain the filename after the parent entry.
|
// The FID for the m2ts file should contain the filename after the parent entry.
|
||||||
// Search for "00042.m2ts" in the sector data
|
// Search for "00042.m2ts" in the sector data
|
||||||
let name = b"00042.m2ts";
|
let name = b"00042.m2ts";
|
||||||
let found = stream_dir
|
let found = stream_dir.windows(name.len()).any(|w| w == name);
|
||||||
.windows(name.len())
|
|
||||||
.any(|w| w == name);
|
|
||||||
assert!(
|
assert!(
|
||||||
found,
|
found,
|
||||||
"STREAM directory should contain m2ts filename '00042.m2ts'"
|
"STREAM directory should contain m2ts filename '00042.m2ts'"
|
||||||
@@ -599,7 +602,11 @@ mod tests {
|
|||||||
// Should still have valid UDF structure
|
// Should still have valid UDF structure
|
||||||
// AVDP at sector 256
|
// AVDP at sector 256
|
||||||
let avdp = sector(&data, AVDP_SECTOR);
|
let avdp = sector(&data, AVDP_SECTOR);
|
||||||
assert_eq!(le_u16(avdp, 0), 2, "AVDP tag should be present even with no data");
|
assert_eq!(
|
||||||
|
le_u16(avdp, 0),
|
||||||
|
2,
|
||||||
|
"AVDP tag should be present even with no data"
|
||||||
|
);
|
||||||
|
|
||||||
// VRS
|
// VRS
|
||||||
let vrs = sector(&data, VRS_START);
|
let vrs = sector(&data, VRS_START);
|
||||||
|
|||||||
+1
-1
@@ -189,7 +189,7 @@ impl M2tsMeta {
|
|||||||
|
|
||||||
/// Write the metadata header to a writer. Padded to 192-byte boundary.
|
/// Write the metadata header to a writer. Padded to 192-byte boundary.
|
||||||
pub fn write_header(w: &mut impl Write, meta: &M2tsMeta) -> io::Result<()> {
|
pub fn write_header(w: &mut impl Write, meta: &M2tsMeta) -> io::Result<()> {
|
||||||
let json = serde_json::to_vec(meta).map_err(|e| io::Error::other(e))?;
|
let json = serde_json::to_vec(meta).map_err(io::Error::other)?;
|
||||||
|
|
||||||
let json_len = json.len() as u32;
|
let json_len = json.len() as u32;
|
||||||
let raw_len = 8 + 4 + json.len(); // magic + len + json
|
let raw_len = 8 + 4 + json.len(); // magic + len + json
|
||||||
|
|||||||
+24
-22
@@ -578,9 +578,7 @@ mod tests {
|
|||||||
let writer = SharedWriter(shared.clone());
|
let writer = SharedWriter(shared.clone());
|
||||||
let tracks = [make_video_track()];
|
let tracks = [make_video_track()];
|
||||||
let mut muxer = MkvMuxer::new(writer, &tracks, Some("Cue Test"), 60.0).unwrap();
|
let mut muxer = MkvMuxer::new(writer, &tracks, Some("Cue Test"), 60.0).unwrap();
|
||||||
muxer
|
muxer.write_frame(0, 0, true, &[0x01, 0x02, 0x03]).unwrap();
|
||||||
.write_frame(0, 0, true, &[0x01, 0x02, 0x03])
|
|
||||||
.unwrap();
|
|
||||||
muxer.finish().unwrap();
|
muxer.finish().unwrap();
|
||||||
|
|
||||||
let data = shared.lock().unwrap().clone().into_inner();
|
let data = shared.lock().unwrap().clone().into_inner();
|
||||||
@@ -596,12 +594,8 @@ mod tests {
|
|||||||
let tracks = [make_video_track(), make_audio_track()];
|
let tracks = [make_video_track(), make_audio_track()];
|
||||||
let mut muxer = MkvMuxer::new(buf, &tracks, Some("Multi"), 120.0).unwrap();
|
let mut muxer = MkvMuxer::new(buf, &tracks, Some("Multi"), 120.0).unwrap();
|
||||||
// Write frames to both tracks
|
// Write frames to both tracks
|
||||||
muxer
|
muxer.write_frame(0, 0, true, &[0x00, 0x00, 0x01]).unwrap();
|
||||||
.write_frame(0, 0, true, &[0x00, 0x00, 0x01])
|
muxer.write_frame(1, 0, false, &[0x0B, 0x77, 0x00]).unwrap();
|
||||||
.unwrap();
|
|
||||||
muxer
|
|
||||||
.write_frame(1, 0, false, &[0x0B, 0x77, 0x00])
|
|
||||||
.unwrap();
|
|
||||||
muxer
|
muxer
|
||||||
.write_frame(0, 40_000_000, false, &[0x00, 0x00, 0x01])
|
.write_frame(0, 40_000_000, false, &[0x00, 0x00, 0x01])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -621,14 +615,10 @@ mod tests {
|
|||||||
|
|
||||||
// Record position before first frame
|
// Record position before first frame
|
||||||
let pos_before_kf = muxer.writer.position();
|
let pos_before_kf = muxer.writer.position();
|
||||||
muxer
|
muxer.write_frame(0, 0, true, &[0xAA]).unwrap();
|
||||||
.write_frame(0, 0, true, &[0xAA])
|
|
||||||
.unwrap();
|
|
||||||
let pos_after_kf = muxer.writer.position();
|
let pos_after_kf = muxer.writer.position();
|
||||||
|
|
||||||
muxer
|
muxer.write_frame(0, 1_000_000, false, &[0xBB]).unwrap();
|
||||||
.write_frame(0, 1_000_000, false, &[0xBB])
|
|
||||||
.unwrap();
|
|
||||||
let pos_after_nkf = muxer.writer.position();
|
let pos_after_nkf = muxer.writer.position();
|
||||||
|
|
||||||
let data = muxer.writer.into_inner();
|
let data = muxer.writer.into_inner();
|
||||||
@@ -682,13 +672,22 @@ mod tests {
|
|||||||
let buf = Cursor::new(Vec::new());
|
let buf = Cursor::new(Vec::new());
|
||||||
let tracks = [make_video_track()];
|
let tracks = [make_video_track()];
|
||||||
let chapters = vec![
|
let chapters = vec![
|
||||||
Chapter { time_secs: 0.0, name: "Chapter 1".into() },
|
Chapter {
|
||||||
Chapter { time_secs: 300.0, name: "Chapter 2".into() },
|
time_secs: 0.0,
|
||||||
Chapter { time_secs: 600.0, name: "Chapter 3".into() },
|
name: "Chapter 1".into(),
|
||||||
|
},
|
||||||
|
Chapter {
|
||||||
|
time_secs: 300.0,
|
||||||
|
name: "Chapter 2".into(),
|
||||||
|
},
|
||||||
|
Chapter {
|
||||||
|
time_secs: 600.0,
|
||||||
|
name: "Chapter 3".into(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let muxer = MkvMuxer::new_with_chapters(
|
let muxer =
|
||||||
buf, &tracks, Some("Chapter Test"), 900.0, &chapters,
|
MkvMuxer::new_with_chapters(buf, &tracks, Some("Chapter Test"), 900.0, &chapters)
|
||||||
).unwrap();
|
.unwrap();
|
||||||
let data = muxer.writer.into_inner();
|
let data = muxer.writer.into_inner();
|
||||||
|
|
||||||
// Chapters element ID: 0x1043A770
|
// Chapters element ID: 0x1043A770
|
||||||
@@ -742,7 +741,10 @@ mod tests {
|
|||||||
let count = data.windows(1).filter(|w| w[0] == 0x88).count();
|
let count = data.windows(1).filter(|w| w[0] == 0x88).count();
|
||||||
// 0x88 appears as FlagDefault + as TrackType (also 0x83... no, 0x83 != 0x88)
|
// 0x88 appears as FlagDefault + as TrackType (also 0x83... no, 0x83 != 0x88)
|
||||||
// FlagDefault (0x88) should appear for the non-default track
|
// FlagDefault (0x88) should appear for the non-default track
|
||||||
assert!(count >= 1, "FlagDefault should be written for non-default tracks");
|
assert!(
|
||||||
|
count >= 1,
|
||||||
|
"FlagDefault should be written for non-default tracks"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -381,7 +381,10 @@ fn parse_mkv_header(r: &mut (impl Read + Seek)) -> io::Result<DiscTitle> {
|
|||||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "not EBML"));
|
return Err(io::Error::new(io::ErrorKind::InvalidData, "not EBML"));
|
||||||
}
|
}
|
||||||
if size > i64::MAX as u64 {
|
if size > i64::MAX as u64 {
|
||||||
return Err(io::Error::new(io::ErrorKind::InvalidData, "EBML header too large"));
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidData,
|
||||||
|
"EBML header too large",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
r.seek(SeekFrom::Current(size as i64))?;
|
r.seek(SeekFrom::Current(size as i64))?;
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -31,9 +31,9 @@ pub mod mkv;
|
|||||||
mod mkvstream;
|
mod mkvstream;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
pub mod null;
|
pub mod null;
|
||||||
|
pub mod ps;
|
||||||
pub mod resolve;
|
pub mod resolve;
|
||||||
pub mod stdio;
|
pub mod stdio;
|
||||||
pub mod ps;
|
|
||||||
pub mod ts;
|
pub mod ts;
|
||||||
|
|
||||||
pub use disc::{DiscOptions, DiscStream};
|
pub use disc::{DiscOptions, DiscStream};
|
||||||
|
|||||||
+15
-35
@@ -118,8 +118,8 @@ impl PsDemuxer {
|
|||||||
if sc + 6 > self.buffer.len() {
|
if sc + 6 > self.buffer.len() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let header_len = ((self.buffer[sc + 4] as usize) << 8)
|
let header_len =
|
||||||
| self.buffer[sc + 5] as usize;
|
((self.buffer[sc + 4] as usize) << 8) | self.buffer[sc + 5] as usize;
|
||||||
let total = 6 + header_len;
|
let total = 6 + header_len;
|
||||||
if sc + total > self.buffer.len() {
|
if sc + total > self.buffer.len() {
|
||||||
break;
|
break;
|
||||||
@@ -131,8 +131,8 @@ impl PsDemuxer {
|
|||||||
if sc + 6 > self.buffer.len() {
|
if sc + 6 > self.buffer.len() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let pes_packet_len = ((self.buffer[sc + 4] as usize) << 8)
|
let pes_packet_len =
|
||||||
| self.buffer[sc + 5] as usize;
|
((self.buffer[sc + 4] as usize) << 8) | self.buffer[sc + 5] as usize;
|
||||||
|
|
||||||
// Total bytes = 6 (start code + stream_id + length) + pes_packet_len.
|
// Total bytes = 6 (start code + stream_id + length) + pes_packet_len.
|
||||||
// A length of 0 means unbounded (video streams); in that case we need
|
// A length of 0 means unbounded (video streams); in that case we need
|
||||||
@@ -176,7 +176,7 @@ fn is_pes_stream_id(id: u8) -> bool {
|
|||||||
// Video: 0xE0-0xEF, MPEG audio: 0xC0-0xDF, private stream 1: 0xBD,
|
// Video: 0xE0-0xEF, MPEG audio: 0xC0-0xDF, private stream 1: 0xBD,
|
||||||
// private stream 2: 0xBF, padding: 0xBE, ECM/EMM etc.
|
// private stream 2: 0xBF, padding: 0xBE, ECM/EMM etc.
|
||||||
// We parse anything in the PES range.
|
// We parse anything in the PES range.
|
||||||
matches!(id, 0xBD | 0xBE | 0xBF | 0xC0..=0xEF)
|
matches!(id, 0xBD..=0xEF)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a single PES packet from a byte slice that starts at the start code.
|
/// Parse a single PES packet from a byte slice that starts at the start code.
|
||||||
@@ -267,11 +267,7 @@ fn parse_pts(buf: &[u8]) -> u64 {
|
|||||||
let b3 = buf[3] as u64;
|
let b3 = buf[3] as u64;
|
||||||
let b4 = buf[4] as u64;
|
let b4 = buf[4] as u64;
|
||||||
|
|
||||||
((b0 >> 1) & 0x07) << 30
|
((b0 >> 1) & 0x07) << 30 | b1 << 22 | (b2 >> 1) << 15 | b3 << 7 | b4 >> 1
|
||||||
| b1 << 22
|
|
||||||
| (b2 >> 1) << 15
|
|
||||||
| b3 << 7
|
|
||||||
| b4 >> 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the position of the next start code (00 00 01) at or after `from`.
|
/// Find the position of the next start code (00 00 01) at or after `from`.
|
||||||
@@ -325,9 +321,7 @@ mod tests {
|
|||||||
|
|
||||||
// Pack header with 3 stuffing bytes
|
// Pack header with 3 stuffing bytes
|
||||||
let mut data = vec![
|
let mut data = vec![
|
||||||
0x00, 0x00, 0x01, 0xBA,
|
0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x89, 0xC3,
|
||||||
0x44, 0x00, 0x04, 0x00, 0x04, 0x01,
|
|
||||||
0x01, 0x89, 0xC3,
|
|
||||||
0xFB, // stuffing_length = 3
|
0xFB, // stuffing_length = 3
|
||||||
0xFF, 0xFF, 0xFF, // stuffing bytes
|
0xFF, 0xFF, 0xFF, // stuffing bytes
|
||||||
];
|
];
|
||||||
@@ -394,8 +388,7 @@ mod tests {
|
|||||||
let dts_bytes = encode_pts(90000, 0x10); // DTS marker = 0x10
|
let dts_bytes = encode_pts(90000, 0x10); // DTS marker = 0x10
|
||||||
|
|
||||||
let mut data = vec![
|
let mut data = vec![
|
||||||
0x00, 0x00, 0x01, 0xE0,
|
0x00, 0x00, 0x01, 0xE0, 0x00, 0x11, // length = 17
|
||||||
0x00, 0x11, // length = 17
|
|
||||||
0x80, 0xC0, 0x0A, // flags: PTS+DTS, header_data_len=10
|
0x80, 0xC0, 0x0A, // flags: PTS+DTS, header_data_len=10
|
||||||
];
|
];
|
||||||
data.extend_from_slice(&pts_bytes);
|
data.extend_from_slice(&pts_bytes);
|
||||||
@@ -438,10 +431,8 @@ mod tests {
|
|||||||
let mut demuxer = PsDemuxer::new();
|
let mut demuxer = PsDemuxer::new();
|
||||||
|
|
||||||
let mut data = vec![
|
let mut data = vec![
|
||||||
0x00, 0x00, 0x01, 0xBD,
|
0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, // length = 6
|
||||||
0x00, 0x06, // length = 6
|
0x80, 0x00, 0x00, 0x88, // sub-stream ID: DTS stream 0
|
||||||
0x80, 0x00, 0x00,
|
|
||||||
0x88, // sub-stream ID: DTS stream 0
|
|
||||||
0x11, 0x22,
|
0x11, 0x22,
|
||||||
];
|
];
|
||||||
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
|
data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]);
|
||||||
@@ -456,9 +447,7 @@ mod tests {
|
|||||||
let mut demuxer = PsDemuxer::new();
|
let mut demuxer = PsDemuxer::new();
|
||||||
|
|
||||||
let mut data = vec![
|
let mut data = vec![
|
||||||
0x00, 0x00, 0x01, 0xBD,
|
0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, 0x80, 0x00, 0x00,
|
||||||
0x00, 0x06,
|
|
||||||
0x80, 0x00, 0x00,
|
|
||||||
0x20, // sub-stream ID: subtitle stream 0
|
0x20, // sub-stream ID: subtitle stream 0
|
||||||
0xFF, 0xFE,
|
0xFF, 0xFE,
|
||||||
];
|
];
|
||||||
@@ -474,9 +463,7 @@ mod tests {
|
|||||||
let mut demuxer = PsDemuxer::new();
|
let mut demuxer = PsDemuxer::new();
|
||||||
|
|
||||||
let mut data = vec![
|
let mut data = vec![
|
||||||
0x00, 0x00, 0x01, 0xBD,
|
0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, 0x80, 0x00, 0x00,
|
||||||
0x00, 0x06,
|
|
||||||
0x80, 0x00, 0x00,
|
|
||||||
0xA0, // sub-stream ID: LPCM stream 0
|
0xA0, // sub-stream ID: LPCM stream 0
|
||||||
0x01, 0x02,
|
0x01, 0x02,
|
||||||
];
|
];
|
||||||
@@ -494,8 +481,7 @@ mod tests {
|
|||||||
let mut demuxer = PsDemuxer::new();
|
let mut demuxer = PsDemuxer::new();
|
||||||
|
|
||||||
let mut full = vec![
|
let mut full = vec![
|
||||||
0x00, 0x00, 0x01, 0xE0,
|
0x00, 0x00, 0x01, 0xE0, 0x00, 0x06, // length = 6
|
||||||
0x00, 0x06, // length = 6
|
|
||||||
0x80, 0x00, 0x00, // no PTS, header_data_len=0
|
0x80, 0x00, 0x00, // no PTS, header_data_len=0
|
||||||
0xAA, 0xBB, 0xCC,
|
0xAA, 0xBB, 0xCC,
|
||||||
];
|
];
|
||||||
@@ -521,18 +507,12 @@ mod tests {
|
|||||||
|
|
||||||
// First PES: video
|
// First PES: video
|
||||||
data.extend_from_slice(&[
|
data.extend_from_slice(&[
|
||||||
0x00, 0x00, 0x01, 0xE0,
|
0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x11, 0x22,
|
||||||
0x00, 0x05,
|
|
||||||
0x80, 0x00, 0x00,
|
|
||||||
0x11, 0x22,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Second PES: audio
|
// Second PES: audio
|
||||||
data.extend_from_slice(&[
|
data.extend_from_slice(&[
|
||||||
0x00, 0x00, 0x01, 0xC0,
|
0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x33, 0x44,
|
||||||
0x00, 0x05,
|
|
||||||
0x80, 0x00, 0x00,
|
|
||||||
0x33, 0x44,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Delimiter
|
// Delimiter
|
||||||
|
|||||||
@@ -255,4 +255,3 @@ pub struct InputOptions {
|
|||||||
pub keydb_path: Option<String>,
|
pub keydb_path: Option<String>,
|
||||||
pub title_index: Option<usize>,
|
pub title_index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+18
-8
@@ -211,7 +211,7 @@ impl UdfFs {
|
|||||||
|
|
||||||
if let Ok((data_lba, data_len)) = self.read_icb_extent(reader, child.meta_lba) {
|
if let Ok((data_lba, data_len)) = self.read_icb_extent(reader, child.meta_lba) {
|
||||||
let abs_start = self.partition_start + data_lba;
|
let abs_start = self.partition_start + data_lba;
|
||||||
let sector_count = ((data_len as u64 + 2047) / 2048) as u32;
|
let sector_count = (data_len as u64).div_ceil(2048) as u32;
|
||||||
ranges.push((abs_start, sector_count));
|
ranges.push((abs_start, sector_count));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,7 +256,9 @@ impl UdfFs {
|
|||||||
let l_ad = u32::from_le_bytes([icb[212], icb[213], icb[214], icb[215]]) as usize;
|
let l_ad = u32::from_le_bytes([icb[212], icb[213], icb[214], icb[215]]) as usize;
|
||||||
let ad_offset = 216 + l_ea;
|
let ad_offset = 216 + l_ea;
|
||||||
if ad_offset + l_ad > icb.len() {
|
if ad_offset + l_ad > icb.len() {
|
||||||
return Err(Error::DiscRead { sector: self.meta_to_abs(meta_lba) as u64 });
|
return Err(Error::DiscRead {
|
||||||
|
sector: self.meta_to_abs(meta_lba) as u64,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
(ad_offset, l_ad)
|
(ad_offset, l_ad)
|
||||||
}
|
}
|
||||||
@@ -266,7 +268,9 @@ impl UdfFs {
|
|||||||
let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize;
|
let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize;
|
||||||
let ad_offset = 176 + l_ea;
|
let ad_offset = 176 + l_ea;
|
||||||
if ad_offset + l_ad > icb.len() {
|
if ad_offset + l_ad > icb.len() {
|
||||||
return Err(Error::DiscRead { sector: self.meta_to_abs(meta_lba) as u64 });
|
return Err(Error::DiscRead {
|
||||||
|
sector: self.meta_to_abs(meta_lba) as u64,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
(ad_offset, l_ad)
|
(ad_offset, l_ad)
|
||||||
}
|
}
|
||||||
@@ -442,7 +446,9 @@ pub fn read_filesystem(reader: &mut dyn SectorReader) -> Result<UdfFs> {
|
|||||||
]) as usize;
|
]) as usize;
|
||||||
let ad_off = 216 + l_ea;
|
let ad_off = 216 + l_ea;
|
||||||
if ad_off + 8 > meta_icb.len() {
|
if ad_off + 8 > meta_icb.len() {
|
||||||
return Err(Error::DiscRead { sector: meta_file_lba as u64 });
|
return Err(Error::DiscRead {
|
||||||
|
sector: meta_file_lba as u64,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let ad_len = u32::from_le_bytes([
|
let ad_len = u32::from_le_bytes([
|
||||||
meta_icb[ad_off],
|
meta_icb[ad_off],
|
||||||
@@ -491,7 +497,7 @@ pub fn read_filesystem(reader: &mut dyn SectorReader) -> Result<UdfFs> {
|
|||||||
// Step 5: Read root directory and build file tree
|
// Step 5: Read root directory and build file tree
|
||||||
let root = read_directory(reader, partition_start, metadata_start, root_lba, "", 0)?;
|
let root = read_directory(reader, partition_start, metadata_start, root_lba, "", 0)?;
|
||||||
|
|
||||||
let metadata_sectors = ((metadata_size_bytes as u64 + 2047) / 2048) as u32;
|
let metadata_sectors = (metadata_size_bytes as u64).div_ceil(2048) as u32;
|
||||||
|
|
||||||
Ok(UdfFs {
|
Ok(UdfFs {
|
||||||
root,
|
root,
|
||||||
@@ -527,7 +533,9 @@ fn read_directory(
|
|||||||
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
|
let l_ea = u32::from_le_bytes([icb[208], icb[209], icb[210], icb[211]]) as usize;
|
||||||
let ad_off = 216 + l_ea;
|
let ad_off = 216 + l_ea;
|
||||||
if ad_off + 8 > icb.len() {
|
if ad_off + 8 > icb.len() {
|
||||||
return Err(Error::DiscRead { sector: (meta_start + meta_lba) as u64 });
|
return Err(Error::DiscRead {
|
||||||
|
sector: (meta_start + meta_lba) as u64,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let len = u32::from_le_bytes([
|
let len = u32::from_le_bytes([
|
||||||
icb[ad_off],
|
icb[ad_off],
|
||||||
@@ -547,7 +555,9 @@ fn read_directory(
|
|||||||
let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
|
let l_ea = u32::from_le_bytes([icb[168], icb[169], icb[170], icb[171]]) as usize;
|
||||||
let ad_off = 176 + l_ea;
|
let ad_off = 176 + l_ea;
|
||||||
if ad_off + 8 > icb.len() {
|
if ad_off + 8 > icb.len() {
|
||||||
return Err(Error::DiscRead { sector: (meta_start + meta_lba) as u64 });
|
return Err(Error::DiscRead {
|
||||||
|
sector: (meta_start + meta_lba) as u64,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let len = u32::from_le_bytes([
|
let len = u32::from_le_bytes([
|
||||||
icb[ad_off],
|
icb[ad_off],
|
||||||
@@ -651,7 +661,7 @@ fn read_directory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Advance to next FID (4-byte aligned)
|
// Advance to next FID (4-byte aligned)
|
||||||
let fid_len = ((38 + l_iu + l_fi + 3) & !3);
|
let fid_len = (38 + l_iu + l_fi + 3) & !3;
|
||||||
pos += fid_len;
|
pos += fid_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+65
-22
@@ -40,7 +40,11 @@ fn css_descramble_sector_roundtrip_via_public_api() {
|
|||||||
// First descramble
|
// First descramble
|
||||||
css::descramble_sector(&state, &mut sector);
|
css::descramble_sector(&state, &mut sector);
|
||||||
assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared");
|
assert_eq!(sector[0x14] & 0x30, 0x00, "flag not cleared");
|
||||||
assert_ne!(§or[0x80..0x84], &original[0x80..0x84], "content unchanged");
|
assert_ne!(
|
||||||
|
§or[0x80..0x84],
|
||||||
|
&original[0x80..0x84],
|
||||||
|
"content unchanged"
|
||||||
|
);
|
||||||
|
|
||||||
// Restore flag for second pass
|
// Restore flag for second pass
|
||||||
sector[0x14] = 0x30;
|
sector[0x14] = 0x30;
|
||||||
@@ -58,7 +62,10 @@ fn css_descramble_sector_roundtrip_via_public_api() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn css_is_scrambled_detection() {
|
fn css_is_scrambled_detection() {
|
||||||
let mut sector = vec![0u8; 2048];
|
let mut sector = vec![0u8; 2048];
|
||||||
assert!(!css::is_scrambled(§or), "empty sector should not be scrambled");
|
assert!(
|
||||||
|
!css::is_scrambled(§or),
|
||||||
|
"empty sector should not be scrambled"
|
||||||
|
);
|
||||||
|
|
||||||
sector[0x14] = 0x10; // bit 4 set
|
sector[0x14] = 0x10; // bit 4 set
|
||||||
assert!(css::is_scrambled(§or), "bit 4 set should be detected");
|
assert!(css::is_scrambled(§or), "bit 4 set should be detected");
|
||||||
@@ -67,10 +74,16 @@ fn css_is_scrambled_detection() {
|
|||||||
assert!(css::is_scrambled(§or), "bit 5 set should be detected");
|
assert!(css::is_scrambled(§or), "bit 5 set should be detected");
|
||||||
|
|
||||||
sector[0x14] = 0x30; // both bits set
|
sector[0x14] = 0x30; // both bits set
|
||||||
assert!(css::is_scrambled(§or), "both bits set should be detected");
|
assert!(
|
||||||
|
css::is_scrambled(§or),
|
||||||
|
"both bits set should be detected"
|
||||||
|
);
|
||||||
|
|
||||||
sector[0x14] = 0xCF; // bits 4-5 clear, other bits set
|
sector[0x14] = 0xCF; // bits 4-5 clear, other bits set
|
||||||
assert!(!css::is_scrambled(§or), "bits 4-5 clear should not be scrambled");
|
assert!(
|
||||||
|
!css::is_scrambled(§or),
|
||||||
|
"bits 4-5 clear should not be scrambled"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── AACS Public API Tests ───────────────────────────────────────────────────
|
// ── AACS Public API Tests ───────────────────────────────────────────────────
|
||||||
@@ -87,8 +100,8 @@ fn aacs_decrypt_unit_roundtrip() {
|
|||||||
|
|
||||||
let unit_key = [0xAAu8; 16];
|
let unit_key = [0xAAu8; 16];
|
||||||
let aacs_iv: [u8; 16] = [
|
let aacs_iv: [u8; 16] = [
|
||||||
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05,
|
0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F,
|
||||||
0x0F, 0x78,
|
0x78,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Build plaintext unit with TS sync bytes every 192 bytes starting at offset 4
|
// Build plaintext unit with TS sync bytes every 192 bytes starting at offset 4
|
||||||
@@ -140,8 +153,14 @@ fn aacs_decrypt_unit_roundtrip() {
|
|||||||
|
|
||||||
// Now decrypt
|
// Now decrypt
|
||||||
let result = aacs::decrypt_unit(&mut plain, &unit_key);
|
let result = aacs::decrypt_unit(&mut plain, &unit_key);
|
||||||
assert!(result, "decrypt_unit should return true on valid encrypted unit");
|
assert!(
|
||||||
assert!(!aacs::is_unit_encrypted(&plain), "encryption flag should be cleared");
|
result,
|
||||||
|
"decrypt_unit should return true on valid encrypted unit"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!aacs::is_unit_encrypted(&plain),
|
||||||
|
"encryption flag should be cleared"
|
||||||
|
);
|
||||||
|
|
||||||
// Verify TS sync bytes at expected positions (flag byte is cleared by decrypt)
|
// Verify TS sync bytes at expected positions (flag byte is cleared by decrypt)
|
||||||
let mut sync_count = 0;
|
let mut sync_count = 0;
|
||||||
@@ -166,7 +185,11 @@ fn aacs_decrypt_unit_roundtrip() {
|
|||||||
"decrypted unit body does not match original"
|
"decrypted unit body does not match original"
|
||||||
);
|
);
|
||||||
// Byte 0: original had 0xC0 set, decrypted has it cleared
|
// Byte 0: original had 0xC0 set, decrypted has it cleared
|
||||||
assert_eq!(plain[0] & !0xC0, expected[0] & !0xC0, "byte 0 mismatch ignoring flag");
|
assert_eq!(
|
||||||
|
plain[0] & !0xC0,
|
||||||
|
expected[0] & !0xC0,
|
||||||
|
"byte 0 mismatch ignoring flag"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test 7: aacs_disc_hash_deterministic
|
/// Test 7: aacs_disc_hash_deterministic
|
||||||
@@ -182,7 +205,10 @@ fn aacs_disc_hash_deterministic() {
|
|||||||
assert_eq!(hash1a, hash1b, "disc_hash not deterministic on same input");
|
assert_eq!(hash1a, hash1b, "disc_hash not deterministic on same input");
|
||||||
|
|
||||||
let hash2 = aacs::disc_hash(data2);
|
let hash2 = aacs::disc_hash(data2);
|
||||||
assert_ne!(hash1a, hash2, "different inputs should produce different hashes");
|
assert_ne!(
|
||||||
|
hash1a, hash2,
|
||||||
|
"different inputs should produce different hashes"
|
||||||
|
);
|
||||||
|
|
||||||
// Verify it is a 20-byte SHA-1 hash
|
// Verify it is a 20-byte SHA-1 hash
|
||||||
assert_eq!(hash1a.len(), 20);
|
assert_eq!(hash1a.len(), 20);
|
||||||
@@ -190,7 +216,11 @@ fn aacs_disc_hash_deterministic() {
|
|||||||
// Verify disc_hash_hex formatting
|
// Verify disc_hash_hex formatting
|
||||||
let hex = aacs::disc_hash_hex(&hash1a);
|
let hex = aacs::disc_hash_hex(&hash1a);
|
||||||
assert!(hex.starts_with("0x"), "hex should start with 0x prefix");
|
assert!(hex.starts_with("0x"), "hex should start with 0x prefix");
|
||||||
assert_eq!(hex.len(), 42, "hex string should be 42 chars (0x + 40 hex digits)");
|
assert_eq!(
|
||||||
|
hex.len(),
|
||||||
|
42,
|
||||||
|
"hex string should be 42 chars (0x + 40 hex digits)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test: aacs_decrypt_unit_key_roundtrip
|
/// Test: aacs_decrypt_unit_key_roundtrip
|
||||||
@@ -203,12 +233,12 @@ fn aacs_decrypt_unit_key_roundtrip() {
|
|||||||
use aes::Aes128;
|
use aes::Aes128;
|
||||||
|
|
||||||
let vuk = [
|
let vuk = [
|
||||||
0x11u8, 0x14, 0x36, 0x0B, 0x10, 0xEE, 0x6E, 0xAC, 0x78, 0xAA, 0x4A, 0xC0, 0xB7, 0x52,
|
0x11u8, 0x14, 0x36, 0x0B, 0x10, 0xEE, 0x6E, 0xAC, 0x78, 0xAA, 0x4A, 0xC0, 0xB7, 0x52, 0xEA,
|
||||||
0xEA, 0xEB,
|
0xEB,
|
||||||
];
|
];
|
||||||
let original_unit_key = [
|
let original_unit_key = [
|
||||||
0x9E, 0x5D, 0x13, 0x10, 0x33, 0x74, 0x43, 0xE8, 0x11, 0xA5, 0x2E, 0xBB, 0xEA, 0xE0,
|
0x9E, 0x5D, 0x13, 0x10, 0x33, 0x74, 0x43, 0xE8, 0x11, 0xA5, 0x2E, 0xBB, 0xEA, 0xE0, 0x47,
|
||||||
0x47, 0x0F,
|
0x0F,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Encrypt: AES-ECB encrypt the unit key with VUK
|
// Encrypt: AES-ECB encrypt the unit key with VUK
|
||||||
@@ -231,10 +261,14 @@ fn aacs_decrypt_unit_key_roundtrip() {
|
|||||||
/// Verify derive_vuk: VUK = AES-ECB-DECRYPT(media_key, volume_id) XOR volume_id
|
/// Verify derive_vuk: VUK = AES-ECB-DECRYPT(media_key, volume_id) XOR volume_id
|
||||||
#[test]
|
#[test]
|
||||||
fn aacs_vuk_derivation_roundtrip() {
|
fn aacs_vuk_derivation_roundtrip() {
|
||||||
let media_key = [0x25u8, 0x2F, 0xB6, 0x36, 0xE8, 0x83, 0x52, 0x9E,
|
let media_key = [
|
||||||
0x11, 0x9A, 0xB7, 0x15, 0xF4, 0xEB, 0x16, 0x40];
|
0x25u8, 0x2F, 0xB6, 0x36, 0xE8, 0x83, 0x52, 0x9E, 0x11, 0x9A, 0xB7, 0x15, 0xF4, 0xEB, 0x16,
|
||||||
let volume_id = [0xA1u8, 0x3C, 0xBE, 0x2C, 0xE4, 0x05, 0x65, 0xD1,
|
0x40,
|
||||||
0x04, 0xB5, 0x3E, 0x76, 0x8C, 0x70, 0x0E, 0x30];
|
];
|
||||||
|
let volume_id = [
|
||||||
|
0xA1u8, 0x3C, 0xBE, 0x2C, 0xE4, 0x05, 0x65, 0xD1, 0x04, 0xB5, 0x3E, 0x76, 0x8C, 0x70, 0x0E,
|
||||||
|
0x30,
|
||||||
|
];
|
||||||
|
|
||||||
let vuk = aacs::derive_vuk(&media_key, &volume_id);
|
let vuk = aacs::derive_vuk(&media_key, &volume_id);
|
||||||
|
|
||||||
@@ -253,7 +287,10 @@ fn aacs_vuk_derivation_roundtrip() {
|
|||||||
fn aacs_is_unit_encrypted_detection() {
|
fn aacs_is_unit_encrypted_detection() {
|
||||||
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
let mut unit = vec![0u8; aacs::ALIGNED_UNIT_LEN];
|
||||||
|
|
||||||
assert!(!aacs::is_unit_encrypted(&unit), "zero unit should not be encrypted");
|
assert!(
|
||||||
|
!aacs::is_unit_encrypted(&unit),
|
||||||
|
"zero unit should not be encrypted"
|
||||||
|
);
|
||||||
|
|
||||||
unit[0] = 0x40; // bit 6 set
|
unit[0] = 0x40; // bit 6 set
|
||||||
assert!(aacs::is_unit_encrypted(&unit));
|
assert!(aacs::is_unit_encrypted(&unit));
|
||||||
@@ -269,7 +306,10 @@ fn aacs_is_unit_encrypted_detection() {
|
|||||||
|
|
||||||
// Too short
|
// Too short
|
||||||
let short = vec![0xC0u8; 100];
|
let short = vec![0xC0u8; 100];
|
||||||
assert!(!aacs::is_unit_encrypted(&short), "short buffer should not be detected");
|
assert!(
|
||||||
|
!aacs::is_unit_encrypted(&short),
|
||||||
|
"short buffer should not be detected"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test: aacs_decrypt_unit_unencrypted_passthrough
|
/// Test: aacs_decrypt_unit_unencrypted_passthrough
|
||||||
@@ -316,7 +356,10 @@ fn aacs_parse_unit_key_ro_minimal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let result = aacs::parse_unit_key_ro(&data, false);
|
let result = aacs::parse_unit_key_ro(&data, false);
|
||||||
assert!(result.is_some(), "parse_unit_key_ro should succeed on valid data");
|
assert!(
|
||||||
|
result.is_some(),
|
||||||
|
"parse_unit_key_ro should succeed on valid data"
|
||||||
|
);
|
||||||
|
|
||||||
let ukf = result.unwrap();
|
let ukf = result.unwrap();
|
||||||
assert_eq!(ukf.app_type, 1);
|
assert_eq!(ukf.app_type, 1);
|
||||||
|
|||||||
+10
-3
@@ -295,7 +295,9 @@ fn build_minimal_udf(reader: &mut MockSectorReader) {
|
|||||||
let partition_start: u32 = 512;
|
let partition_start: u32 = 512;
|
||||||
reader.sectors.insert(256, make_avdp_sector(32));
|
reader.sectors.insert(256, make_avdp_sector(32));
|
||||||
reader.sectors.insert(32, make_pvd_sector("TEST_DISC"));
|
reader.sectors.insert(32, make_pvd_sector("TEST_DISC"));
|
||||||
reader.sectors.insert(33, make_partition_desc(partition_start));
|
reader
|
||||||
|
.sectors
|
||||||
|
.insert(33, make_partition_desc(partition_start));
|
||||||
reader.sectors.insert(34, make_lvd_sector_simple());
|
reader.sectors.insert(34, make_lvd_sector_simple());
|
||||||
reader.sectors.insert(35, make_terminator());
|
reader.sectors.insert(35, make_terminator());
|
||||||
|
|
||||||
@@ -316,7 +318,9 @@ fn build_udf_with_aacs_dir(reader: &mut MockSectorReader) {
|
|||||||
let partition_start: u32 = 512;
|
let partition_start: u32 = 512;
|
||||||
reader.sectors.insert(256, make_avdp_sector(32));
|
reader.sectors.insert(256, make_avdp_sector(32));
|
||||||
reader.sectors.insert(32, make_pvd_sector("ENCRYPTED_DISC"));
|
reader.sectors.insert(32, make_pvd_sector("ENCRYPTED_DISC"));
|
||||||
reader.sectors.insert(33, make_partition_desc(partition_start));
|
reader
|
||||||
|
.sectors
|
||||||
|
.insert(33, make_partition_desc(partition_start));
|
||||||
reader.sectors.insert(34, make_lvd_sector_simple());
|
reader.sectors.insert(34, make_lvd_sector_simple());
|
||||||
reader.sectors.insert(35, make_terminator());
|
reader.sectors.insert(35, make_terminator());
|
||||||
|
|
||||||
@@ -357,7 +361,10 @@ fn resolve_encryption_no_aacs_dir() {
|
|||||||
let opts = ScanOptions::default();
|
let opts = ScanOptions::default();
|
||||||
let disc = Disc::scan_image(&mut reader, 1000, &opts).unwrap();
|
let disc = Disc::scan_image(&mut reader, 1000, &opts).unwrap();
|
||||||
|
|
||||||
assert!(!disc.encrypted, "disc without /AACS should not be encrypted");
|
assert!(
|
||||||
|
!disc.encrypted,
|
||||||
|
"disc without /AACS should not be encrypted"
|
||||||
|
);
|
||||||
assert!(disc.aacs.is_none(), "aacs should be None without /AACS dir");
|
assert!(disc.aacs.is_none(), "aacs should be None without /AACS dir");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user