Audit v3 fixes: all 3 tiers (19 findings)

Tier 1 (compilation + correctness):
- Fix nightly-only is_multiple_of → % 2 != 0 (stable Rust compat)
- Fix parse_sample_rate: check 192 before 96 (was returning wrong rate)
- macOS drive discovery: split unix.rs → linux.rs + macos.rs
- Linux: EACCES returns DevicePermission not DeviceNotFound
- CLI pipe.rs: Ctrl+C signal handler added

Tier 2 (correctness + security):
- MkvStream: reset demuxer after scanning→streaming transition
- Windows SPTI: zero data buffer before ioctl
- AACS cert verification: documented why silently skipped
- KEYDB: HOME + USERPROFILE fallback for Windows
- Library modules: pub(crate) for internal modules
- AACS: explicit re-exports, AES primitives pub(crate)

Tier 3 (performance + polish):
- IsoStream: batch 64-sector reads (was 1 sector at a time)
- DiscStream: buffer swap instead of copy in decrypt_and_buffer
- Vec capacity hints in TS/PS demuxer hot paths
- NetworkStream: TLS warning documented
- Batch rip: per-title progress display
- cargo fmt: 0 violations

319 tests, 0 fmt violations.
This commit is contained in:
MattJackson
2026-04-11 19:24:25 +00:00
parent 43f81e4eae
commit a74d395f68
22 changed files with 174 additions and 73 deletions
+9 -13
View File
@@ -9,9 +9,8 @@
use super::IOStream;
use crate::disc::{
ContentFormat, Disc, DiscTitle, Extent,
MIN_BATCH_SECTORS, RAMP_BATCH_AFTER, RAMP_SPEED_AFTER,
SLOW_SPEED_AFTER, detect_max_batch_sectors,
detect_max_batch_sectors, ContentFormat, Disc, DiscTitle, Extent, MIN_BATCH_SECTORS,
RAMP_BATCH_AFTER, RAMP_SPEED_AFTER, SLOW_SPEED_AFTER,
};
use crate::drive::DriveSession;
use crate::error::Error;
@@ -187,8 +186,7 @@ impl DiscStream {
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.batch_sectors = (self.batch_sectors * 2).min(self.max_batch_sectors);
self.ok_streak = 0;
}
@@ -220,14 +218,12 @@ impl DiscStream {
}
if self.batch_sectors > MIN_BATCH_SECTORS {
self.batch_sectors =
(self.batch_sectors / 2).max(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);
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;
@@ -243,8 +239,7 @@ impl DiscStream {
self.current_extent += 1;
self.current_offset = 0;
}
self.read_buf
.resize(crate::aacs::ALIGNED_UNIT_LEN, 0);
self.read_buf.resize(crate::aacs::ALIGNED_UNIT_LEN, 0);
self.read_buf.fill(0);
return Ok(true);
}
@@ -283,8 +278,9 @@ impl DiscStream {
}
// No encryption: read_buf is already plaintext
self.batch_buf.clear();
self.batch_buf.extend_from_slice(&self.read_buf[..total_bytes]);
// Swap buffers instead of copying — the old batch_buf becomes
// read_buf and will be overwritten on the next read.
std::mem::swap(&mut self.batch_buf, &mut self.read_buf);
self.batch_pos = 0;
}
}
+18 -10
View File
@@ -18,6 +18,9 @@ use std::path::Path;
const SECTOR_SIZE: u64 = 2048;
/// Maximum sectors to batch-read at once (64 sectors = 128 KB).
const BATCH_SECTORS: usize = 64;
/// File-backed sector reader for ISO images.
pub struct IsoSectorReader {
file: File,
@@ -63,7 +66,8 @@ pub struct IsoStream {
extents: Vec<(u32, u32)>,
extent_idx: usize,
sectors_remaining: u32,
sector_buf: [u8; SECTOR_SIZE as usize],
/// Batch buffer: holds up to BATCH_SECTORS sectors (128 KB) at once.
batch_buf: Vec<u8>,
buf_pos: usize,
buf_len: usize,
eof: bool,
@@ -107,7 +111,7 @@ impl IsoStream {
extents,
extent_idx: 0,
sectors_remaining,
sector_buf: [0u8; SECTOR_SIZE as usize],
batch_buf: vec![0u8; BATCH_SECTORS * SECTOR_SIZE as usize],
buf_pos: 0,
buf_len: 0,
eof: false,
@@ -130,7 +134,7 @@ impl IsoStream {
extents: Vec::new(),
extent_idx: 0,
sectors_remaining: 0,
sector_buf: [0u8; SECTOR_SIZE as usize],
batch_buf: Vec::new(),
buf_pos: 0,
buf_len: 0,
eof: false,
@@ -163,7 +167,8 @@ impl IsoStream {
self.disc.as_ref()
}
fn read_next_sector(&mut self) -> io::Result<bool> {
/// Read up to BATCH_SECTORS sectors at once into the batch buffer.
fn read_next_batch(&mut self) -> io::Result<bool> {
let reader = match self.reader.as_mut() {
Some(r) => r,
None => return Ok(false),
@@ -177,13 +182,16 @@ impl IsoStream {
let offset = total - self.sectors_remaining;
let lba = start_lba + offset;
// Read up to BATCH_SECTORS, but no more than remaining in this extent
let count = (self.sectors_remaining as usize).min(BATCH_SECTORS) as u16;
reader
.read_sectors(lba, 1, &mut self.sector_buf)
.read_sectors(lba, count, &mut self.batch_buf)
.map_err(|e| io::Error::other(e.to_string()))?;
self.buf_pos = 0;
self.buf_len = SECTOR_SIZE as usize;
self.buf_len = count as usize * SECTOR_SIZE as usize;
self.sectors_remaining -= 1;
self.sectors_remaining -= count as u32;
if self.sectors_remaining == 0 {
self.extent_idx += 1;
if self.extent_idx < self.extents.len() {
@@ -223,14 +231,14 @@ impl Read for IsoStream {
if self.buf_pos < self.buf_len {
let n = (self.buf_len - self.buf_pos).min(buf.len());
buf[..n].copy_from_slice(&self.sector_buf[self.buf_pos..self.buf_pos + n]);
buf[..n].copy_from_slice(&self.batch_buf[self.buf_pos..self.buf_pos + n]);
self.buf_pos += n;
return Ok(n);
}
if self.read_next_sector()? {
if self.read_next_batch()? {
let n = self.buf_len.min(buf.len());
buf[..n].copy_from_slice(&self.sector_buf[..n]);
buf[..n].copy_from_slice(&self.batch_buf[..n]);
self.buf_pos = n;
Ok(n)
} else {
+3 -3
View File
@@ -431,10 +431,10 @@ fn parse_resolution(s: &str) -> (u32, u32) {
}
fn parse_sample_rate(s: &str) -> f64 {
if s.contains("96") {
96000.0
} else if s.contains("192") {
if s.contains("192") {
192000.0
} else if s.contains("96") {
96000.0
} else {
48000.0
}
+5 -2
View File
@@ -333,10 +333,12 @@ fn begin_streaming(ws: &mut WriteState, dt: &DiscTitle) -> io::Result<()> {
)?);
ws.phase = WritePhase::Streaming;
// Re-parse buffered data through a fresh demuxer
// Re-parse buffered data through a fresh demuxer, then reset the main
// demuxer so stale PES assembler state from scanning doesn't cause
// duplicate or incomplete packets during streaming.
let pids: Vec<u16> = ws.pid_to_track.iter().map(|(pid, _)| *pid).collect();
let buffered = ws.lookahead.drain();
if !buffered.is_empty() {
let pids: Vec<u16> = ws.pid_to_track.iter().map(|(pid, _)| *pid).collect();
let mut temp = TsDemuxer::new(&pids);
let packets = temp.feed(&buffered);
if let Some(ref mut muxer) = ws.muxer {
@@ -345,6 +347,7 @@ fn begin_streaming(ws: &mut WriteState, dt: &DiscTitle) -> io::Result<()> {
}
}
}
ws.demuxer = TsDemuxer::new(&pids);
Ok(())
}
+3
View File
@@ -1,5 +1,8 @@
//! NetworkStream — BD-TS over TCP with embedded metadata.
//!
//! **Security:** Data is transmitted over plain TCP with no encryption.
//! Use only on trusted networks (LAN). TLS support is planned.
//!
//! Write side (sender): connects to a listener, sends FMKV header + BD-TS data.
//! Read side (receiver): listens for a connection, reads FMKV header + BD-TS data.
//!
+1 -1
View File
@@ -78,7 +78,7 @@ impl PsDemuxer {
/// Scan the buffer for complete start-code-delimited units and parse them.
fn extract_packets(&mut self) -> Vec<PsPacket> {
let mut packets = Vec::new();
let mut packets = Vec::with_capacity(4);
let mut pos = 0;
loop {
+1 -1
View File
@@ -122,7 +122,7 @@ impl TsDemuxer {
/// Feed a chunk of BD transport stream data. Handles non-192-byte-aligned input
/// by buffering leftover bytes between calls. Returns completed PES packets.
pub fn feed(&mut self, data: &[u8]) -> Vec<PesPacket> {
let mut completed = Vec::new();
let mut completed = Vec::with_capacity(4);
// Prepend any remainder from previous call
let work: &[u8];