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,
74 whole_file_end: bool,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum ProbeMode {
83 Full,
86 Lengthless,
96 LengthlessWholeEnd,
105}
106
107impl PartialFileSource {
108 pub fn open(
117 path: &Path,
118 bytes_written: Arc<AtomicU64>,
119 total: u64,
120 status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
121 mode: ProbeMode,
122 ) -> io::Result<Self> {
123 let mut source = Self::with_stall_limit(path, bytes_written, total, status, STALL_LIMIT)?;
124 source.advertise_len = mode == ProbeMode::Full;
125 source.whole_file_end = mode != ProbeMode::Lengthless;
126 Ok(source)
127 }
128
129 pub fn open_for_probe(
132 path: &Path,
133 bytes_written: Arc<AtomicU64>,
134 total: u64,
135 status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
136 mode: ProbeMode,
137 ) -> io::Result<Self> {
138 let mut source = Self::with_stall_limit(path, bytes_written, total, status, STALL_LIMIT)?;
139 source.wait_for_bytes = false;
140 source.advertise_len = mode == ProbeMode::Full;
141 source.whole_file_end = mode != ProbeMode::Lengthless;
142 Ok(source)
143 }
144
145 fn with_stall_limit(
146 path: &Path,
147 bytes_written: Arc<AtomicU64>,
148 total: u64,
149 status: Arc<dyn Fn() -> StreamStatus + Send + Sync>,
150 stall_limit: Duration,
151 ) -> io::Result<Self> {
152 Ok(Self {
153 file: File::open(path)?,
154 pos: 0,
155 bytes_written,
156 total,
157 status,
158 stall_limit,
159 wait_for_bytes: true,
160 advertise_len: true,
161 whole_file_end: true,
162 })
163 }
164
165 fn available(&self) -> u64 {
168 let written = self.bytes_written.load(Ordering::Acquire);
169 match (self.status)() {
170 StreamStatus::Complete => self.file.metadata().map(|m| m.len()).unwrap_or(written),
171 _ => written,
172 }
173 }
174
175 fn read_available(&mut self, buf: &mut [u8], limit: u64) -> io::Result<usize> {
179 let to_read = (limit as usize).min(buf.len());
180 self.file.read(&mut buf[..to_read]).inspect(|n| {
181 self.pos += *n as u64;
182 })
183 }
184}
185
186impl Read for PartialFileSource {
187 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
188 if buf.is_empty() {
189 return Ok(0);
190 }
191
192 let deadline = Instant::now() + self.stall_limit;
193 loop {
194 let available = self.available();
195 if available > self.pos {
196 let n = self.read_available(buf, available - self.pos)?;
197 if n > 0 {
198 return Ok(n);
199 }
200 }
203
204 match (self.status)() {
205 StreamStatus::Failed => {
206 return Err(io::Error::new(
207 io::ErrorKind::BrokenPipe,
208 "stream download failed before delivering the whole track",
209 ));
210 }
211 StreamStatus::Complete if available <= self.pos => return Ok(0),
213 StreamStatus::Complete => {}
214 StreamStatus::Downloading => {
215 if self.total > 0 && available >= self.total && self.pos >= self.total {
217 return Ok(0);
218 }
219 }
220 }
221
222 if !self.wait_for_bytes {
223 return Err(io::Error::new(
224 io::ErrorKind::UnexpectedEof,
225 "past what the download has delivered",
226 ));
227 }
228 if Instant::now() >= deadline {
229 return Err(io::Error::new(
230 io::ErrorKind::TimedOut,
231 "stream download stalled",
232 ));
233 }
234 std::thread::sleep(POLL_INTERVAL);
235 }
236 }
237}
238
239impl Seek for PartialFileSource {
240 fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
241 let target: i64 = match pos {
242 SeekFrom::Start(n) => n as i64,
243 SeekFrom::Current(n) => self.pos as i64 + n,
244 SeekFrom::End(n) => {
251 let len = if self.whole_file_end && self.total > 0 {
252 self.total
253 } else {
254 self.available()
255 };
256 len as i64 + n
257 }
258 };
259
260 if target < 0 {
261 return Err(io::Error::new(
262 io::ErrorKind::InvalidInput,
263 "seek before beginning of stream",
264 ));
265 }
266
267 self.pos = self.file.seek(SeekFrom::Start(target as u64))?;
268 Ok(self.pos)
269 }
270}
271
272impl symphonia::core::io::MediaSource for PartialFileSource {
274 fn is_seekable(&self) -> bool {
275 true
284 }
285
286 fn byte_len(&self) -> Option<u64> {
287 (self.advertise_len && self.total > 0).then_some(self.total)
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use std::io::Write;
294 use std::sync::atomic::AtomicU8;
295
296 use symphonia::core::io::MediaSource;
297
298 use super::*;
299
300 struct Fixture {
303 _dir: tempfile::TempDir,
304 path: std::path::PathBuf,
305 written: Arc<AtomicU64>,
306 status: Arc<AtomicU8>,
307 }
308
309 const DOWNLOADING: u8 = 0;
310 const COMPLETE: u8 = 1;
311 const FAILED: u8 = 2;
312
313 impl Fixture {
314 fn new() -> Self {
315 let dir = tempfile::tempdir().unwrap();
316 let path = dir.path().join("track.opus.part");
317 File::create(&path).unwrap();
318 Self {
319 _dir: dir,
320 path,
321 written: Arc::new(AtomicU64::new(0)),
322 status: Arc::new(AtomicU8::new(DOWNLOADING)),
323 }
324 }
325
326 fn push(&self, chunk: &[u8]) {
328 let mut f = std::fs::OpenOptions::new()
329 .append(true)
330 .open(&self.path)
331 .unwrap();
332 f.write_all(chunk).unwrap();
333 f.flush().unwrap();
334 self.written
335 .fetch_add(chunk.len() as u64, Ordering::Release);
336 }
337
338 fn set(&self, status: u8) {
339 self.status.store(status, Ordering::Release);
340 }
341
342 fn status_fn(&self) -> Arc<dyn Fn() -> StreamStatus + Send + Sync> {
343 let status = self.status.clone();
344 Arc::new(move || match status.load(Ordering::Acquire) {
345 COMPLETE => StreamStatus::Complete,
346 FAILED => StreamStatus::Failed,
347 _ => StreamStatus::Downloading,
348 })
349 }
350
351 fn source(&self, total: u64) -> PartialFileSource {
352 self.source_with_stall(total, STALL_LIMIT)
353 }
354
355 fn source_with_stall(&self, total: u64, stall: Duration) -> PartialFileSource {
356 let status = self.status.clone();
357 PartialFileSource::with_stall_limit(
358 &self.path,
359 self.written.clone(),
360 total,
361 Arc::new(move || match status.load(Ordering::Acquire) {
362 COMPLETE => StreamStatus::Complete,
363 FAILED => StreamStatus::Failed,
364 _ => StreamStatus::Downloading,
365 }),
366 stall,
367 )
368 .unwrap()
369 }
370 }
371
372 #[test]
373 fn reads_what_has_landed() {
374 let fx = Fixture::new();
375 fx.push(b"hello streaming world");
376 fx.set(COMPLETE);
377
378 let mut out = Vec::new();
379 fx.source(21).read_to_end(&mut out).unwrap();
380 assert_eq!(out, b"hello streaming world");
381 }
382
383 #[test]
384 fn read_stops_at_the_write_head_then_resumes() {
385 let fx = Fixture::new();
386 fx.push(b"abcd");
387 let mut src = fx.source(10);
388
389 let mut first = [0u8; 8];
390 assert_eq!(src.read(&mut first).unwrap(), 4);
391 assert_eq!(&first[..4], b"abcd");
392
393 std::thread::spawn({
395 let path = fx.path.clone();
396 let written = fx.written.clone();
397 move || {
398 std::thread::sleep(Duration::from_millis(20));
399 let mut f = std::fs::OpenOptions::new()
400 .append(true)
401 .open(&path)
402 .unwrap();
403 f.write_all(b"efghij").unwrap();
404 f.flush().unwrap();
405 written.fetch_add(6, Ordering::Release);
406 }
407 });
408
409 let mut rest = [0u8; 8];
410 let n = src.read(&mut rest).unwrap();
411 assert_eq!(&rest[..n], b"efghij");
412 }
413
414 #[test]
415 fn seeks_freely_below_the_write_head() {
416 let fx = Fixture::new();
417 fx.push(b"0123456789");
418 let mut src = fx.source(1_000_000);
419
420 assert_eq!(src.seek(SeekFrom::Start(5)).unwrap(), 5);
421 let mut out = [0u8; 3];
422 src.read_exact(&mut out).unwrap();
423 assert_eq!(&out, b"567");
424
425 assert_eq!(src.seek(SeekFrom::Start(1)).unwrap(), 1);
427 src.read_exact(&mut out).unwrap();
428 assert_eq!(&out, b"123");
429
430 assert_eq!(src.seek(SeekFrom::Current(-2)).unwrap(), 2);
431 }
432
433 #[test]
434 fn seek_from_end_uses_the_advertised_length() {
435 let fx = Fixture::new();
436 fx.push(b"0123456789");
437 let mut src = fx.source(10);
438
439 assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
440 assert_eq!(src.seek(SeekFrom::End(-3)).unwrap(), 7);
441
442 let mut out = [0u8; 3];
443 src.read_exact(&mut out).unwrap();
444 assert_eq!(&out, b"789");
445 }
446
447 #[test]
448 fn a_lengthless_source_ends_where_the_download_does() {
449 let fx = Fixture::new();
455 fx.push(b"0123456789");
456
457 let mut src = PartialFileSource::open(
458 &fx.path,
459 fx.written.clone(),
460 1_000,
461 fx.status_fn(),
462 ProbeMode::Lengthless,
463 )
464 .unwrap();
465 assert_eq!(src.byte_len(), None);
466 assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
467
468 let mut src = PartialFileSource::open(
470 &fx.path,
471 fx.written.clone(),
472 1_000,
473 fx.status_fn(),
474 ProbeMode::Full,
475 )
476 .unwrap();
477 assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 1_000);
478 }
479
480 #[test]
487 fn an_ogg_source_still_ends_at_the_whole_file() {
488 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::LengthlessWholeEnd,
496 )
497 .unwrap();
498 assert_eq!(src.byte_len(), None);
501 assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 1_000);
503 }
504
505 #[test]
506 fn a_landed_download_ends_at_the_whole_file() {
507 let fx = Fixture::new();
510 fx.push(b"0123456789");
511 fx.set(COMPLETE);
512
513 let mut src = PartialFileSource::open(
514 &fx.path,
515 fx.written.clone(),
516 10,
517 fx.status_fn(),
518 ProbeMode::Lengthless,
519 )
520 .unwrap();
521 assert_eq!(src.seek(SeekFrom::End(0)).unwrap(), 10);
522 }
523
524 #[test]
525 fn seek_before_start_errors() {
526 let fx = Fixture::new();
527 fx.push(b"hello");
528 assert!(fx.source(5).seek(SeekFrom::Current(-1)).is_err());
529 }
530
531 #[test]
532 fn failed_download_errors_instead_of_reporting_eof() {
533 let fx = Fixture::new();
534 fx.push(b"partial");
535 fx.set(FAILED);
536 let mut src = fx.source(1000);
537
538 let mut out = [0u8; 7];
539 src.read_exact(&mut out).unwrap();
540 assert_eq!(&out, b"partial");
541
542 assert_eq!(
545 src.read(&mut out).unwrap_err().kind(),
546 io::ErrorKind::BrokenPipe
547 );
548 }
549
550 #[test]
551 fn failure_wakes_a_blocked_reader() {
552 let fx = Fixture::new();
553 let mut src = fx.source(1000);
554
555 let status = fx.status.clone();
556 std::thread::spawn(move || {
557 std::thread::sleep(Duration::from_millis(20));
558 status.store(FAILED, Ordering::Release);
559 });
560
561 let mut out = [0u8; 8];
562 assert_eq!(
563 src.read(&mut out).unwrap_err().kind(),
564 io::ErrorKind::BrokenPipe
565 );
566 }
567
568 #[test]
569 fn a_probe_reads_only_what_has_arrived() {
570 let fx = Fixture::new();
574 fx.push(b"0123456789");
575 let mut src = PartialFileSource::open_for_probe(
576 &fx.path,
577 fx.written.clone(),
578 1_000,
579 fx.status_fn(),
580 ProbeMode::Full,
581 )
582 .unwrap();
583
584 let mut out = [0u8; 10];
585 src.read_exact(&mut out).unwrap();
586 assert_eq!(
587 src.read(&mut out).unwrap_err().kind(),
588 io::ErrorKind::UnexpectedEof,
589 "past the write head is an answer, not a wait"
590 );
591 }
592
593 #[test]
594 fn playback_reads_wait_for_what_has_not_arrived() {
595 let fx = Fixture::new();
599 fx.push(b"0123456789");
600 let mut src = PartialFileSource::open(
601 &fx.path,
602 fx.written.clone(),
603 1_000,
604 fx.status_fn(),
605 ProbeMode::Lengthless,
606 )
607 .unwrap();
608
609 let mut out = [0u8; 10];
610 src.read_exact(&mut out).unwrap();
611
612 std::thread::spawn({
613 let path = fx.path.clone();
614 let written = fx.written.clone();
615 move || {
616 std::thread::sleep(Duration::from_millis(20));
617 let mut f = std::fs::OpenOptions::new()
618 .append(true)
619 .open(&path)
620 .unwrap();
621 f.write_all(b"abcde").unwrap();
622 f.flush().unwrap();
623 written.fetch_add(5, Ordering::Release);
624 }
625 });
626
627 let n = src.read(&mut out).unwrap();
628 assert_eq!(&out[..n], b"abcde", "it waited rather than giving up");
629 }
630
631 #[test]
632 fn stalled_download_times_out() {
633 let fx = Fixture::new();
636 let mut src = fx.source_with_stall(1000, Duration::from_millis(20));
637 let mut out = [0u8; 8];
638 assert_eq!(
639 src.read(&mut out).unwrap_err().kind(),
640 io::ErrorKind::TimedOut
641 );
642 }
643
644 #[test]
645 fn completion_ends_the_read_at_the_true_length() {
646 let fx = Fixture::new();
649 fx.push(b"chunked");
650 fx.set(COMPLETE);
651
652 let mut out = Vec::new();
653 fx.source(0).read_to_end(&mut out).unwrap();
654 assert_eq!(out, b"chunked");
655 }
656
657 #[test]
658 fn survives_the_part_file_being_renamed() {
659 let fx = Fixture::new();
662 fx.push(b"0123456789");
663 let mut src = fx.source(10);
664
665 let mut out = [0u8; 4];
666 src.read_exact(&mut out).unwrap();
667 assert_eq!(&out, b"0123");
668
669 std::fs::rename(&fx.path, fx.path.with_extension("")).unwrap();
670 fx.set(COMPLETE);
671
672 let mut rest = Vec::new();
673 src.read_to_end(&mut rest).unwrap();
674 assert_eq!(rest, b"456789");
675 }
676
677 #[test]
678 fn byte_len_is_the_advertised_length_only() {
679 let fx = Fixture::new();
680 assert_eq!(fx.source(42).byte_len(), Some(42));
681 assert_eq!(fx.source(0).byte_len(), None);
684 }
685
686 #[test]
687 fn is_seekable_true() {
688 let fx = Fixture::new();
689 assert!(fx.source(0).is_seekable());
690 }
691}