1use std::cmp::min;
2
3use crate::data_reader::validate_track_format;
4use crate::{CdReader, CdReaderError, ReadOptions, ReadSpeed, Toc, utils};
5
6fn apply_stream_read_speed_once(
7 options: &ReadOptions,
8 request_read_speed: impl FnOnce(ReadSpeed) -> Result<(), CdReaderError>,
9) -> Result<ReadOptions, CdReaderError> {
10 request_read_speed(options.read_speed())?;
11
12 Ok(options.clone().with_read_speed(ReadSpeed::Unchanged))
16}
17
18pub struct TrackStream<'a> {
23 reader: &'a CdReader,
24 start_lba: u32,
25 next_lba: u32,
26 remaining_sectors: u32,
27 total_sectors: u32,
28 sectors_per_chunk: u32,
29 read_options: ReadOptions,
30}
31
32impl<'a> TrackStream<'a> {
33 const DEFAULT_SECTORS_PER_CHUNK: u32 = 27;
34 const SECTORS_PER_SECOND: f32 = 75.0;
35
36 pub fn with_sectors_per_chunk(mut self, sectors: u32) -> Self {
42 self.sectors_per_chunk = sectors.max(1);
43 self
44 }
45
46 pub fn next_chunk(&mut self) -> Result<Option<Vec<u8>>, CdReaderError> {
57 self.next_chunk_with(|lba, sectors, options| {
58 self.reader.read_sector_range(lba, sectors, options)
59 })
60 }
61
62 fn next_chunk_with<F>(&mut self, mut read_fn: F) -> Result<Option<Vec<u8>>, CdReaderError>
63 where
64 F: FnMut(u32, u32, &ReadOptions) -> Result<Vec<u8>, CdReaderError>,
65 {
66 if self.remaining_sectors == 0 {
67 return Ok(None);
68 }
69
70 let sectors = min(self.remaining_sectors, self.sectors_per_chunk);
71 let chunk = read_fn(self.next_lba, sectors, &self.read_options)?;
72
73 self.next_lba += sectors;
74 self.remaining_sectors -= sectors;
75
76 Ok(Some(chunk))
77 }
78
79 pub fn total_sectors(&self) -> u32 {
81 self.total_sectors
82 }
83
84 pub fn current_sector(&self) -> u32 {
89 self.total_sectors - self.remaining_sectors
90 }
91
92 pub fn seek_to_sector(&mut self, sector: u32) -> Result<(), CdReaderError> {
102 if sector > self.total_sectors {
103 return Err(CdReaderError::Io(std::io::Error::new(
104 std::io::ErrorKind::InvalidInput,
105 "seek sector is out of track bounds",
106 )));
107 }
108
109 self.next_lba = self.start_lba + sector;
110 self.remaining_sectors = self.total_sectors - sector;
111 Ok(())
112 }
113
114 pub fn current_seconds(&self) -> f32 {
119 self.current_sector() as f32 / Self::SECTORS_PER_SECOND
120 }
121
122 pub fn total_seconds(&self) -> f32 {
127 self.total_sectors as f32 / Self::SECTORS_PER_SECOND
128 }
129
130 pub fn seek_to_seconds(&mut self, seconds: f32) -> Result<(), CdReaderError> {
140 if !seconds.is_finite() || seconds < 0.0 {
141 return Err(CdReaderError::Io(std::io::Error::new(
142 std::io::ErrorKind::InvalidInput,
143 "seek seconds must be a finite non-negative number",
144 )));
145 }
146
147 let target_sector = (seconds * Self::SECTORS_PER_SECOND).round() as u32;
148 self.seek_to_sector(target_sector.min(self.total_sectors))
149 }
150}
151
152impl CdReader {
153 pub fn open_track_stream<'a>(
159 &'a self,
160 toc: &Toc,
161 track_no: u8,
162 ) -> Result<TrackStream<'a>, CdReaderError> {
163 self.open_track_stream_with_options(toc, track_no, &ReadOptions::default())
164 }
165
166 pub fn open_track_stream_with_options<'a>(
181 &'a self,
182 toc: &Toc,
183 track_no: u8,
184 options: &ReadOptions,
185 ) -> Result<TrackStream<'a>, CdReaderError> {
186 if let Some(track) = toc.tracks.iter().find(|track| track.number == track_no) {
187 validate_track_format(track, options.format())?;
188 }
189
190 let (start_lba, sectors) =
191 utils::get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
192 let read_options = apply_stream_read_speed_once(options, |read_speed| {
193 self.drive.request_read_speed(read_speed)
194 })?;
195
196 Ok(TrackStream {
197 reader: self,
198 start_lba,
199 next_lba: start_lba,
200 remaining_sectors: sectors,
201 total_sectors: sectors,
202 sectors_per_chunk: TrackStream::DEFAULT_SECTORS_PER_CHUNK,
203 read_options,
204 })
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::{TrackStream, apply_stream_read_speed_once};
211 use crate::{
212 CdReader, CdReaderError, ReadOptions, ReadSpeed, RetryConfig, SectorReadFormat, Toc, Track,
213 };
214
215 fn mk_stream(
216 start_lba: u32,
217 total_sectors: u32,
218 sectors_per_chunk: u32,
219 ) -> TrackStream<'static> {
220 let reader: &'static CdReader = Box::leak(Box::new(CdReader::test_reader()));
221 TrackStream {
222 reader,
223 start_lba,
224 next_lba: start_lba,
225 remaining_sectors: total_sectors,
226 total_sectors,
227 sectors_per_chunk: TrackStream::DEFAULT_SECTORS_PER_CHUNK,
228 read_options: ReadOptions::default(),
229 }
230 .with_sectors_per_chunk(sectors_per_chunk)
231 }
232
233 #[test]
234 fn sectors_per_chunk_normalizes_zero() {
235 let stream = mk_stream(10_000, 100, 0);
236 assert_eq!(stream.sectors_per_chunk, 1);
237 }
238
239 #[test]
240 fn stream_speed_is_requested_once_before_chunk_reads() {
241 let options = ReadOptions::default().with_read_speed(ReadSpeed::CustomMultiplier(4));
242 let mut speed_requests = 0;
243 let chunk_options = apply_stream_read_speed_once(&options, |read_speed| {
244 speed_requests += 1;
245 assert!(matches!(read_speed, ReadSpeed::CustomMultiplier(4)));
246 Ok(())
247 })
248 .unwrap();
249
250 assert_eq!(speed_requests, 1);
251 assert!(matches!(chunk_options.read_speed(), ReadSpeed::Unchanged));
252
253 let mut stream = mk_stream(10_000, 100, 27);
254 stream.read_options = chunk_options;
255 for _ in 0..2 {
256 stream
257 .next_chunk_with(|_, _, options| {
258 assert!(matches!(options.read_speed(), ReadSpeed::Unchanged));
259 Ok(Vec::new())
260 })
261 .unwrap();
262 }
263
264 assert_eq!(speed_requests, 1);
265 }
266
267 #[test]
268 fn open_stream_preserves_chunk_read_options() {
269 let reader = CdReader::test_reader();
270 let toc = Toc {
271 first_track: 1,
272 last_track: 1,
273 tracks: vec![Track {
274 number: 1,
275 start_lba: 10_000,
276 start_msf: (2, 15, 25),
277 is_audio: false,
278 }],
279 leadout_lba: 10_100,
280 };
281 let options = ReadOptions::default()
282 .with_format(SectorReadFormat::Mode1Cooked)
283 .with_retry(RetryConfig::default().with_max_attempts(9));
284
285 let stream = reader
286 .open_track_stream_with_options(&toc, 1, &options)
287 .unwrap();
288
289 assert_eq!(stream.read_options.format(), SectorReadFormat::Mode1Cooked);
290 assert_eq!(stream.read_options.retry().max_attempts, 9);
291 assert!(matches!(
292 stream.read_options.read_speed(),
293 ReadSpeed::Unchanged
294 ));
295 assert_eq!(
296 stream.sectors_per_chunk,
297 TrackStream::DEFAULT_SECTORS_PER_CHUNK
298 );
299 }
300
301 #[test]
302 fn seek_to_sector_updates_position() {
303 let mut stream = mk_stream(10_000, 1_000, 27);
304 stream.seek_to_sector(250).unwrap();
305
306 assert_eq!(stream.current_sector(), 250);
307 assert_eq!(stream.next_lba, 10_250);
308 assert_eq!(stream.remaining_sectors, 750);
309 }
310
311 #[test]
312 fn seek_to_sector_returns_error_out_of_bounds() {
313 let mut stream = mk_stream(10_000, 1_000, 27);
314 let err = stream.seek_to_sector(1_001).unwrap_err();
315
316 match err {
317 CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
318 _ => panic!("expected Io(InvalidInput)"),
319 }
320 }
321
322 #[test]
323 fn seek_to_seconds_and_time_helpers_work() {
324 let mut stream = mk_stream(10_000, 750, 27); assert_eq!(stream.total_seconds(), 10.0);
326
327 stream.seek_to_seconds(2.0).unwrap();
328 assert_eq!(stream.current_sector(), 150);
329 assert!((stream.current_seconds() - 2.0).abs() < f32::EPSILON);
330 }
331
332 #[test]
333 fn seek_to_seconds_rejects_invalid_input() {
334 let mut stream = mk_stream(10_000, 750, 27);
335 let err = stream.seek_to_seconds(f32::NAN).unwrap_err();
336 match err {
337 CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
338 _ => panic!("expected Io(InvalidInput)"),
339 }
340 }
341
342 #[test]
343 fn next_chunk_uses_configured_read_options_and_advances() {
344 let mut stream = mk_stream(10_000, 100, 27);
345 stream.read_options = ReadOptions::default()
346 .with_format(SectorReadFormat::Mode1Cooked)
347 .with_retry(RetryConfig::default().with_max_attempts(9));
348 let mut called = false;
349
350 let chunk = stream
351 .next_chunk_with(|lba, sectors, options| {
352 called = true;
353 assert_eq!(lba, 10_000);
354 assert_eq!(sectors, 27);
355 assert_eq!(options.format(), SectorReadFormat::Mode1Cooked);
356 assert_eq!(options.retry().max_attempts, 9);
357 assert!(matches!(options.read_speed(), ReadSpeed::Unchanged));
358 Ok(vec![
359 0u8;
360 (sectors as usize) * options.format().sector_size()
361 ])
362 })
363 .unwrap()
364 .unwrap();
365
366 assert!(called);
367 assert_eq!(chunk.len(), 27 * 2048);
368 assert_eq!(stream.current_sector(), 27);
369 assert_eq!(stream.remaining_sectors, 73);
370 }
371
372 #[test]
373 fn next_chunk_returns_none_when_finished() {
374 let mut stream = mk_stream(10_000, 0, 27);
375 let result = stream.next_chunk_with(|_, _, _| Ok(vec![1, 2, 3])).unwrap();
376 assert!(result.is_none());
377 }
378
379 #[test]
380 fn next_chunk_error_does_not_advance_position() {
381 let mut stream = mk_stream(10_000, 100, 27);
382 let err = stream
383 .next_chunk_with(|_, _, _| {
384 Err(CdReaderError::Io(std::io::Error::other(
385 "simulated read failure",
386 )))
387 })
388 .unwrap_err();
389
390 match err {
391 CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::Other),
392 _ => panic!("expected Io(Other)"),
393 }
394 assert_eq!(stream.current_sector(), 0);
395 assert_eq!(stream.next_lba, 10_000);
396 assert_eq!(stream.remaining_sectors, 100);
397 }
398}