1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
//! IsoStream — read BD-TS data from a Blu-ray ISO image file.
//!
//! Read-only. Parses the UDF filesystem inside the ISO to find
//! BDMV/STREAM/*.m2ts files, then streams the BD-TS bytes.
//!
//! 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.
use std::io::{self, Read, Write, Seek, SeekFrom};
use std::fs::File;
use std::path::Path;
use super::IOStream;
use crate::disc::DiscTitle;
const SECTOR_SIZE: u64 = 2048;
/// Blu-ray ISO image stream. Read-only.
///
/// Opens an ISO file, parses UDF to locate BDMV playlists and streams,
/// then reads the m2ts content sectors in order.
pub struct IsoStream {
disc_title: DiscTitle,
file: File,
/// Sector ranges to read: (start_lba, sector_count)
extents: Vec<(u64, u64)>,
/// Current extent index
extent_idx: usize,
/// Sectors remaining in current extent
sectors_remaining: u64,
/// Read buffer for one sector
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)))?;
let mut stream = IsoStream {
disc_title: DiscTitle::empty(),
file,
extents: Vec::new(),
extent_idx: 0,
sectors_remaining: 0,
sector_buf: [0u8; SECTOR_SIZE as usize],
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(())
}
/// 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()),
}
}
/// Read the next sector from the current extent.
fn read_next_sector(&mut self) -> io::Result<bool> {
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 lba = start_lba + offset;
self.file.seek(SeekFrom::Start(lba * SECTOR_SIZE))?;
self.file.read_exact(&mut self.sector_buf)?;
self.buf_pos = 0;
self.buf_len = SECTOR_SIZE as usize;
self.sectors_remaining -= 1;
if self.sectors_remaining == 0 {
self.extent_idx += 1;
if self.extent_idx < self.extents.len() {
self.sectors_remaining = self.extents[self.extent_idx].1;
}
}
Ok(true)
}
}
impl IOStream for IsoStream {
fn info(&self) -> &DiscTitle { &self.disc_title }
fn finish(&mut self) -> io::Result<()> { Ok(()) }
}
impl Read for IsoStream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.eof { return Ok(0); }
// Drain current sector buffer
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]);
self.buf_pos += n;
return Ok(n);
}
// Read next sector
if self.read_next_sector()? {
let n = self.buf_len.min(buf.len());
buf[..n].copy_from_slice(&self.sector_buf[..n]);
self.buf_pos = n;
Ok(n)
} else {
self.eof = true;
Ok(0)
}
}
}
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 flush(&mut self) -> io::Result<()> { Ok(()) }
}