v0.7.1: SectorReader trait, IsoStream, StdioStream, resolve_encryption
- SectorReader trait decouples disc scanning from SCSI - Disc::scan_image() for ISO and any sector source - resolve_encryption() handles AACS 1.0/2.0/none in one path - IsoStream: full UDF/MPLS/CLPI/labels pipeline from ISO files - StdioStream: stdin/stdout pipe - Strict scheme:// URL format with validation - Labels module refactored to SectorReader - 7 stream types total
This commit is contained in:
+124
-141
@@ -1,54 +1,117 @@
|
||||
//! IsoStream — read BD-TS data from a Blu-ray ISO image file.
|
||||
//! IsoStream — read/write Blu-ray ISO disc images.
|
||||
//!
|
||||
//! Read-only. Parses the UDF filesystem inside the ISO to find
|
||||
//! BDMV/STREAM/*.m2ts files, then streams the BD-TS bytes.
|
||||
//! Read: parses UDF filesystem inside the ISO using the same pipeline as
|
||||
//! DiscStream (titles, streams, labels, AACS). An ISO is a flat image of
|
||||
//! 2048-byte sectors — sector N starts at byte offset N * 2048.
|
||||
//!
|
||||
//! An ISO file is a flat image of 2048-byte sectors — the same
|
||||
//! layout as on a real disc. Sector N starts at byte offset N * 2048.
|
||||
//! Write: creates a sector-by-sector disc image from a SectorReader source.
|
||||
|
||||
use std::io::{self, Read, Write, Seek, SeekFrom};
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
use super::IOStream;
|
||||
use crate::disc::DiscTitle;
|
||||
use crate::disc::{Disc, DiscTitle, ScanOptions};
|
||||
use crate::sector::SectorReader;
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
const SECTOR_SIZE: u64 = 2048;
|
||||
|
||||
/// Blu-ray ISO image stream. Read-only.
|
||||
/// File-backed sector reader for ISO images.
|
||||
pub struct IsoSectorReader {
|
||||
file: File,
|
||||
capacity: u32,
|
||||
}
|
||||
|
||||
impl IsoSectorReader {
|
||||
pub fn open(path: &str) -> io::Result<Self> {
|
||||
let file = File::open(Path::new(path))
|
||||
.map_err(|e| io::Error::new(e.kind(), format!("iso://{}: {}", path, e)))?;
|
||||
let size = file.metadata()?.len();
|
||||
let capacity = (size / SECTOR_SIZE) as u32;
|
||||
Ok(Self { file, capacity })
|
||||
}
|
||||
|
||||
pub fn capacity(&self) -> u32 { self.capacity }
|
||||
}
|
||||
|
||||
impl SectorReader for IsoSectorReader {
|
||||
fn read_sectors(&mut self, lba: u32, count: u16, buf: &mut [u8]) -> Result<usize> {
|
||||
let bytes = count as usize * SECTOR_SIZE as usize;
|
||||
self.file.seek(SeekFrom::Start(lba as u64 * SECTOR_SIZE))
|
||||
.map_err(|e| Error::IoError { source: e })?;
|
||||
self.file.read_exact(&mut buf[..bytes])
|
||||
.map_err(|e| Error::IoError { source: e })?;
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// Blu-ray ISO image stream.
|
||||
///
|
||||
/// Opens an ISO file, parses UDF to locate BDMV playlists and streams,
|
||||
/// then reads the m2ts content sectors in order.
|
||||
/// Read: opens ISO, parses UDF (same as DiscStream), streams BD-TS content.
|
||||
/// Write: receives sector data and writes to ISO file.
|
||||
pub struct IsoStream {
|
||||
disc_title: DiscTitle,
|
||||
file: File,
|
||||
disc: Option<Disc>,
|
||||
reader: Option<IsoSectorReader>,
|
||||
writer: Option<io::BufWriter<File>>,
|
||||
/// Sector ranges to read: (start_lba, sector_count)
|
||||
extents: Vec<(u64, u64)>,
|
||||
/// Current extent index
|
||||
extents: Vec<(u32, u32)>,
|
||||
extent_idx: usize,
|
||||
/// Sectors remaining in current extent
|
||||
sectors_remaining: u64,
|
||||
/// Read buffer for one sector
|
||||
sectors_remaining: u32,
|
||||
sector_buf: [u8; SECTOR_SIZE as usize],
|
||||
/// Position within current sector buffer
|
||||
buf_pos: usize,
|
||||
/// Bytes valid in sector buffer
|
||||
buf_len: usize,
|
||||
eof: bool,
|
||||
}
|
||||
|
||||
impl IsoStream {
|
||||
/// Open an ISO file and scan its contents.
|
||||
///
|
||||
/// Parses UDF filesystem, finds playlists and stream extents.
|
||||
/// The title_index selects which title to read (0-based, default: longest).
|
||||
pub fn open(path: &str, title_index: Option<usize>) -> io::Result<Self> {
|
||||
let file = File::open(Path::new(path))
|
||||
.map_err(|e| io::Error::new(e.kind(),
|
||||
format!("iso://{}: {}", path, e)))?;
|
||||
/// Open an ISO file for reading. Parses UDF, scans titles, streams, labels.
|
||||
pub fn open(path: &str, title_index: Option<usize>, opts: &ScanOptions) -> io::Result<Self> {
|
||||
let mut reader = IsoSectorReader::open(path)?;
|
||||
let capacity = reader.capacity();
|
||||
|
||||
let mut stream = IsoStream {
|
||||
let disc = Disc::scan_image(&mut reader, capacity, opts)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
|
||||
let idx = title_index.unwrap_or(0).min(disc.titles.len().saturating_sub(1));
|
||||
let disc_title = if disc.titles.is_empty() {
|
||||
return Err(io::Error::new(io::ErrorKind::NotFound, "no titles found in ISO image"));
|
||||
} else {
|
||||
disc.titles[idx].clone()
|
||||
};
|
||||
|
||||
let extents: Vec<(u32, u32)> = disc_title.extents.iter()
|
||||
.map(|e| (e.start_lba, e.sector_count))
|
||||
.collect();
|
||||
|
||||
let sectors_remaining = extents.first().map(|e| e.1).unwrap_or(0);
|
||||
|
||||
Ok(IsoStream {
|
||||
disc_title,
|
||||
disc: Some(disc),
|
||||
reader: Some(reader),
|
||||
writer: None,
|
||||
extents,
|
||||
extent_idx: 0,
|
||||
sectors_remaining,
|
||||
sector_buf: [0u8; SECTOR_SIZE as usize],
|
||||
buf_pos: 0,
|
||||
buf_len: 0,
|
||||
eof: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an ISO file for writing. Receives raw sector data.
|
||||
pub fn create(path: &str) -> io::Result<Self> {
|
||||
let file = File::create(Path::new(path))
|
||||
.map_err(|e| io::Error::new(e.kind(), format!("iso://{}: {}", path, e)))?;
|
||||
let writer = io::BufWriter::with_capacity(4 * 1024 * 1024, file);
|
||||
|
||||
Ok(IsoStream {
|
||||
disc_title: DiscTitle::empty(),
|
||||
file,
|
||||
disc: None,
|
||||
reader: None,
|
||||
writer: Some(writer),
|
||||
extents: Vec::new(),
|
||||
extent_idx: 0,
|
||||
sectors_remaining: 0,
|
||||
@@ -56,128 +119,35 @@ impl IsoStream {
|
||||
buf_pos: 0,
|
||||
buf_len: 0,
|
||||
eof: false,
|
||||
};
|
||||
|
||||
stream.scan_iso(title_index)?;
|
||||
Ok(stream)
|
||||
})
|
||||
}
|
||||
|
||||
/// Scan the ISO: parse UDF, build title metadata, extract extent map.
|
||||
fn scan_iso(&mut self, title_index: Option<usize>) -> io::Result<()> {
|
||||
// Read AVDP at sector 256 to verify this is a UDF disc image
|
||||
let avdp = self.read_sector(256)?;
|
||||
let tag_id = u16::from_le_bytes([avdp[0], avdp[1]]);
|
||||
if tag_id != 2 {
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData,
|
||||
"not a valid UDF image — no AVDP at sector 256"));
|
||||
}
|
||||
|
||||
// For now, scan BDMV/PLAYLIST and BDMV/STREAM directories
|
||||
// by searching for MPLS and M2TS markers in the UDF metadata.
|
||||
//
|
||||
// Full UDF parsing (AVDP → VDS → metadata → FSD → root → files)
|
||||
// will be refactored out of udf.rs to work with both DriveSession
|
||||
// and file-backed sector reads. For now, find the main m2ts file
|
||||
// by scanning for the stream file extents in the UDF file entries.
|
||||
|
||||
// Find all .m2ts file extents from UDF metadata
|
||||
let disc_size = self.file.seek(SeekFrom::End(0))?;
|
||||
let total_sectors = disc_size / SECTOR_SIZE;
|
||||
self.file.seek(SeekFrom::Start(0))?;
|
||||
|
||||
// Scan UDF partition for BDMV structure
|
||||
let titles = self.find_stream_extents(total_sectors)?;
|
||||
|
||||
if titles.is_empty() {
|
||||
return Err(io::Error::new(io::ErrorKind::NotFound,
|
||||
"no BD stream files found in ISO image"));
|
||||
}
|
||||
|
||||
// Select title
|
||||
let idx = title_index.unwrap_or(0).min(titles.len() - 1);
|
||||
let (title, extents) = &titles[idx];
|
||||
|
||||
self.disc_title = title.clone();
|
||||
self.extents = extents.clone();
|
||||
|
||||
if !self.extents.is_empty() {
|
||||
self.sectors_remaining = self.extents[0].1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
/// Set metadata (for write mode).
|
||||
pub fn meta(mut self, dt: &DiscTitle) -> Self {
|
||||
self.disc_title = dt.clone();
|
||||
self
|
||||
}
|
||||
|
||||
/// Read a single sector from the ISO file.
|
||||
fn read_sector(&mut self, lba: u64) -> io::Result<Vec<u8>> {
|
||||
let mut buf = vec![0u8; SECTOR_SIZE as usize];
|
||||
self.file.seek(SeekFrom::Start(lba * SECTOR_SIZE))?;
|
||||
self.file.read_exact(&mut buf)?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Scan the ISO for BD stream file extents.
|
||||
///
|
||||
/// Returns: Vec of (DiscTitle, Vec<(start_lba, sector_count)>)
|
||||
///
|
||||
/// This is a simplified scanner that finds m2ts content by looking
|
||||
/// for 192-byte BD-TS packet boundaries (0x47 sync byte at offset 4).
|
||||
/// Full UDF parsing will replace this once udf.rs is decoupled from DriveSession.
|
||||
fn find_stream_extents(&mut self, total_sectors: u64) -> io::Result<Vec<(DiscTitle, Vec<(u64, u64)>)>> {
|
||||
// Strategy: scan the UDF file entry area for allocation descriptors
|
||||
// pointing to large contiguous regions (m2ts files are large).
|
||||
//
|
||||
// For a BD-ROM ISO, the main m2ts typically starts after the BDMV
|
||||
// metadata (around sector 1000-5000) and runs contiguously to the end.
|
||||
//
|
||||
// Quick approach: find first sector with BD-TS sync (0x47 at byte 4)
|
||||
// and treat everything from there to the end as one extent.
|
||||
|
||||
let probe_start = 256u64; // skip lead-in
|
||||
let probe_end = total_sectors.min(10000); // probe first 20 MB
|
||||
|
||||
let mut stream_start: Option<u64> = None;
|
||||
|
||||
for lba in probe_start..probe_end {
|
||||
let sector = self.read_sector(lba)?;
|
||||
// BD-TS: 192-byte packets, sync byte 0x47 at offset 4 of each packet
|
||||
// A sector (2048 bytes) holds partial packets, but the sync pattern
|
||||
// should appear at regular intervals
|
||||
if sector.len() >= 196 && sector[4] == 0x47 {
|
||||
// Verify: check for another sync at offset 196 (4 + 192)
|
||||
if sector[196] == 0x47 {
|
||||
stream_start = Some(lba);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match stream_start {
|
||||
Some(start) => {
|
||||
let sector_count = total_sectors - start;
|
||||
let size_bytes = sector_count * SECTOR_SIZE;
|
||||
|
||||
let mut title = DiscTitle::empty();
|
||||
title.playlist = "Main Title".into();
|
||||
title.size_bytes = size_bytes;
|
||||
|
||||
Ok(vec![(title, vec![(start, sector_count)])])
|
||||
}
|
||||
None => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
/// Get the full Disc (for listing all titles).
|
||||
pub fn disc(&self) -> Option<&Disc> { self.disc.as_ref() }
|
||||
|
||||
/// Read the next sector from the current extent.
|
||||
fn read_next_sector(&mut self) -> io::Result<bool> {
|
||||
let reader = match self.reader.as_mut() {
|
||||
Some(r) => r,
|
||||
None => return Ok(false),
|
||||
};
|
||||
|
||||
if self.extent_idx >= self.extents.len() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let (start_lba, _) = self.extents[self.extent_idx];
|
||||
let offset = self.extents[self.extent_idx].1 - self.sectors_remaining;
|
||||
let (start_lba, total) = self.extents[self.extent_idx];
|
||||
let offset = total - self.sectors_remaining;
|
||||
let lba = start_lba + offset;
|
||||
|
||||
self.file.seek(SeekFrom::Start(lba * SECTOR_SIZE))?;
|
||||
self.file.read_exact(&mut self.sector_buf)?;
|
||||
reader.read_sectors(lba, 1, &mut self.sector_buf)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
self.buf_pos = 0;
|
||||
self.buf_len = SECTOR_SIZE as usize;
|
||||
|
||||
@@ -195,7 +165,12 @@ impl IsoStream {
|
||||
|
||||
impl IOStream for IsoStream {
|
||||
fn info(&self) -> &DiscTitle { &self.disc_title }
|
||||
fn finish(&mut self) -> io::Result<()> { Ok(()) }
|
||||
fn finish(&mut self) -> io::Result<()> {
|
||||
if let Some(ref mut w) = self.writer {
|
||||
w.flush()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for IsoStream {
|
||||
@@ -224,9 +199,17 @@ impl Read for IsoStream {
|
||||
}
|
||||
|
||||
impl Write for IsoStream {
|
||||
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
|
||||
Err(io::Error::new(io::ErrorKind::Unsupported,
|
||||
"iso:// is read-only"))
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self.writer.as_mut() {
|
||||
Some(w) => w.write(buf),
|
||||
None => Err(io::Error::new(io::ErrorKind::Unsupported,
|
||||
"iso:// opened for reading — cannot write")),
|
||||
}
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self.writer.as_mut() {
|
||||
Some(w) => w.flush(),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> { Ok(()) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user