diff --git a/src/aacs/keys.rs b/src/aacs/keys.rs index bbdec3c..0ce3a00 100644 --- a/src/aacs/keys.rs +++ b/src/aacs/keys.rs @@ -655,9 +655,9 @@ pub fn resolve_keys( #[cfg(test)] mod tests { - use super::*; use super::super::decrypt::{aes_ecb_encrypt, ALIGNED_UNIT_LEN}; use super::super::keydb::{DiscEntry, KeyDb}; + use super::*; /// Get KEYDB path from KEYDB_PATH environment variable. Returns None if not set or not found. fn keydb_path() -> Option { @@ -755,7 +755,10 @@ mod tests { let original = std::fs::read(unit_path).unwrap(); 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() { Some(p) => p, diff --git a/src/css/crack.rs b/src/css/crack.rs index 5d85c96..4ed6da6 100644 --- a/src/css/crack.rs +++ b/src/css/crack.rs @@ -16,7 +16,7 @@ use super::tables::{TAB1, TAB2, TAB3, TAB4, TAB5}; /// Sector layout constants. const SECTOR_SIZE: usize = 2048; const ENCRYPTED_START: usize = 0x80; // byte 128 -const SEED_OFFSET: usize = 0x54; // sector seed at bytes 0x54-0x58 +const SEED_OFFSET: usize = 0x54; // sector seed at bytes 0x54-0x58 const FLAG_BYTE: usize = 0x14; /// Recover the CSS title key from a scrambled sector using known plaintext. @@ -26,10 +26,7 @@ const FLAG_BYTE: usize = 0x14; /// a PES header: `00 00 01 [stream_id] ...` /// /// Returns the recovered 5-byte title key, or None if recovery fails. -pub fn recover_title_key( - sector: &[u8], - plain: &[u8], -) -> Option<[u8; 5]> { +pub fn recover_title_key(sector: &[u8], plain: &[u8]) -> Option<[u8; 5]> { if sector.len() < SECTOR_SIZE || plain.len() < 10 { return None; } @@ -58,14 +55,13 @@ pub fn recover_title_key( let mut result_key = [0u8; 5]; 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 t2 = i_try & 0xFF; let mut t5: u32 = 0; // Clock LFSR1 forward 4 steps to reconstruct LFSR0 state let mut t3: u32 = 0; - let mut ok = true; for i in 0..4 { // Advance LFSR1 @@ -102,7 +98,7 @@ pub fn recover_title_key( let t4_perm = TAB5[t4 as usize]; // 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); 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 t3 = candidate; + let mut recovery_ok = true; for _ in 0..4 { let t1_byte = t3 & 0xFF; t3 >>= 8; @@ -127,16 +124,20 @@ pub fn recover_title_key( let mut found_j = false; for j in 0u32..256 { 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 { found_j = true; break; } } if !found_j { - continue; + recovery_ok = false; + break; } } + if !recovery_ok { + continue 'outer; + } // Convert LFSR0 initial state back to key bytes let t4 = (t3 >> 1).wrapping_sub(4); @@ -356,9 +357,8 @@ mod tests { ); // Try with exact known plaintext instead of guessing - let exact_plain: [u8; 10] = [ - 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21, - ]; + let exact_plain: [u8; 10] = + [0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x80, 0x80, 0x05, 0x21]; let recovered = recover_title_key(&plaintext, &exact_plain); if let Some(key) = recovered { let mut test = plaintext.clone(); @@ -366,7 +366,10 @@ mod tests { assert_eq!(test[0x80], 0x00); assert_eq!(test[0x81], 0x00); 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 { eprintln!( "recover_title_key also returned None. The attack may not converge \ diff --git a/src/css/lfsr.rs b/src/css/lfsr.rs index e097580..9be009e 100644 --- a/src/css/lfsr.rs +++ b/src/css/lfsr.rs @@ -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) | ((working_key[3] as u32) << 9) - | ((working_key[2] as u32) << 1) - + 8 - - (working_key[2] as u32 & 7); + | (((working_key[2] as u32) << 1) + 8 - (working_key[2] as u32 & 7)); lfsr0 = (TAB4[(lfsr0 & 0xFF) as usize] as u32) << 24 | (TAB4[((lfsr0 >> 8) & 0xFF) as usize] as u32) << 16 | (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. /// The `invert` parameter controls the XOR applied to LFSR0 output /// (0x00 for disc key decryption, 0xFF for title key / sector key). -pub(crate) fn decrypt_key( - invert: u8, - p_key: &[u8; 5], - p_crypted: &[u8], -) -> [u8; 5] { +pub(crate) fn decrypt_key(invert: u8, p_key: &[u8; 5], p_crypted: &[u8]) -> [u8; 5] { if p_crypted.len() < 5 { return *p_key; } @@ -99,9 +93,7 @@ pub(crate) fn decrypt_key( let mut lfsr0: u32 = ((p_key[4] as u32) << 17) | ((p_key[3] as u32) << 9) - | ((p_key[2] as u32) << 1) - + 8 - - (p_key[2] as u32 & 7); + | (((p_key[2] as u32) << 1) + 8 - (p_key[2] as u32 & 7)); lfsr0 = (TAB4[(lfsr0 & 0xFF) as usize] as u32) << 24 | (TAB4[((lfsr0 >> 8) & 0xFF) as usize] as u32) << 16 | (TAB4[((lfsr0 >> 16) & 0xFF) as usize] as u32) << 8 @@ -160,7 +152,7 @@ mod tests { let key = [0x01, 0x02, 0x03, 0x04, 0x05]; let mut sector = vec![0xAA; 2048]; sector[0x14] = 0x30; // scramble flag set - // Set a sector seed + // Set a sector seed sector[0x54..0x59].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0x55]); let original = sector.clone(); descramble_sector(&key, &mut sector); @@ -230,7 +222,11 @@ mod tests { assert_eq!(rff, rff_again, "decrypt_key(0xFF) not deterministic"); // With different invert values, the keystream differs - assert_ne!(r0, rff, "invert=0x00 and 0xFF gave same result for key {:?}", key); + 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 descramble_sector(&title_key, &mut sector); // 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 - assert_ne!(§or[0x80..0x84], &original[0x80..0x84], - "encrypted region unchanged after descramble"); + assert_ne!( + §or[0x80..0x84], + &original[0x80..0x84], + "encrypted region unchanged after descramble" + ); // Restore the scramble flag and sector seed for second pass sector[0x14] = 0x30; @@ -281,8 +284,11 @@ mod tests { // Second descramble: XOR again = roundtrip descramble_sector(&title_key, &mut sector); // Now the encrypted region should match original - assert_eq!(§or[0x80..2048], &original[0x80..2048], - "double descramble did not roundtrip"); + assert_eq!( + §or[0x80..2048], + &original[0x80..2048], + "double descramble did not roundtrip" + ); } /// Test 4: css_tab1_relationship @@ -315,9 +321,7 @@ mod tests { #[test] fn css_tab4_is_bit_reversal() { for i in 0u16..256 { - let expected = (0..8).fold(0u8, |acc, bit| { - acc | (((i as u8 >> bit) & 1) << (7 - bit)) - }); + let expected = (0..8).fold(0u8, |acc, bit| acc | (((i as u8 >> bit) & 1) << (7 - bit))); assert_eq!( TAB4[i as usize], expected, "TAB4[{:#04x}] = {:#04x}, expected {:#04x} (bit reversal)", @@ -328,7 +332,8 @@ mod tests { for i in 0..256 { assert_eq!( TAB4[TAB4[i] as usize], i as u8, - "TAB4 is not an involution at {:#04x}", i + "TAB4 is not an involution at {:#04x}", + i ); } } diff --git a/src/css/tables.rs b/src/css/tables.rs index 3808403..d966b75 100644 --- a/src/css/tables.rs +++ b/src/css/tables.rs @@ -6,128 +6,117 @@ /// Table 1: byte substitution used in key mangling and sector seed processing. pub const TAB1: [u8; 256] = [ - 0x33, 0x73, 0x3b, 0x26, 0x63, 0x23, 0x6b, 0x76, 0x3e, 0x7e, 0x36, 0x2b, 0x6e, 0x2e, 0x66, - 0x7b, 0xd3, 0x93, 0xdb, 0x06, 0x43, 0x03, 0x4b, 0x96, 0xde, 0x9e, 0xd6, 0x0b, 0x4e, 0x0e, - 0x46, 0x9b, 0x57, 0x17, 0x5f, 0x82, 0xc7, 0x87, 0xcf, 0x12, 0x5a, 0x1a, 0x52, 0x8f, 0xca, - 0x8a, 0xc2, 0x1f, 0xd9, 0x99, 0xd1, 0x00, 0x49, 0x09, 0x41, 0x90, 0xd8, 0x98, 0xd0, 0x01, - 0x48, 0x08, 0x40, 0x91, 0x3d, 0x7d, 0x35, 0x24, 0x6d, 0x2d, 0x65, 0x74, 0x3c, 0x7c, 0x34, - 0x25, 0x6c, 0x2c, 0x64, 0x75, 0xdd, 0x9d, 0xd5, 0x04, 0x4d, 0x0d, 0x45, 0x94, 0xdc, 0x9c, - 0xd4, 0x05, 0x4c, 0x0c, 0x44, 0x95, 0x59, 0x19, 0x51, 0x80, 0xc9, 0x89, 0xc1, 0x10, 0x58, - 0x18, 0x50, 0x81, 0xc8, 0x88, 0xc0, 0x11, 0xd7, 0x97, 0xdf, 0x02, 0x47, 0x07, 0x4f, 0x92, - 0xda, 0x9a, 0xd2, 0x0f, 0x4a, 0x0a, 0x42, 0x9f, 0x53, 0x13, 0x5b, 0x86, 0xc3, 0x83, 0xcb, - 0x16, 0x5e, 0x1e, 0x56, 0x8b, 0xce, 0x8e, 0xc6, 0x1b, 0xb3, 0xf3, 0xbb, 0xa6, 0xe3, 0xa3, - 0xeb, 0xf6, 0xbe, 0xfe, 0xb6, 0xab, 0xee, 0xae, 0xe6, 0xfb, 0x37, 0x77, 0x3f, 0x22, 0x67, - 0x27, 0x6f, 0x72, 0x3a, 0x7a, 0x32, 0x2f, 0x6a, 0x2a, 0x62, 0x7f, 0xb9, 0xf9, 0xb1, 0xa0, - 0xe9, 0xa9, 0xe1, 0xf0, 0xb8, 0xf8, 0xb0, 0xa1, 0xe8, 0xa8, 0xe0, 0xf1, 0x5d, 0x1d, 0x55, - 0x84, 0xcd, 0x8d, 0xc5, 0x14, 0x5c, 0x1c, 0x54, 0x85, 0xcc, 0x8c, 0xc4, 0x15, 0xbd, 0xfd, - 0xb5, 0xa4, 0xed, 0xad, 0xe5, 0xf4, 0xbc, 0xfc, 0xb4, 0xa5, 0xec, 0xac, 0xe4, 0xf5, 0x39, - 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, + 0x33, 0x73, 0x3b, 0x26, 0x63, 0x23, 0x6b, 0x76, 0x3e, 0x7e, 0x36, 0x2b, 0x6e, 0x2e, 0x66, 0x7b, + 0xd3, 0x93, 0xdb, 0x06, 0x43, 0x03, 0x4b, 0x96, 0xde, 0x9e, 0xd6, 0x0b, 0x4e, 0x0e, 0x46, 0x9b, + 0x57, 0x17, 0x5f, 0x82, 0xc7, 0x87, 0xcf, 0x12, 0x5a, 0x1a, 0x52, 0x8f, 0xca, 0x8a, 0xc2, 0x1f, + 0xd9, 0x99, 0xd1, 0x00, 0x49, 0x09, 0x41, 0x90, 0xd8, 0x98, 0xd0, 0x01, 0x48, 0x08, 0x40, 0x91, + 0x3d, 0x7d, 0x35, 0x24, 0x6d, 0x2d, 0x65, 0x74, 0x3c, 0x7c, 0x34, 0x25, 0x6c, 0x2c, 0x64, 0x75, + 0xdd, 0x9d, 0xd5, 0x04, 0x4d, 0x0d, 0x45, 0x94, 0xdc, 0x9c, 0xd4, 0x05, 0x4c, 0x0c, 0x44, 0x95, + 0x59, 0x19, 0x51, 0x80, 0xc9, 0x89, 0xc1, 0x10, 0x58, 0x18, 0x50, 0x81, 0xc8, 0x88, 0xc0, 0x11, + 0xd7, 0x97, 0xdf, 0x02, 0x47, 0x07, 0x4f, 0x92, 0xda, 0x9a, 0xd2, 0x0f, 0x4a, 0x0a, 0x42, 0x9f, + 0x53, 0x13, 0x5b, 0x86, 0xc3, 0x83, 0xcb, 0x16, 0x5e, 0x1e, 0x56, 0x8b, 0xce, 0x8e, 0xc6, 0x1b, + 0xb3, 0xf3, 0xbb, 0xa6, 0xe3, 0xa3, 0xeb, 0xf6, 0xbe, 0xfe, 0xb6, 0xab, 0xee, 0xae, 0xe6, 0xfb, + 0x37, 0x77, 0x3f, 0x22, 0x67, 0x27, 0x6f, 0x72, 0x3a, 0x7a, 0x32, 0x2f, 0x6a, 0x2a, 0x62, 0x7f, + 0xb9, 0xf9, 0xb1, 0xa0, 0xe9, 0xa9, 0xe1, 0xf0, 0xb8, 0xf8, 0xb0, 0xa1, 0xe8, 0xa8, 0xe0, 0xf1, + 0x5d, 0x1d, 0x55, 0x84, 0xcd, 0x8d, 0xc5, 0x14, 0x5c, 0x1c, 0x54, 0x85, 0xcc, 0x8c, 0xc4, 0x15, + 0xbd, 0xfd, 0xb5, 0xa4, 0xed, 0xad, 0xe5, 0xf4, 0xbc, 0xfc, 0xb4, 0xa5, 0xec, 0xac, 0xe4, 0xf5, + 0x39, 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, ]; /// Table 2: LFSR1 high-byte feedback permutation. pub const TAB2: [u8; 256] = [ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x09, 0x08, 0x0b, 0x0a, 0x0d, 0x0c, 0x0f, - 0x0e, 0x12, 0x13, 0x10, 0x11, 0x16, 0x17, 0x14, 0x15, 0x1b, 0x1a, 0x19, 0x18, 0x1f, 0x1e, - 0x1d, 0x1c, 0x24, 0x25, 0x26, 0x27, 0x20, 0x21, 0x22, 0x23, 0x2d, 0x2c, 0x2f, 0x2e, 0x29, - 0x28, 0x2b, 0x2a, 0x36, 0x37, 0x34, 0x35, 0x32, 0x33, 0x30, 0x31, 0x3f, 0x3e, 0x3d, 0x3c, - 0x3b, 0x3a, 0x39, 0x38, 0x49, 0x48, 0x4b, 0x4a, 0x4d, 0x4c, 0x4f, 0x4e, 0x40, 0x41, 0x42, - 0x43, 0x44, 0x45, 0x46, 0x47, 0x5b, 0x5a, 0x59, 0x58, 0x5f, 0x5e, 0x5d, 0x5c, 0x52, 0x53, - 0x50, 0x51, 0x56, 0x57, 0x54, 0x55, 0x6d, 0x6c, 0x6f, 0x6e, 0x69, 0x68, 0x6b, 0x6a, 0x64, - 0x65, 0x66, 0x67, 0x60, 0x61, 0x62, 0x63, 0x7f, 0x7e, 0x7d, 0x7c, 0x7b, 0x7a, 0x79, 0x78, - 0x76, 0x77, 0x74, 0x75, 0x72, 0x73, 0x70, 0x71, 0x92, 0x93, 0x90, 0x91, 0x96, 0x97, 0x94, - 0x95, 0x9b, 0x9a, 0x99, 0x98, 0x9f, 0x9e, 0x9d, 0x9c, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, - 0x86, 0x87, 0x89, 0x88, 0x8b, 0x8a, 0x8d, 0x8c, 0x8f, 0x8e, 0xb6, 0xb7, 0xb4, 0xb5, 0xb2, - 0xb3, 0xb0, 0xb1, 0xbf, 0xbe, 0xbd, 0xbc, 0xbb, 0xba, 0xb9, 0xb8, 0xa4, 0xa5, 0xa6, 0xa7, - 0xa0, 0xa1, 0xa2, 0xa3, 0xad, 0xac, 0xaf, 0xae, 0xa9, 0xa8, 0xab, 0xaa, 0xdb, 0xda, 0xd9, - 0xd8, 0xdf, 0xde, 0xdd, 0xdc, 0xd2, 0xd3, 0xd0, 0xd1, 0xd6, 0xd7, 0xd4, 0xd5, 0xc9, 0xc8, - 0xcb, 0xca, 0xcd, 0xcc, 0xcf, 0xce, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xed, - 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, + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x09, 0x08, 0x0b, 0x0a, 0x0d, 0x0c, 0x0f, 0x0e, + 0x12, 0x13, 0x10, 0x11, 0x16, 0x17, 0x14, 0x15, 0x1b, 0x1a, 0x19, 0x18, 0x1f, 0x1e, 0x1d, 0x1c, + 0x24, 0x25, 0x26, 0x27, 0x20, 0x21, 0x22, 0x23, 0x2d, 0x2c, 0x2f, 0x2e, 0x29, 0x28, 0x2b, 0x2a, + 0x36, 0x37, 0x34, 0x35, 0x32, 0x33, 0x30, 0x31, 0x3f, 0x3e, 0x3d, 0x3c, 0x3b, 0x3a, 0x39, 0x38, + 0x49, 0x48, 0x4b, 0x4a, 0x4d, 0x4c, 0x4f, 0x4e, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x5b, 0x5a, 0x59, 0x58, 0x5f, 0x5e, 0x5d, 0x5c, 0x52, 0x53, 0x50, 0x51, 0x56, 0x57, 0x54, 0x55, + 0x6d, 0x6c, 0x6f, 0x6e, 0x69, 0x68, 0x6b, 0x6a, 0x64, 0x65, 0x66, 0x67, 0x60, 0x61, 0x62, 0x63, + 0x7f, 0x7e, 0x7d, 0x7c, 0x7b, 0x7a, 0x79, 0x78, 0x76, 0x77, 0x74, 0x75, 0x72, 0x73, 0x70, 0x71, + 0x92, 0x93, 0x90, 0x91, 0x96, 0x97, 0x94, 0x95, 0x9b, 0x9a, 0x99, 0x98, 0x9f, 0x9e, 0x9d, 0x9c, + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x89, 0x88, 0x8b, 0x8a, 0x8d, 0x8c, 0x8f, 0x8e, + 0xb6, 0xb7, 0xb4, 0xb5, 0xb2, 0xb3, 0xb0, 0xb1, 0xbf, 0xbe, 0xbd, 0xbc, 0xbb, 0xba, 0xb9, 0xb8, + 0xa4, 0xa5, 0xa6, 0xa7, 0xa0, 0xa1, 0xa2, 0xa3, 0xad, 0xac, 0xaf, 0xae, 0xa9, 0xa8, 0xab, 0xaa, + 0xdb, 0xda, 0xd9, 0xd8, 0xdf, 0xde, 0xdd, 0xdc, 0xd2, 0xd3, 0xd0, 0xd1, 0xd6, 0xd7, 0xd4, 0xd5, + 0xc9, 0xc8, 0xcb, 0xca, 0xcd, 0xcc, 0xcf, 0xce, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, + 0xed, 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, ]; /// Table 3: LFSR1 low-byte feedback permutation. pub const TAB3: [u8; 512] = [ - 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, 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, 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, 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, - 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, 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, 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, - 0xfe, 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, 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, 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, - 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, 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, 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, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, 0xd8, 0xfc, 0x03, 0x27, 0x4a, 0x6e, 0x91, 0xb5, - 0xd8, 0xfc, + 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, + 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, + 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, + 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, 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, 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, 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, 0xfe, + 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, 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, 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, 0xfd, 0x02, 0x26, 0x4b, 0x6f, 0x90, 0xb4, 0xd9, 0xfd, + 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, 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, + 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). pub const TAB4: [u8; 256] = [ - 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, - 0xf0, 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, - 0x78, 0xf8, 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 0x14, 0x94, 0x54, 0xd4, 0x34, - 0xb4, 0x74, 0xf4, 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 0x1c, 0x9c, 0x5c, 0xdc, - 0x3c, 0xbc, 0x7c, 0xfc, 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 0x12, 0x92, 0x52, - 0xd2, 0x32, 0xb2, 0x72, 0xf2, 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 0x1a, 0x9a, - 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 0x16, - 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, - 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, - 0xe1, 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, - 0x69, 0xe9, 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, 0x05, 0x85, 0x45, 0xc5, 0x25, - 0xa5, 0x65, 0xe5, 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, 0x0d, 0x8d, 0x4d, 0xcd, - 0x2d, 0xad, 0x6d, 0xed, 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, 0x03, 0x83, 0x43, - 0xc3, 0x23, 0xa3, 0x63, 0xe3, 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, 0x0b, 0x8b, - 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, 0x07, - 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, + 0x00, 0x80, 0x40, 0xc0, 0x20, 0xa0, 0x60, 0xe0, 0x10, 0x90, 0x50, 0xd0, 0x30, 0xb0, 0x70, 0xf0, + 0x08, 0x88, 0x48, 0xc8, 0x28, 0xa8, 0x68, 0xe8, 0x18, 0x98, 0x58, 0xd8, 0x38, 0xb8, 0x78, 0xf8, + 0x04, 0x84, 0x44, 0xc4, 0x24, 0xa4, 0x64, 0xe4, 0x14, 0x94, 0x54, 0xd4, 0x34, 0xb4, 0x74, 0xf4, + 0x0c, 0x8c, 0x4c, 0xcc, 0x2c, 0xac, 0x6c, 0xec, 0x1c, 0x9c, 0x5c, 0xdc, 0x3c, 0xbc, 0x7c, 0xfc, + 0x02, 0x82, 0x42, 0xc2, 0x22, 0xa2, 0x62, 0xe2, 0x12, 0x92, 0x52, 0xd2, 0x32, 0xb2, 0x72, 0xf2, + 0x0a, 0x8a, 0x4a, 0xca, 0x2a, 0xaa, 0x6a, 0xea, 0x1a, 0x9a, 0x5a, 0xda, 0x3a, 0xba, 0x7a, 0xfa, + 0x06, 0x86, 0x46, 0xc6, 0x26, 0xa6, 0x66, 0xe6, 0x16, 0x96, 0x56, 0xd6, 0x36, 0xb6, 0x76, 0xf6, + 0x0e, 0x8e, 0x4e, 0xce, 0x2e, 0xae, 0x6e, 0xee, 0x1e, 0x9e, 0x5e, 0xde, 0x3e, 0xbe, 0x7e, 0xfe, + 0x01, 0x81, 0x41, 0xc1, 0x21, 0xa1, 0x61, 0xe1, 0x11, 0x91, 0x51, 0xd1, 0x31, 0xb1, 0x71, 0xf1, + 0x09, 0x89, 0x49, 0xc9, 0x29, 0xa9, 0x69, 0xe9, 0x19, 0x99, 0x59, 0xd9, 0x39, 0xb9, 0x79, 0xf9, + 0x05, 0x85, 0x45, 0xc5, 0x25, 0xa5, 0x65, 0xe5, 0x15, 0x95, 0x55, 0xd5, 0x35, 0xb5, 0x75, 0xf5, + 0x0d, 0x8d, 0x4d, 0xcd, 0x2d, 0xad, 0x6d, 0xed, 0x1d, 0x9d, 0x5d, 0xdd, 0x3d, 0xbd, 0x7d, 0xfd, + 0x03, 0x83, 0x43, 0xc3, 0x23, 0xa3, 0x63, 0xe3, 0x13, 0x93, 0x53, 0xd3, 0x33, 0xb3, 0x73, 0xf3, + 0x0b, 0x8b, 0x4b, 0xcb, 0x2b, 0xab, 0x6b, 0xeb, 0x1b, 0x9b, 0x5b, 0xdb, 0x3b, 0xbb, 0x7b, 0xfb, + 0x07, 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, ]; /// Table 5: LFSR1 output permutation for the Stevenson attack. /// This is the inverse byte-reversal of TAB4. pub const TAB5: [u8; 256] = [ - 0xff, 0x7f, 0xbf, 0x3f, 0xdf, 0x5f, 0x9f, 0x1f, 0xef, 0x6f, 0xaf, 0x2f, 0xcf, 0x4f, 0x8f, - 0x0f, 0xf7, 0x77, 0xb7, 0x37, 0xd7, 0x57, 0x97, 0x17, 0xe7, 0x67, 0xa7, 0x27, 0xc7, 0x47, - 0x87, 0x07, 0xfb, 0x7b, 0xbb, 0x3b, 0xdb, 0x5b, 0x9b, 0x1b, 0xeb, 0x6b, 0xab, 0x2b, 0xcb, - 0x4b, 0x8b, 0x0b, 0xf3, 0x73, 0xb3, 0x33, 0xd3, 0x53, 0x93, 0x13, 0xe3, 0x63, 0xa3, 0x23, - 0xc3, 0x43, 0x83, 0x03, 0xfd, 0x7d, 0xbd, 0x3d, 0xdd, 0x5d, 0x9d, 0x1d, 0xed, 0x6d, 0xad, - 0x2d, 0xcd, 0x4d, 0x8d, 0x0d, 0xf5, 0x75, 0xb5, 0x35, 0xd5, 0x55, 0x95, 0x15, 0xe5, 0x65, - 0xa5, 0x25, 0xc5, 0x45, 0x85, 0x05, 0xf9, 0x79, 0xb9, 0x39, 0xd9, 0x59, 0x99, 0x19, 0xe9, - 0x69, 0xa9, 0x29, 0xc9, 0x49, 0x89, 0x09, 0xf1, 0x71, 0xb1, 0x31, 0xd1, 0x51, 0x91, 0x11, - 0xe1, 0x61, 0xa1, 0x21, 0xc1, 0x41, 0x81, 0x01, 0xfe, 0x7e, 0xbe, 0x3e, 0xde, 0x5e, 0x9e, - 0x1e, 0xee, 0x6e, 0xae, 0x2e, 0xce, 0x4e, 0x8e, 0x0e, 0xf6, 0x76, 0xb6, 0x36, 0xd6, 0x56, - 0x96, 0x16, 0xe6, 0x66, 0xa6, 0x26, 0xc6, 0x46, 0x86, 0x06, 0xfa, 0x7a, 0xba, 0x3a, 0xda, - 0x5a, 0x9a, 0x1a, 0xea, 0x6a, 0xaa, 0x2a, 0xca, 0x4a, 0x8a, 0x0a, 0xf2, 0x72, 0xb2, 0x32, - 0xd2, 0x52, 0x92, 0x12, 0xe2, 0x62, 0xa2, 0x22, 0xc2, 0x42, 0x82, 0x02, 0xfc, 0x7c, 0xbc, - 0x3c, 0xdc, 0x5c, 0x9c, 0x1c, 0xec, 0x6c, 0xac, 0x2c, 0xcc, 0x4c, 0x8c, 0x0c, 0xf4, 0x74, - 0xb4, 0x34, 0xd4, 0x54, 0x94, 0x14, 0xe4, 0x64, 0xa4, 0x24, 0xc4, 0x44, 0x84, 0x04, 0xf8, - 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, + 0xff, 0x7f, 0xbf, 0x3f, 0xdf, 0x5f, 0x9f, 0x1f, 0xef, 0x6f, 0xaf, 0x2f, 0xcf, 0x4f, 0x8f, 0x0f, + 0xf7, 0x77, 0xb7, 0x37, 0xd7, 0x57, 0x97, 0x17, 0xe7, 0x67, 0xa7, 0x27, 0xc7, 0x47, 0x87, 0x07, + 0xfb, 0x7b, 0xbb, 0x3b, 0xdb, 0x5b, 0x9b, 0x1b, 0xeb, 0x6b, 0xab, 0x2b, 0xcb, 0x4b, 0x8b, 0x0b, + 0xf3, 0x73, 0xb3, 0x33, 0xd3, 0x53, 0x93, 0x13, 0xe3, 0x63, 0xa3, 0x23, 0xc3, 0x43, 0x83, 0x03, + 0xfd, 0x7d, 0xbd, 0x3d, 0xdd, 0x5d, 0x9d, 0x1d, 0xed, 0x6d, 0xad, 0x2d, 0xcd, 0x4d, 0x8d, 0x0d, + 0xf5, 0x75, 0xb5, 0x35, 0xd5, 0x55, 0x95, 0x15, 0xe5, 0x65, 0xa5, 0x25, 0xc5, 0x45, 0x85, 0x05, + 0xf9, 0x79, 0xb9, 0x39, 0xd9, 0x59, 0x99, 0x19, 0xe9, 0x69, 0xa9, 0x29, 0xc9, 0x49, 0x89, 0x09, + 0xf1, 0x71, 0xb1, 0x31, 0xd1, 0x51, 0x91, 0x11, 0xe1, 0x61, 0xa1, 0x21, 0xc1, 0x41, 0x81, 0x01, + 0xfe, 0x7e, 0xbe, 0x3e, 0xde, 0x5e, 0x9e, 0x1e, 0xee, 0x6e, 0xae, 0x2e, 0xce, 0x4e, 0x8e, 0x0e, + 0xf6, 0x76, 0xb6, 0x36, 0xd6, 0x56, 0x96, 0x16, 0xe6, 0x66, 0xa6, 0x26, 0xc6, 0x46, 0x86, 0x06, + 0xfa, 0x7a, 0xba, 0x3a, 0xda, 0x5a, 0x9a, 0x1a, 0xea, 0x6a, 0xaa, 0x2a, 0xca, 0x4a, 0x8a, 0x0a, + 0xf2, 0x72, 0xb2, 0x32, 0xd2, 0x52, 0x92, 0x12, 0xe2, 0x62, 0xa2, 0x22, 0xc2, 0x42, 0x82, 0x02, + 0xfc, 0x7c, 0xbc, 0x3c, 0xdc, 0x5c, 0x9c, 0x1c, 0xec, 0x6c, 0xac, 0x2c, 0xcc, 0x4c, 0x8c, 0x0c, + 0xf4, 0x74, 0xb4, 0x34, 0xd4, 0x54, 0x94, 0x14, 0xe4, 0x64, 0xa4, 0x24, 0xc4, 0x44, 0x84, 0x04, + 0xf8, 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, ]; diff --git a/src/disc/bluray.rs b/src/disc/bluray.rs index 7151343..5d16bee 100644 --- a/src/disc/bluray.rs +++ b/src/disc/bluray.rs @@ -8,7 +8,10 @@ use crate::udf; impl Disc { /// Scan Blu-ray titles from MPLS playlists. - pub(super) fn scan_bluray_titles(reader: &mut dyn SectorReader, udf_fs: &udf::UdfFs) -> Vec { + pub(super) fn scan_bluray_titles( + reader: &mut dyn SectorReader, + udf_fs: &udf::UdfFs, + ) -> Vec { let mut titles = Vec::new(); if let Some(playlist_dir) = udf_fs.find_dir("/BDMV/PLAYLIST") { 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). /// Prefers English, falls back to first available language. /// 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 { + pub(super) fn read_meta_title( + reader: &mut dyn SectorReader, + udf_fs: &udf::UdfFs, + ) -> Option { let meta_dir = udf_fs.find_dir("/BDMV/META")?; for sub in &meta_dir.entries { if !sub.is_dir { diff --git a/src/disc/dvd.rs b/src/disc/dvd.rs index e62445a..2773e01 100644 --- a/src/disc/dvd.rs +++ b/src/disc/dvd.rs @@ -7,7 +7,10 @@ use crate::udf; impl Disc { /// 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 { + pub(super) fn scan_dvd_titles( + reader: &mut dyn SectorReader, + udf_fs: &udf::UdfFs, + ) -> Vec { let dvd_info = match ifo::parse_vmg(reader, udf_fs) { Ok(info) => info, Err(_) => return Vec::new(), @@ -83,8 +86,8 @@ impl Disc { .cells .iter() .map(|cell| { - let start = ts.vob_start_sector + cell.first_sector; - let count = cell.last_sector.saturating_sub(cell.first_sector) + 1; + let start = ts.vob_start_sector.saturating_add(cell.first_sector); + let count = cell.last_sector.saturating_sub(cell.first_sector).saturating_add(1); Extent { start_lba: start, sector_count: count, @@ -92,10 +95,7 @@ impl Disc { }) .collect(); - let size_bytes: u64 = extents - .iter() - .map(|e| e.sector_count as u64 * 2048) - .sum(); + let size_bytes: u64 = extents.iter().map(|e| e.sector_count as u64 * 2048).sum(); // Build pre-formatted palette codec_data for VobSub subtitle streams let codec_data = dvd_title diff --git a/src/disc/encrypt.rs b/src/disc/encrypt.rs index de9c36c..b46e217 100644 --- a/src/disc/encrypt.rs +++ b/src/disc/encrypt.rs @@ -11,13 +11,15 @@ use crate::udf; pub(super) struct HandshakeResult { pub volume_id: [u8; 16], pub read_data_key: Option<[u8; 16]>, - pub error: Option, } impl Disc { /// SCSI handshake result — volume ID and bus keys from ECDH authentication. /// Only available when scanning from a real drive (not ISO images). - pub(super) fn do_handshake(session: &mut crate::drive::DriveSession, opts: &ScanOptions) -> Option { + pub(super) fn do_handshake( + session: &mut crate::drive::DriveSession, + opts: &ScanOptions, + ) -> Option { use crate::aacs::{self, KeyDb}; let keydb_path = opts.resolve_keydb()?; @@ -35,7 +37,6 @@ impl Disc { return Some(HandshakeResult { volume_id, read_data_key, - error: None, }); } Err(e) => { @@ -45,10 +46,9 @@ impl Disc { } } } - last_error.map(|e| HandshakeResult { + last_error.map(|_e| HandshakeResult { volume_id: [0u8; 16], read_data_key: None, - error: Some(e), }) } @@ -90,7 +90,6 @@ impl Disc { // (KEYDB VUK lookup by disc hash works without volume ID) 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 handshake_error = None; // Resolve: tries all available paths — KEYDB VUK, media key, processing key, device key let resolved = aacs::resolve_keys( @@ -118,7 +117,6 @@ impl Disc { unit_keys: resolved.unit_keys, read_data_key, volume_id, - handshake_error, }) } } diff --git a/src/disc/mod.rs b/src/disc/mod.rs index 9c6e4bf..a3fabaf 100644 --- a/src/disc/mod.rs +++ b/src/disc/mod.rs @@ -370,8 +370,6 @@ pub struct AacsState { pub read_data_key: Option<[u8; 16]>, /// Volume ID (16 bytes) -- from SCSI handshake pub volume_id: [u8; 16], - /// Handshake error code if authentication failed (None = no HC, or success) - pub handshake_error: Option, } /// How AACS keys were resolved. @@ -414,7 +412,6 @@ pub struct ScanOptions { pub keydb_path: Option, } - impl ScanOptions { /// Create options with a specific KEYDB path. pub fn with_keydb(path: impl Into) -> Self { @@ -577,9 +574,15 @@ impl Disc { // 3. Titles — BD (MPLS playlists) or DVD (IFO title sets) 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() { - (Self::scan_dvd_titles(reader, &udf_fs), ContentFormat::MpegPs) + ( + Self::scan_dvd_titles(reader, &udf_fs), + ContentFormat::MpegPs, + ) } else { (Vec::new(), ContentFormat::BdTs) }; @@ -667,7 +670,6 @@ impl Disc { let lba = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]); Ok(lba + 1) } - } // ─── Decrypted reader ────────────────────────────────────────────────────── @@ -684,7 +686,6 @@ pub struct ContentReader<'a> { session: &'a mut DriveSession, aacs: Option<&'a AacsState>, css: Option<&'a crate::css::CssState>, - content_format: ContentFormat, extents: Vec, current_extent: usize, current_offset: u32, @@ -716,13 +717,10 @@ impl Disc { session: &'a mut DriveSession, title_idx: usize, ) -> Result> { - let title = self - .titles - .get(title_idx) - .ok_or(Error::DiscTitleRange { - index: title_idx, - count: self.titles.len(), - })?; + let title = self.titles.get(title_idx).ok_or(Error::DiscTitleRange { + index: title_idx, + count: self.titles.len(), + })?; // Let the drive manage its own read speed after init. // SET_CD_SPEED is only used reactively by the error handler to slow @@ -735,7 +733,6 @@ impl Disc { session, aacs: self.aacs.as_ref(), css: self.css.as_ref(), - content_format: title.content_format, extents: title.extents.clone(), current_extent: 0, current_offset: 0, @@ -756,7 +753,7 @@ impl Disc { /// Reads /sys/block//queue/max_hw_sectors_kb on Linux. /// For sg devices, resolves the corresponding block device via sysfs. /// 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(""); if dev_name.is_empty() { return DEFAULT_BATCH_SECTORS; @@ -793,12 +790,12 @@ fn detect_max_batch_sectors(device_path: &str) -> u16 { } /// Read strategy constants -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) -const MIN_BATCH_SECTORS: u16 = 3; // 1 aligned unit = 6KB (error recovery) -const RAMP_BATCH_AFTER: u32 = 5; // successes before doubling batch size -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 MAX_BATCH_SECTORS: u16 = 510; // absolute max (170 aligned units ≈ 1MB) +pub(crate) const DEFAULT_BATCH_SECTORS: u16 = 60; // fallback: typical kernel limit (120KB = 60 sectors) +pub(crate) const MIN_BATCH_SECTORS: u16 = 3; // 1 aligned unit = 6KB (error recovery) +pub(crate) const RAMP_BATCH_AFTER: u32 = 5; // successes before doubling batch size +pub(crate) const RAMP_SPEED_AFTER: u32 = 50; // successes at max batch before restoring speed +pub(crate) const SLOW_SPEED_AFTER: u32 = 3; // consecutive errors before reducing disc speed impl<'a> ContentReader<'a> { /// Total bytes across all extents (for progress display). @@ -814,10 +811,9 @@ impl<'a> ContentReader<'a> { /// Returns None when all extents are exhausted. pub fn read_unit(&mut self) -> Result>> { // Refill buffer if empty - if self.buf_pos >= self.buf_len - && !self.fill_buffer()? { - return Ok(None); - } + if self.buf_pos >= self.buf_len && !self.fill_buffer()? { + return Ok(None); + } // Extract one aligned unit from buffer let start = self.buf_pos * crate::aacs::ALIGNED_UNIT_LEN; diff --git a/src/ifo.rs b/src/ifo.rs index b1133c5..9862d9b 100644 --- a/src/ifo.rs +++ b/src/ifo.rs @@ -200,7 +200,9 @@ pub fn parse_vmg(reader: &mut dyn SectorReader, udf: &UdfFs) -> Result // 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. - 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 // (IFO files are typically small, a few sectors). Check bounds. @@ -312,7 +314,9 @@ fn parse_vts( } // 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)?; Ok(DvdTitleSet { @@ -409,8 +413,10 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result { // Language code: bytes 2-3 as ISO 639 let lang_bytes = sub_slice(data, offset + 2, 2)?; - let language = if lang_bytes[0] >= b'a' && lang_bytes[0] <= b'z' - && lang_bytes[1] >= b'a' && lang_bytes[1] <= b'z' + let language = if lang_bytes[0] >= b'a' + && lang_bytes[0] <= b'z' + && lang_bytes[1] >= b'a' + && lang_bytes[1] <= b'z' { String::from_utf8_lossy(lang_bytes).to_string() } else if lang_bytes[0] == 0 && lang_bytes[1] == 0 { @@ -437,8 +443,10 @@ fn parse_audio_attr(data: &[u8], offset: usize) -> Result { fn parse_subtitle_attr(data: &[u8], offset: usize) -> Result { // Language code: bytes 2-3 as ISO 639 let lang_bytes = sub_slice(data, offset + 2, 2)?; - let language = if lang_bytes[0] >= b'a' && lang_bytes[0] <= b'z' - && lang_bytes[1] >= b'a' && lang_bytes[1] <= b'z' + let language = if lang_bytes[0] >= b'a' + && lang_bytes[0] <= b'z' + && lang_bytes[1] >= b'a' + && lang_bytes[1] <= b'z' { String::from_utf8_lossy(lang_bytes).to_string() } 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 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) { Ok(title) => titles.push(title), @@ -527,7 +537,9 @@ fn parse_pgc(data: &[u8], pgc_offset: usize, chapters: u16) -> Result // Parse cells let mut cells = Vec::with_capacity(num_cells); 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 { let co = cell_base + i * 24; if co + 24 > data.len() { diff --git a/src/labels/criterion.rs b/src/labels/criterion.rs index f11b6cb..5588d1b 100644 --- a/src/labels/criterion.rs +++ b/src/labels/criterion.rs @@ -143,7 +143,9 @@ fn parse_playback_config(xml: &str, map: &mut HashMap) { while pos < xml.len() { let tag_start = if let Some(p) = xml[pos..].find("") { Some(p + pos) - } else { xml[pos..].find("").map(|p| p + pos) }; + } else { + xml[pos..].find("").map(|p| p + pos) + }; let tag_start = match tag_start { Some(p) => p, diff --git a/src/lib.rs b/src/lib.rs index 887022e..8a04aab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,9 +73,9 @@ pub mod css; pub mod disc; pub mod drive; pub mod error; -pub mod ifo; pub mod event; pub mod identity; +pub mod ifo; pub mod keydb; pub mod labels; pub mod mpls; diff --git a/src/mpls.rs b/src/mpls.rs index e27c716..cdc25b1 100644 --- a/src/mpls.rs +++ b/src/mpls.rs @@ -264,7 +264,8 @@ pub fn parse(data: &[u8]) -> Result { } let mark_type = ms[mpos]; 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 { mark_type, play_item_ref, @@ -536,11 +537,11 @@ mod tests { buf.extend_from_slice(&(mark_section_len as u32).to_be_bytes()); buf.extend_from_slice(&(marks.len() as u16).to_be_bytes()); for m in marks { - buf.push(m.mark_type); // [0] mark_type - buf.push(0); // [1] reserved + buf.push(m.mark_type); // [0] mark_type + buf.push(0); // [1] reserved buf.extend_from_slice(&m.play_item_ref.to_be_bytes()); // [2-3] play_item_ref - buf.extend_from_slice(&m.timestamp.to_be_bytes()); // [4-7] timestamp - buf.extend_from_slice(&[0u8; 6]); // [8-13] padding (entry_ES_PID + duration + mark_data) + buf.extend_from_slice(&m.timestamp.to_be_bytes()); // [4-7] timestamp + buf.extend_from_slice(&[0u8; 6]); // [8-13] padding (entry_ES_PID + duration + mark_data) } buf @@ -771,9 +772,21 @@ mod tests { fn parse_marks_chapter_entries() { let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None); let marks = vec![ - TestMark { mark_type: 1, 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 }, + TestMark { + mark_type: 1, + 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( @@ -799,10 +812,26 @@ mod tests { // Chapters at 0s, 100s, 200s relative to in_time let marks = vec![ - TestMark { mark_type: 1, play_item_ref: 0, 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 + TestMark { + mark_type: 1, + play_item_ref: 0, + 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( diff --git a/src/mux/codec/dts.rs b/src/mux/codec/dts.rs index e92af38..594d4c8 100644 --- a/src/mux/codec/dts.rs +++ b/src/mux/codec/dts.rs @@ -87,9 +87,8 @@ pub fn find_dts_hd_ext_sync(data: &[u8]) -> Option { /// ((ext[6] & 0x1F) << 11) | (ext[7] << 3) | (ext[8] >> 5) + 1 pub fn dts_hd_ext_frame_size(ext: &[u8]) -> usize { debug_assert!(ext.len() >= 9); - let raw = ((ext[6] as usize & 0x1F) << 11) - | ((ext[7] as usize) << 3) - | ((ext[8] as usize) >> 5); + let raw = + ((ext[6] as usize & 0x1F) << 11) | ((ext[7] as usize) << 3) | ((ext[8] as usize) >> 5); raw + 1 } @@ -218,7 +217,7 @@ mod tests { fn parse_core_plus_extension_truncated_at_buffer_end() { let mut parser = DtsParser::new(); let core = make_dts_core(4); // 8 bytes - // Extension claims 200 bytes but we only provide 20 + // Extension claims 200 bytes but we only provide 20 let ext = make_dts_hd_ext(199, 0xDD); // wants 200 bytes let mut data = core; // Only append partial extension (first 20 bytes) diff --git a/src/mux/codec/dvdsub.rs b/src/mux/codec/dvdsub.rs index 89ef273..bb5102a 100644 --- a/src/mux/codec/dvdsub.rs +++ b/src/mux/codec/dvdsub.rs @@ -115,7 +115,10 @@ mod tests { let frames = parser.parse(&pes); 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); } @@ -127,7 +130,10 @@ mod tests { let pes = make_pes(data, Some(90000 * i as i64)); let frames = parser.parse(&pes); 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 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"); // 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 - assert!(text.contains("ffffff"), "white should be ffffff, got: {}", text); + assert!( + text.contains("ffffff"), + "white should be ffffff, got: {}", + text + ); } #[test] fn format_palette_16_colors() { - let palette: Vec<[u8; 4]> = (0..16) - .map(|i| [0x00, (i * 16) as u8, 128, 128]) - .collect(); + let palette: Vec<[u8; 4]> = (0..16).map(|i| [0x00, (i * 16) as u8, 128, 128]).collect(); let result = format_palette(&palette); let text = String::from_utf8(result).unwrap(); // Should have exactly 15 commas (16 colors separated by ", ") 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] diff --git a/src/mux/codec/hevc.rs b/src/mux/codec/hevc.rs index 8a7e78b..f50f688 100644 --- a/src/mux/codec/hevc.rs +++ b/src/mux/codec/hevc.rs @@ -525,7 +525,9 @@ mod tests { let mut nal_types = Vec::new(); let mut offset = 0; 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; assert!(offset + length <= fd.len(), "NAL length exceeds frame data"); let nal_type = (fd[offset] >> 1) & 0x3F; @@ -553,7 +555,9 @@ mod tests { // Verify RPU payload is intact let mut offset = 0; 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; let nal_type = (fd[offset] >> 1) & 0x3F; if nal_type == 62 { diff --git a/src/mux/codec/mpeg2.rs b/src/mux/codec/mpeg2.rs index dff50d4..ab7609c 100644 --- a/src/mux/codec/mpeg2.rs +++ b/src/mux/codec/mpeg2.rs @@ -39,10 +39,10 @@ const FRAME_RATES: [(u32, u32); 9] = [ /// Aspect ratio table (index from sequence header aspect_ratio_information). const ASPECT_RATIOS: [(u8, u8); 5] = [ - (0, 0), // 0: forbidden - (1, 1), // 1: square pixels (1:1 SAR) - (4, 3), // 2: 4:3 display - (16, 9), // 3: 16:9 display + (0, 0), // 0: forbidden + (1, 1), // 1: square pixels (1:1 SAR) + (4, 3), // 2: 4:3 display + (16, 9), // 3: 16:9 display (221, 100), // 4: 2.21:1 display ]; @@ -114,8 +114,7 @@ impl CodecParser for Mpeg2Parser { // Check if sequence extension follows immediately. if hdr_end + 3 < data.len() && data[hdr_end + 3] == SEQ_EXT_CODE { - let ext_end = - find_start_code(data, hdr_end + 4).unwrap_or(data.len()); + let ext_end = find_start_code(data, hdr_end + 4).unwrap_or(data.len()); seq_data.extend_from_slice(&data[hdr_end..ext_end]); } @@ -344,7 +343,10 @@ mod tests { let _frames = parser.parse(&pes); 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(); // Should start with the sequence header start code. assert_eq!(&cp[..4], &[0x00, 0x00, 0x01, SEQ_HEADER_CODE]); @@ -368,7 +370,7 @@ mod tests { // Sequence extension: 00 00 01 B5 [ext data] data.extend_from_slice(&[0x00, 0x00, 0x01, SEQ_EXT_CODE]); data.extend_from_slice(&[0x14, 0x8A, 0x00, 0x01, 0x00, 0x00]); // ext payload - // Picture header follows. + // Picture header follows. data.extend_from_slice(&make_picture_header(PICTURE_TYPE_I)); data.extend_from_slice(&[0xFF; 4]); diff --git a/src/mux/disc.rs b/src/mux/disc.rs index aca0e75..1cb2acb 100644 --- a/src/mux/disc.rs +++ b/src/mux/disc.rs @@ -2,14 +2,30 @@ //! //! Read-only stream. Wraps DriveSession + Disc. //! 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 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::error::Error; +use crate::speed::DriveSpeed; use std::io::{self, Read, Write}; 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. #[derive(Default)] pub struct DiscOptions { @@ -21,18 +37,38 @@ pub struct DiscOptions { pub title_index: Option, } - /// 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 { disc_title: DiscTitle, disc: Disc, session: DriveSession, - title_index: usize, - // Read buffer: holds one batch from ContentReader + // Read buffer: holds one decoded batch batch_buf: Vec, batch_pos: usize, - started: bool, eof: bool, + + // ── Reading state (replaces ContentReader) ── + extents: Vec, + current_extent: usize, + current_offset: u32, + content_format: ContentFormat, + aacs: Option, + css: Option, + unit_key_idx: usize, + read_buf: Vec, + /// 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 { @@ -64,16 +100,36 @@ impl DiscStream { }); } 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 { disc_title, disc, session, - title_index, batch_buf: Vec::new(), batch_pos: 0, - started: 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 { &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 { + 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 { @@ -109,37 +314,24 @@ impl Read for DiscStream { return Ok(0); } - // Open reader on first call - if !self.started { - self.started = true; - } - - // 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) + // Fill the read buffer with the next batch of sectors + let has_data = self + .fill_buffer() .map_err(|e| io::Error::other(e.to_string()))?; - match reader.read_batch() { - 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; - Ok(0) - } - Err(e) => Err(io::Error::other(e.to_string())), + if !has_data { + self.eof = true; + return Ok(0); } + + // 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(()) } } + +#[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)); + } +} diff --git a/src/mux/isowriter.rs b/src/mux/isowriter.rs index f774def..b2df7f9 100644 --- a/src/mux/isowriter.rs +++ b/src/mux/isowriter.rs @@ -238,7 +238,7 @@ impl IsoWriter { // Main VDS extent_ad: {length, location} per UDF spec avdp[16..20].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length avdp[20..24].copy_from_slice(&VDS_START.to_le_bytes()); // location - // Reserve VDS extent_ad (same as main for simplicity) + // Reserve VDS extent_ad (same as main for simplicity) avdp[24..28].copy_from_slice(&(6u32 * 2048).to_le_bytes()); // length avdp[28..32].copy_from_slice(&VDS_START.to_le_bytes()); // location self.writer.write_all(&avdp)?; @@ -394,7 +394,12 @@ impl IsoWriter { // Allocation: data starts at DATA_START in the physical partition let data_offset = self.data_start_sector - PARTITION_START; // 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[220..224].copy_from_slice(&data_offset.to_le_bytes()); 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()); // Descriptor version: 3 (UDF 2.50) 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) buf[8..10].copy_from_slice(&0u16.to_le_bytes()); // Descriptor CRC length buf[10..12].copy_from_slice(&0u16.to_le_bytes()); // Tag location 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). @@ -520,11 +529,7 @@ mod tests { // VRS at sector 16 should contain "BEA01" let vrs = sector(&data, VRS_START); - assert_eq!( - &vrs[1..6], - b"BEA01", - "VRS sector 16 should contain BEA01" - ); + assert_eq!(&vrs[1..6], b"BEA01", "VRS sector 16 should contain BEA01"); // FSD at metadata sector should have tag ID = 256 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. // Search for "00042.m2ts" in the sector data let name = b"00042.m2ts"; - let found = stream_dir - .windows(name.len()) - .any(|w| w == name); + let found = stream_dir.windows(name.len()).any(|w| w == name); assert!( found, "STREAM directory should contain m2ts filename '00042.m2ts'" @@ -599,7 +602,11 @@ mod tests { // Should still have valid UDF structure // AVDP at sector 256 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 let vrs = sector(&data, VRS_START); diff --git a/src/mux/meta.rs b/src/mux/meta.rs index 051f20d..6f1591c 100644 --- a/src/mux/meta.rs +++ b/src/mux/meta.rs @@ -189,7 +189,7 @@ impl M2tsMeta { /// Write the metadata header to a writer. Padded to 192-byte boundary. 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 raw_len = 8 + 4 + json.len(); // magic + len + json diff --git a/src/mux/mkv.rs b/src/mux/mkv.rs index d9ce470..4266f12 100644 --- a/src/mux/mkv.rs +++ b/src/mux/mkv.rs @@ -578,9 +578,7 @@ mod tests { let writer = SharedWriter(shared.clone()); let tracks = [make_video_track()]; let mut muxer = MkvMuxer::new(writer, &tracks, Some("Cue Test"), 60.0).unwrap(); - muxer - .write_frame(0, 0, true, &[0x01, 0x02, 0x03]) - .unwrap(); + muxer.write_frame(0, 0, true, &[0x01, 0x02, 0x03]).unwrap(); muxer.finish().unwrap(); let data = shared.lock().unwrap().clone().into_inner(); @@ -596,12 +594,8 @@ mod tests { let tracks = [make_video_track(), make_audio_track()]; let mut muxer = MkvMuxer::new(buf, &tracks, Some("Multi"), 120.0).unwrap(); // Write frames to both tracks - muxer - .write_frame(0, 0, true, &[0x00, 0x00, 0x01]) - .unwrap(); - muxer - .write_frame(1, 0, false, &[0x0B, 0x77, 0x00]) - .unwrap(); + muxer.write_frame(0, 0, true, &[0x00, 0x00, 0x01]).unwrap(); + muxer.write_frame(1, 0, false, &[0x0B, 0x77, 0x00]).unwrap(); muxer .write_frame(0, 40_000_000, false, &[0x00, 0x00, 0x01]) .unwrap(); @@ -621,14 +615,10 @@ mod tests { // Record position before first frame let pos_before_kf = muxer.writer.position(); - muxer - .write_frame(0, 0, true, &[0xAA]) - .unwrap(); + muxer.write_frame(0, 0, true, &[0xAA]).unwrap(); let pos_after_kf = muxer.writer.position(); - muxer - .write_frame(0, 1_000_000, false, &[0xBB]) - .unwrap(); + muxer.write_frame(0, 1_000_000, false, &[0xBB]).unwrap(); let pos_after_nkf = muxer.writer.position(); let data = muxer.writer.into_inner(); @@ -653,7 +643,7 @@ mod tests { // Track VINT: 1 byte (track 1 = 0x81) let track_vint_pos = after_id + size_len; let track_vint_len = 1; // track 1 encoded as 0x81 - // 2-byte relative timestamp + // 2-byte relative timestamp let ts_pos = track_vint_pos + track_vint_len; // flags byte let flags_pos = ts_pos + 2; @@ -682,13 +672,22 @@ mod tests { let buf = Cursor::new(Vec::new()); let tracks = [make_video_track()]; let chapters = vec![ - Chapter { time_secs: 0.0, name: "Chapter 1".into() }, - Chapter { time_secs: 300.0, name: "Chapter 2".into() }, - Chapter { time_secs: 600.0, name: "Chapter 3".into() }, + Chapter { + time_secs: 0.0, + 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( - buf, &tracks, Some("Chapter Test"), 900.0, &chapters, - ).unwrap(); + let muxer = + MkvMuxer::new_with_chapters(buf, &tracks, Some("Chapter Test"), 900.0, &chapters) + .unwrap(); let data = muxer.writer.into_inner(); // Chapters element ID: 0x1043A770 @@ -742,7 +741,10 @@ mod tests { let count = data.windows(1).filter(|w| w[0] == 0x88).count(); // 0x88 appears as FlagDefault + as TrackType (also 0x83... no, 0x83 != 0x88) // 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] diff --git a/src/mux/mkvstream.rs b/src/mux/mkvstream.rs index f577ad7..d98b79a 100644 --- a/src/mux/mkvstream.rs +++ b/src/mux/mkvstream.rs @@ -381,7 +381,10 @@ fn parse_mkv_header(r: &mut (impl Read + Seek)) -> io::Result { return Err(io::Error::new(io::ErrorKind::InvalidData, "not EBML")); } 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))?; diff --git a/src/mux/mod.rs b/src/mux/mod.rs index 90a8088..4c8d9a0 100644 --- a/src/mux/mod.rs +++ b/src/mux/mod.rs @@ -31,9 +31,9 @@ pub mod mkv; mod mkvstream; pub mod network; pub mod null; +pub mod ps; pub mod resolve; pub mod stdio; -pub mod ps; pub mod ts; pub use disc::{DiscOptions, DiscStream}; diff --git a/src/mux/ps.rs b/src/mux/ps.rs index cddd812..ed718b4 100644 --- a/src/mux/ps.rs +++ b/src/mux/ps.rs @@ -118,8 +118,8 @@ impl PsDemuxer { if sc + 6 > self.buffer.len() { break; } - let header_len = ((self.buffer[sc + 4] as usize) << 8) - | self.buffer[sc + 5] as usize; + let header_len = + ((self.buffer[sc + 4] as usize) << 8) | self.buffer[sc + 5] as usize; let total = 6 + header_len; if sc + total > self.buffer.len() { break; @@ -131,8 +131,8 @@ impl PsDemuxer { if sc + 6 > self.buffer.len() { break; } - let pes_packet_len = ((self.buffer[sc + 4] as usize) << 8) - | self.buffer[sc + 5] as usize; + let pes_packet_len = + ((self.buffer[sc + 4] as usize) << 8) | self.buffer[sc + 5] as usize; // Total bytes = 6 (start code + stream_id + length) + pes_packet_len. // 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, // private stream 2: 0xBF, padding: 0xBE, ECM/EMM etc. // 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. @@ -267,11 +267,7 @@ fn parse_pts(buf: &[u8]) -> u64 { let b3 = buf[3] as u64; let b4 = buf[4] as u64; - ((b0 >> 1) & 0x07) << 30 - | b1 << 22 - | (b2 >> 1) << 15 - | b3 << 7 - | b4 >> 1 + ((b0 >> 1) & 0x07) << 30 | b1 << 22 | (b2 >> 1) << 15 | b3 << 7 | b4 >> 1 } /// 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 let mut data = vec![ - 0x00, 0x00, 0x01, 0xBA, - 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, - 0x01, 0x89, 0xC3, + 0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x01, 0x89, 0xC3, 0xFB, // stuffing_length = 3 0xFF, 0xFF, 0xFF, // stuffing bytes ]; @@ -391,11 +385,10 @@ mod tests { let mut demuxer = PsDemuxer::new(); let pts_bytes = encode_pts(180000, 0x30); // PTS marker = 0x30 - let dts_bytes = encode_pts(90000, 0x10); // DTS marker = 0x10 + let dts_bytes = encode_pts(90000, 0x10); // DTS marker = 0x10 let mut data = vec![ - 0x00, 0x00, 0x01, 0xE0, - 0x00, 0x11, // length = 17 + 0x00, 0x00, 0x01, 0xE0, 0x00, 0x11, // length = 17 0x80, 0xC0, 0x0A, // flags: PTS+DTS, header_data_len=10 ]; data.extend_from_slice(&pts_bytes); @@ -438,10 +431,8 @@ mod tests { let mut demuxer = PsDemuxer::new(); let mut data = vec![ - 0x00, 0x00, 0x01, 0xBD, - 0x00, 0x06, // length = 6 - 0x80, 0x00, 0x00, - 0x88, // sub-stream ID: DTS stream 0 + 0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, // length = 6 + 0x80, 0x00, 0x00, 0x88, // sub-stream ID: DTS stream 0 0x11, 0x22, ]; data.extend_from_slice(&[0x00, 0x00, 0x01, 0xB9]); @@ -456,9 +447,7 @@ mod tests { let mut demuxer = PsDemuxer::new(); let mut data = vec![ - 0x00, 0x00, 0x01, 0xBD, - 0x00, 0x06, - 0x80, 0x00, 0x00, + 0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, 0x80, 0x00, 0x00, 0x20, // sub-stream ID: subtitle stream 0 0xFF, 0xFE, ]; @@ -474,9 +463,7 @@ mod tests { let mut demuxer = PsDemuxer::new(); let mut data = vec![ - 0x00, 0x00, 0x01, 0xBD, - 0x00, 0x06, - 0x80, 0x00, 0x00, + 0x00, 0x00, 0x01, 0xBD, 0x00, 0x06, 0x80, 0x00, 0x00, 0xA0, // sub-stream ID: LPCM stream 0 0x01, 0x02, ]; @@ -494,8 +481,7 @@ mod tests { let mut demuxer = PsDemuxer::new(); let mut full = vec![ - 0x00, 0x00, 0x01, 0xE0, - 0x00, 0x06, // length = 6 + 0x00, 0x00, 0x01, 0xE0, 0x00, 0x06, // length = 6 0x80, 0x00, 0x00, // no PTS, header_data_len=0 0xAA, 0xBB, 0xCC, ]; @@ -521,18 +507,12 @@ mod tests { // First PES: video data.extend_from_slice(&[ - 0x00, 0x00, 0x01, 0xE0, - 0x00, 0x05, - 0x80, 0x00, 0x00, - 0x11, 0x22, + 0x00, 0x00, 0x01, 0xE0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x11, 0x22, ]); // Second PES: audio data.extend_from_slice(&[ - 0x00, 0x00, 0x01, 0xC0, - 0x00, 0x05, - 0x80, 0x00, 0x00, - 0x33, 0x44, + 0x00, 0x00, 0x01, 0xC0, 0x00, 0x05, 0x80, 0x00, 0x00, 0x33, 0x44, ]); // Delimiter diff --git a/src/mux/resolve.rs b/src/mux/resolve.rs index 3fb0365..79c2e36 100644 --- a/src/mux/resolve.rs +++ b/src/mux/resolve.rs @@ -255,4 +255,3 @@ pub struct InputOptions { pub keydb_path: Option, pub title_index: Option, } - diff --git a/src/udf.rs b/src/udf.rs index 003ec82..76e34cf 100644 --- a/src/udf.rs +++ b/src/udf.rs @@ -211,7 +211,7 @@ impl UdfFs { if let Ok((data_lba, data_len)) = self.read_icb_extent(reader, child.meta_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)); } } @@ -256,7 +256,9 @@ impl UdfFs { let l_ad = u32::from_le_bytes([icb[212], icb[213], icb[214], icb[215]]) as usize; let ad_offset = 216 + l_ea; 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) } @@ -266,7 +268,9 @@ impl UdfFs { let l_ad = u32::from_le_bytes([icb[172], icb[173], icb[174], icb[175]]) as usize; let ad_offset = 176 + l_ea; 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) } @@ -442,7 +446,9 @@ pub fn read_filesystem(reader: &mut dyn SectorReader) -> Result { ]) as usize; let ad_off = 216 + l_ea; 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([ meta_icb[ad_off], @@ -491,7 +497,7 @@ pub fn read_filesystem(reader: &mut dyn SectorReader) -> Result { // Step 5: Read root directory and build file tree 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 { 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 ad_off = 216 + l_ea; 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([ 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 ad_off = 176 + l_ea; 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([ icb[ad_off], @@ -651,7 +661,7 @@ fn read_directory( } // 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; } diff --git a/tests/crypto_tests.rs b/tests/crypto_tests.rs index 1dbbc11..1511b68 100644 --- a/tests/crypto_tests.rs +++ b/tests/crypto_tests.rs @@ -26,7 +26,7 @@ fn css_descramble_sector_roundtrip_via_public_api() { let mut sector = vec![0x00u8; 2048]; sector[0x14] = 0x30; // scramble flag sector[0x54..0x59].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]); // seed - // PES header at byte 128 + // PES header at byte 128 sector[0x80] = 0x00; sector[0x81] = 0x00; sector[0x82] = 0x01; @@ -40,7 +40,11 @@ fn css_descramble_sector_roundtrip_via_public_api() { // First descramble css::descramble_sector(&state, &mut sector); 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 sector[0x14] = 0x30; @@ -58,7 +62,10 @@ fn css_descramble_sector_roundtrip_via_public_api() { #[test] fn css_is_scrambled_detection() { 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 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"); 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 - 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 ─────────────────────────────────────────────────── @@ -87,8 +100,8 @@ fn aacs_decrypt_unit_roundtrip() { let unit_key = [0xAAu8; 16]; let aacs_iv: [u8; 16] = [ - 0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, - 0x0F, 0x78, + 0x0B, 0xA0, 0xF8, 0xDD, 0xFE, 0xA6, 0x1F, 0xB3, 0xD8, 0xDF, 0x9F, 0x56, 0x6A, 0x05, 0x0F, + 0x78, ]; // 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 let result = aacs::decrypt_unit(&mut plain, &unit_key); - assert!(result, "decrypt_unit should return true on valid encrypted unit"); - assert!(!aacs::is_unit_encrypted(&plain), "encryption flag should be cleared"); + assert!( + 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) let mut sync_count = 0; @@ -166,7 +185,11 @@ fn aacs_decrypt_unit_roundtrip() { "decrypted unit body does not match original" ); // 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 @@ -182,7 +205,10 @@ fn aacs_disc_hash_deterministic() { assert_eq!(hash1a, hash1b, "disc_hash not deterministic on same input"); 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 assert_eq!(hash1a.len(), 20); @@ -190,7 +216,11 @@ fn aacs_disc_hash_deterministic() { // Verify disc_hash_hex formatting let hex = aacs::disc_hash_hex(&hash1a); 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 @@ -203,12 +233,12 @@ fn aacs_decrypt_unit_key_roundtrip() { use aes::Aes128; let vuk = [ - 0x11u8, 0x14, 0x36, 0x0B, 0x10, 0xEE, 0x6E, 0xAC, 0x78, 0xAA, 0x4A, 0xC0, 0xB7, 0x52, - 0xEA, 0xEB, + 0x11u8, 0x14, 0x36, 0x0B, 0x10, 0xEE, 0x6E, 0xAC, 0x78, 0xAA, 0x4A, 0xC0, 0xB7, 0x52, 0xEA, + 0xEB, ]; let original_unit_key = [ - 0x9E, 0x5D, 0x13, 0x10, 0x33, 0x74, 0x43, 0xE8, 0x11, 0xA5, 0x2E, 0xBB, 0xEA, 0xE0, - 0x47, 0x0F, + 0x9E, 0x5D, 0x13, 0x10, 0x33, 0x74, 0x43, 0xE8, 0x11, 0xA5, 0x2E, 0xBB, 0xEA, 0xE0, 0x47, + 0x0F, ]; // 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 #[test] fn aacs_vuk_derivation_roundtrip() { - let media_key = [0x25u8, 0x2F, 0xB6, 0x36, 0xE8, 0x83, 0x52, 0x9E, - 0x11, 0x9A, 0xB7, 0x15, 0xF4, 0xEB, 0x16, 0x40]; - let volume_id = [0xA1u8, 0x3C, 0xBE, 0x2C, 0xE4, 0x05, 0x65, 0xD1, - 0x04, 0xB5, 0x3E, 0x76, 0x8C, 0x70, 0x0E, 0x30]; + let media_key = [ + 0x25u8, 0x2F, 0xB6, 0x36, 0xE8, 0x83, 0x52, 0x9E, 0x11, 0x9A, 0xB7, 0x15, 0xF4, 0xEB, 0x16, + 0x40, + ]; + 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); @@ -253,7 +287,10 @@ fn aacs_vuk_derivation_roundtrip() { fn aacs_is_unit_encrypted_detection() { 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 assert!(aacs::is_unit_encrypted(&unit)); @@ -269,7 +306,10 @@ fn aacs_is_unit_encrypted_detection() { // Too short 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 @@ -299,7 +339,7 @@ fn aacs_parse_unit_key_ro_minimal() { data[0..4].copy_from_slice(&uk_pos.to_be_bytes()); // app_type data[16] = 1; // BD-ROM - // num_bdmv_dir + // num_bdmv_dir data[17] = 1; // flags data[18] = 0; @@ -316,7 +356,10 @@ fn aacs_parse_unit_key_ro_minimal() { } 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(); assert_eq!(ukf.app_type, 1); diff --git a/tests/disc_tests.rs b/tests/disc_tests.rs index 87535c1..74577f9 100644 --- a/tests/disc_tests.rs +++ b/tests/disc_tests.rs @@ -295,7 +295,9 @@ fn build_minimal_udf(reader: &mut MockSectorReader) { let partition_start: u32 = 512; reader.sectors.insert(256, make_avdp_sector(32)); 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(35, make_terminator()); @@ -316,7 +318,9 @@ fn build_udf_with_aacs_dir(reader: &mut MockSectorReader) { let partition_start: u32 = 512; reader.sectors.insert(256, make_avdp_sector(32)); 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(35, make_terminator()); @@ -357,7 +361,10 @@ fn resolve_encryption_no_aacs_dir() { let opts = ScanOptions::default(); 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"); }