1use std::fs::File;
18use std::io::{self, Read, Seek, SeekFrom};
19use std::path::Path;
20use std::sync::Arc;
21use std::sync::atomic::{AtomicU64, Ordering};
22use std::time::{Duration, Instant};
23
24const STALL_LIMIT: Duration = Duration::from_secs(30);
28
29const POLL_INTERVAL: Duration = Duration::from_millis(10);
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum StreamStatus {
35 Downloading,
37 Complete,
39 Failed,
43}
44
45pub struct PartialFileSource {
47 file: File,
48 pos: u64,
49 bytes_written: Arc<AtomicU64>,
51 total: u64,
53 status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
54 stall_limit: Duration,
55 wait_for_bytes: bool,
63 advertise_len: bool,
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum ProbeMode {
76 Full,
79 Lengthless,
83}
84
85impl PartialFileSource {
86 pub fn open(
95 path: &Path,
96 bytes_written: Arc<AtomicU64>,
97 total: u64,
98 status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
99 mode: ProbeMode,
100 ) -> io::Result<Self> {
101 let mut source = Self::with_stall_limit(path, bytes_written, total, status, STALL_LIMIT)?;
102 source.advertise_len = mode == ProbeMode::Full;
103 Ok(source)
104 }
105
106 pub fn open_for_probe(
109 path: &Path,
110 bytes_written: Arc<AtomicU64>,
111 total: u64,
112 status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
113 mode: ProbeMode,
114 ) -> io::Result<Self> {
115 let mut source = Self::with_stall_limit(path, bytes_written, total, status, STALL_LIMIT)?;
116 source.wait_for_bytes = false;
117 source.advertise_len = mode == ProbeMode::Full;
118 Ok(source)
119 }
120
121 fn with_stall_limit(
122 path: &Path,
123 bytes_written: Arc<AtomicU64>,
124 total: u64,
125 status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
126 stall_limit: Duration,
127 ) -> io::Result<Self> {
128 Ok(Self {
129 file: File::open(path)?,
130 pos: 0,
131 bytes_written,
132 total,
133 status,
134 stall_limit,
135 wait_for_bytes: true,
136 advertise_len: true,
137 })
138 }
139
140 fn available(&self) -> u64 {
143 let written = self.bytes_written.load(Ordering::Acquire);
144 match (self.status)() {
145 StreamStatus::Complete => self.file.metadata().map(|m| m.len()).unwrap_or(written),
146 _ => written,
147 }
148 }
149
150 fn read_available(&mut self, buf: &mut [u8], limit: u64) -> io::Result<usize> {
154 let to_read = (limit as usize).min(buf.len());
155 self.file.read(&mut buf[..to_read]).inspect(|n| {
156 self.pos += *n as u64;
157 })
158 }
159}
160
161impl Read for PartialFileSource {
162 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
163 if buf.is_empty() {
164 return Ok(0);
165 }
166
167 let deadline = Instant::now() + self.stall_limit;
168 loop {
169 let available = self.available();
170 if available > self.pos {
171 let n = self.read_available(buf, available - self.pos)?;
172 if n > 0 {
173 return Ok(n);
174 }
175 }
178
179 match (self.status)() {
180 StreamStatus::Failed => {
181 return Err(io::Error::new(
182 io::ErrorKind::BrokenPipe,
183 "stream download failed before delivering the whole track",
184 ));
185 }
186 StreamStatus::Complete if available <= self.pos => return Ok(0),
188 StreamStatus::Complete => {}
189 StreamStatus::Downloading => {
190 if self.total > 0 && available >= self.total && self.pos >= self.total {
192 return Ok(0);
193 }
194 }
195 }
196
197 if !self.wait_for_bytes {
198 return Err(io::Error::new(
199 io::ErrorKind::UnexpectedEof,
200 "past what the download has delivered",
201 ));
202 }
203 if Instant::now() >= deadline {
204 return Err(io::Error::new(
205 io::ErrorKind::TimedOut,
206 "stream download stalled",
207 ));
208 }
209 std::thread::sleep(POLL_INTERVAL);
210 }
211 }
212}
213
214impl Seek for PartialFileSource {
215 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
216 let target: i64 = match pos {
217 SeekFrom::Start(n) => n as i64,
218 SeekFrom::Current(n) => self.pos as i64 + n,
219 SeekFrom::End(n) => {
222 let len = if self.total > 0 {
223 self.total
224 } else {
225 self.available()
226 };
227 len as i64 + n
228 }
229 };
230
231 if target < 0 {
232 return Err(io::Error::new(
233 io::ErrorKind::InvalidInput,
234 "seek before beginning of stream",
235 ));
236 }
237
238 self.pos = self.file.seek(SeekFrom::Start(target as u64))?;
239 Ok(self.pos)
240 }
241}
242
243impl symphonia::core::io::MediaSource for PartialFileSource {
245 fn is_seekable(&self) -> bool {
246 true
251 }
252
253 fn byte_len(&self) -> Option<u64> {
254 (self.advertise_len && self.total > 0).then_some(self.total)
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use std::io::Write;
261 use std::sync::atomic::AtomicU8;
262
263 use symphonia::core::io::MediaSource;
264
265 use super::*;
266
267 struct Fixture {
270 _dir: tempfile::TempDir,
271 path: std::path::PathBuf,
272 written: Arc<AtomicU64>,
273 status: Arc<AtomicU8>,
274 }
275
276 const DOWNLOADING: u8 = 0;
277 const COMPLETE: u8 = 1;
278 const FAILED: u8 = 2;
279
280 impl Fixture {
281 fn new() -> Self {
282 let dir = tempfile::tempdir().unwrap();
283 let path = dir.path().join("track.opus.part");
284 File::create(&path).unwrap();
285 Self {
286 _dir: dir,
287 path,
288 written: Arc::new(AtomicU64::new(0)),
289 status: Arc::new(AtomicU8::new(DOWNLOADING)),
290 }
291 }
292
293 fn push(&self, chunk: &[u8]) {
295 let mut f = std::fs::OpenOptions::new()
296 .append(true)
297 .open(&self.path)
298 .unwrap();
299 f.write_all(chunk).unwrap();
300 f.flush().unwrap();
301 self.written
302 .fetch_add(chunk.len() as u64, Ordering::Release);
303 }
304
305 fn set(&self, status: u8) {
306 self.status.store(status, Ordering::Release);
307 }
308
309 fn status_fn(&self) -> Arc<dyn Fn() -> StreamStatus + Send + Sync> {
310 let status = self.status.clone();
311 Arc::new(move || match status.load(Ordering::Acquire) {
312 COMPLETE => StreamStatus::Complete,
313 FAILED => StreamStatus::Failed,
314 _ => StreamStatus::Downloading,
315 })
316 }
317
318 fn source(&self, total: u64) -> PartialFileSource {
319 self.source_with_stall(total, STALL_LIMIT)
320 }
321
322 fn source_with_stall(&self, total: u64, stall: Duration) -> PartialFileSource {
323 let status = self.status.clone();
324 PartialFileSource::with_stall_limit(
325 &self.path,
326 self.written.clone(),
327 total,
328 Arc::new(move || match status.load(Ordering::Acquire) {
329 COMPLETE => StreamStatus::Complete,
330 FAILED => StreamStatus::Failed,
331 _ => StreamStatus::Downloading,
332 }),
333 stall,
334 )
335 .unwrap()
336 }
337 }
338
339 #[test]
340 fn reads_what_has_landed() {
341 let fx = Fixture::new();
342 fx.push(b"hello streaming world");
343 fx.set(COMPLETE);
344
345 let mut out = Vec::new();
346 fx.source(21).read_to_end(&mut out).unwrap();
347 assert_eq!(out, b"hello streaming world");
348 }
349
350 #[test]
351 fn read_stops_at_the_write_head_then_resumes() {
352 let fx = Fixture::new();
353 fx.push(b"abcd");
354 let mut src = fx.source(10);
355
356 let mut first = [0u8; 8];
357 assert_eq!(src.read(&mut first).unwrap(), 4);
358 assert_eq!(&first[..4], b"abcd");
359
360 std::thread::spawn({
362 let path = fx.path.clone();
363 let written = fx.written.clone();
364 move || {
365 std::thread::sleep(Duration::from_millis(20));
366 let mut f = std::fs::OpenOptions::new()
367 .append(true)
368 .open(&path)
369 .unwrap();
370 f.write_all(b"efghij").unwrap();
371 f.flush().unwrap();
372 written.fetch_add(6, Ordering::Release);
373 }
374 });
375
376 let mut rest = [0u8; 8];
377 let n = src.read(&mut rest).unwrap();
378 assert_eq!(&rest[..n], b"efghij");
379 }
380
381 #[test]
382 fn seeks_freely_below_the_write_head() {
383 let fx = Fixture::new();
384 fx.push(b"0123456789");
385 let mut src = fx.source(1_000_000);
386
387 assert_eq!(src.seek(SeekFrom::Start(5)).unwrap(), 5);
388 let mut out = [0u8; 3];
389 src.read_exact(&mut out).unwrap();
390 assert_eq!(&out, b"567");
391
392 assert_eq!(src.seek(SeekFrom::Start(1)).unwrap(), 1);
394 src.read_exact(&mut out).unwrap();
395 assert_eq!(&out, b"123");
396
397 assert_eq!(src.seek(SeekFrom::Current(-2)).unwrap(), 2);
398 }
399
400 #[test]
401 fn seek_from_end_uses_the_advertised_length() {
402 let fx = Fixture::new();
403 fx.push(b"0123456789");
404 let mut src = fx.source(10);
405
406 assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
407 assert_eq!(src.seek(SeekFrom::End(-3)).unwrap(), 7);
408
409 let mut out = [0u8; 3];
410 src.read_exact(&mut out).unwrap();
411 assert_eq!(&out, b"789");
412 }
413
414 #[test]
415 fn seek_before_start_errors() {
416 let fx = Fixture::new();
417 fx.push(b"hello");
418 assert!(fx.source(5).seek(SeekFrom::Current(-1)).is_err());
419 }
420
421 #[test]
422 fn failed_download_errors_instead_of_reporting_eof() {
423 let fx = Fixture::new();
424 fx.push(b"partial");
425 fx.set(FAILED);
426 let mut src = fx.source(1000);
427
428 let mut out = [0u8; 7];
429 src.read_exact(&mut out).unwrap();
430 assert_eq!(&out, b"partial");
431
432 assert_eq!(
435 src.read(&mut out).unwrap_err().kind(),
436 io::ErrorKind::BrokenPipe
437 );
438 }
439
440 #[test]
441 fn failure_wakes_a_blocked_reader() {
442 let fx = Fixture::new();
443 let mut src = fx.source(1000);
444
445 let status = fx.status.clone();
446 std::thread::spawn(move || {
447 std::thread::sleep(Duration::from_millis(20));
448 status.store(FAILED, Ordering::Release);
449 });
450
451 let mut out = [0u8; 8];
452 assert_eq!(
453 src.read(&mut out).unwrap_err().kind(),
454 io::ErrorKind::BrokenPipe
455 );
456 }
457
458 #[test]
459 fn a_probe_reads_only_what_has_arrived() {
460 let fx = Fixture::new();
464 fx.push(b"0123456789");
465 let mut src = PartialFileSource::open_for_probe(
466 &fx.path,
467 fx.written.clone(),
468 1_000,
469 fx.status_fn(),
470 ProbeMode::Full,
471 )
472 .unwrap();
473
474 let mut out = [0u8; 10];
475 src.read_exact(&mut out).unwrap();
476 assert_eq!(
477 src.read(&mut out).unwrap_err().kind(),
478 io::ErrorKind::UnexpectedEof,
479 "past the write head is an answer, not a wait"
480 );
481 }
482
483 #[test]
484 fn playback_reads_wait_for_what_has_not_arrived() {
485 let fx = Fixture::new();
489 fx.push(b"0123456789");
490 let mut src = PartialFileSource::open(
491 &fx.path,
492 fx.written.clone(),
493 1_000,
494 fx.status_fn(),
495 ProbeMode::Lengthless,
496 )
497 .unwrap();
498
499 let mut out = [0u8; 10];
500 src.read_exact(&mut out).unwrap();
501
502 std::thread::spawn({
503 let path = fx.path.clone();
504 let written = fx.written.clone();
505 move || {
506 std::thread::sleep(Duration::from_millis(20));
507 let mut f = std::fs::OpenOptions::new()
508 .append(true)
509 .open(&path)
510 .unwrap();
511 f.write_all(b"abcde").unwrap();
512 f.flush().unwrap();
513 written.fetch_add(5, Ordering::Release);
514 }
515 });
516
517 let n = src.read(&mut out).unwrap();
518 assert_eq!(&out[..n], b"abcde", "it waited rather than giving up");
519 }
520
521 #[test]
522 fn stalled_download_times_out() {
523 let fx = Fixture::new();
526 let mut src = fx.source_with_stall(1000, Duration::from_millis(20));
527 let mut out = [0u8; 8];
528 assert_eq!(
529 src.read(&mut out).unwrap_err().kind(),
530 io::ErrorKind::TimedOut
531 );
532 }
533
534 #[test]
535 fn completion_ends_the_read_at_the_true_length() {
536 let fx = Fixture::new();
539 fx.push(b"chunked");
540 fx.set(COMPLETE);
541
542 let mut out = Vec::new();
543 fx.source(0).read_to_end(&mut out).unwrap();
544 assert_eq!(out, b"chunked");
545 }
546
547 #[test]
548 fn survives_the_part_file_being_renamed() {
549 let fx = Fixture::new();
552 fx.push(b"0123456789");
553 let mut src = fx.source(10);
554
555 let mut out = [0u8; 4];
556 src.read_exact(&mut out).unwrap();
557 assert_eq!(&out, b"0123");
558
559 std::fs::rename(&fx.path, fx.path.with_extension("")).unwrap();
560 fx.set(COMPLETE);
561
562 let mut rest = Vec::new();
563 src.read_to_end(&mut rest).unwrap();
564 assert_eq!(rest, b"456789");
565 }
566
567 #[test]
568 fn byte_len_is_the_advertised_length_only() {
569 let fx = Fixture::new();
570 assert_eq!(fx.source(42).byte_len(), Some(42));
571 assert_eq!(fx.source(0).byte_len(), None);
574 }
575
576 #[test]
577 fn is_seekable_true() {
578 let fx = Fixture::new();
579 assert!(fx.source(0).is_seekable());
580 }
581}