1use std::cmp::min;
2
3use crate::{CdReader, CdReaderError, RetryConfig, Toc, utils};
4
5#[derive(Debug, Clone)]
7pub struct TrackStreamConfig {
8 pub sectors_per_chunk: u32,
12 pub retry: RetryConfig,
14}
15
16impl Default for TrackStreamConfig {
17 fn default() -> Self {
18 Self {
19 sectors_per_chunk: 27,
20 retry: RetryConfig::default(),
21 }
22 }
23}
24
25pub struct TrackStream<'a> {
33 reader: &'a CdReader,
34 start_lba: u32,
35 next_lba: u32,
36 remaining_sectors: u32,
37 total_sectors: u32,
38 cfg: TrackStreamConfig,
39}
40
41impl<'a> TrackStream<'a> {
42 const SECTORS_PER_SECOND: f32 = 75.0;
43
44 pub fn next_chunk(&mut self) -> Result<Option<Vec<u8>>, CdReaderError> {
48 self.next_chunk_with(|lba, sectors, retry| {
49 self.reader.read_sectors_with_retry(lba, sectors, retry)
50 })
51 }
52
53 fn next_chunk_with<F>(&mut self, mut read_fn: F) -> Result<Option<Vec<u8>>, CdReaderError>
54 where
55 F: FnMut(u32, u32, &RetryConfig) -> Result<Vec<u8>, CdReaderError>,
56 {
57 if self.remaining_sectors == 0 {
58 return Ok(None);
59 }
60
61 let sectors = min(self.remaining_sectors, self.cfg.sectors_per_chunk.max(1));
62 let chunk = read_fn(self.next_lba, sectors, &self.cfg.retry)?;
63
64 self.next_lba += sectors;
65 self.remaining_sectors -= sectors;
66
67 Ok(Some(chunk))
68 }
69
70 pub fn total_sectors(&self) -> u32 {
72 self.total_sectors
73 }
74
75 pub fn current_sector(&self) -> u32 {
80 self.total_sectors - self.remaining_sectors
81 }
82
83 pub fn seek_to_sector(&mut self, sector: u32) -> Result<(), CdReaderError> {
88 if sector > self.total_sectors {
89 return Err(CdReaderError::Io(std::io::Error::new(
90 std::io::ErrorKind::InvalidInput,
91 "seek sector is out of track bounds",
92 )));
93 }
94
95 self.next_lba = self.start_lba + sector;
96 self.remaining_sectors = self.total_sectors - sector;
97 Ok(())
98 }
99
100 pub fn current_seconds(&self) -> f32 {
105 self.current_sector() as f32 / Self::SECTORS_PER_SECOND
106 }
107
108 pub fn total_seconds(&self) -> f32 {
113 self.total_sectors as f32 / Self::SECTORS_PER_SECOND
114 }
115
116 pub fn seek_to_seconds(&mut self, seconds: f32) -> Result<(), CdReaderError> {
120 if !seconds.is_finite() || seconds < 0.0 {
121 return Err(CdReaderError::Io(std::io::Error::new(
122 std::io::ErrorKind::InvalidInput,
123 "seek seconds must be a finite non-negative number",
124 )));
125 }
126
127 let target_sector = (seconds * Self::SECTORS_PER_SECOND).round() as u32;
128 self.seek_to_sector(target_sector.min(self.total_sectors))
129 }
130}
131
132impl CdReader {
133 pub fn open_track_stream<'a>(
140 &'a self,
141 toc: &Toc,
142 track_no: u8,
143 cfg: TrackStreamConfig,
144 ) -> Result<TrackStream<'a>, CdReaderError> {
145 let (start_lba, sectors) =
146 utils::get_track_bounds(toc, track_no).map_err(CdReaderError::Io)?;
147
148 Ok(TrackStream {
149 reader: self,
150 start_lba,
151 next_lba: start_lba,
152 remaining_sectors: sectors,
153 total_sectors: sectors,
154 cfg,
155 })
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::{TrackStream, TrackStreamConfig};
162 use crate::{CdReader, CdReaderError, RetryConfig};
163
164 fn mk_stream(
165 start_lba: u32,
166 total_sectors: u32,
167 sectors_per_chunk: u32,
168 ) -> TrackStream<'static> {
169 let reader: &'static CdReader = Box::leak(Box::new(CdReader {}));
170 TrackStream {
171 reader,
172 start_lba,
173 next_lba: start_lba,
174 remaining_sectors: total_sectors,
175 total_sectors,
176 cfg: TrackStreamConfig {
177 sectors_per_chunk,
178 retry: RetryConfig::default(),
179 },
180 }
181 }
182
183 #[test]
184 fn seek_to_sector_updates_position() {
185 let mut stream = mk_stream(10_000, 1_000, 27);
186 stream.seek_to_sector(250).unwrap();
187
188 assert_eq!(stream.current_sector(), 250);
189 assert_eq!(stream.next_lba, 10_250);
190 assert_eq!(stream.remaining_sectors, 750);
191 }
192
193 #[test]
194 fn seek_to_sector_returns_error_out_of_bounds() {
195 let mut stream = mk_stream(10_000, 1_000, 27);
196 let err = stream.seek_to_sector(1_001).unwrap_err();
197
198 match err {
199 CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
200 _ => panic!("expected Io(InvalidInput)"),
201 }
202 }
203
204 #[test]
205 fn seek_to_seconds_and_time_helpers_work() {
206 let mut stream = mk_stream(10_000, 750, 27); assert_eq!(stream.total_seconds(), 10.0);
208
209 stream.seek_to_seconds(2.0).unwrap();
210 assert_eq!(stream.current_sector(), 150);
211 assert!((stream.current_seconds() - 2.0).abs() < f32::EPSILON);
212 }
213
214 #[test]
215 fn seek_to_seconds_rejects_invalid_input() {
216 let mut stream = mk_stream(10_000, 750, 27);
217 let err = stream.seek_to_seconds(f32::NAN).unwrap_err();
218 match err {
219 CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::InvalidInput),
220 _ => panic!("expected Io(InvalidInput)"),
221 }
222 }
223
224 #[test]
225 fn next_chunk_reads_expected_size_and_advances() {
226 let mut stream = mk_stream(10_000, 100, 27);
227 let mut called = false;
228
229 let chunk = stream
230 .next_chunk_with(|lba, sectors, _| {
231 called = true;
232 assert_eq!(lba, 10_000);
233 assert_eq!(sectors, 27);
234 Ok(vec![0u8; (sectors as usize) * 2352])
235 })
236 .unwrap()
237 .unwrap();
238
239 assert!(called);
240 assert_eq!(chunk.len(), 27 * 2352);
241 assert_eq!(stream.current_sector(), 27);
242 assert_eq!(stream.remaining_sectors, 73);
243 }
244
245 #[test]
246 fn next_chunk_returns_none_when_finished() {
247 let mut stream = mk_stream(10_000, 0, 27);
248 let result = stream.next_chunk_with(|_, _, _| Ok(vec![1, 2, 3])).unwrap();
249 assert!(result.is_none());
250 }
251
252 #[test]
253 fn next_chunk_error_does_not_advance_position() {
254 let mut stream = mk_stream(10_000, 100, 27);
255 let err = stream
256 .next_chunk_with(|_, _, _| {
257 Err(CdReaderError::Io(std::io::Error::other(
258 "simulated read failure",
259 )))
260 })
261 .unwrap_err();
262
263 match err {
264 CdReaderError::Io(io) => assert_eq!(io.kind(), std::io::ErrorKind::Other),
265 _ => panic!("expected Io(Other)"),
266 }
267 assert_eq!(stream.current_sector(), 0);
268 assert_eq!(stream.next_lba, 10_000);
269 assert_eq!(stream.remaining_sectors, 100);
270 }
271}