1use std::{
15 cell::RefCell,
16 collections::VecDeque,
17 future::Future,
18 pin::Pin,
19 rc::Rc,
20 task::{Context, Poll, Waker},
21};
22
23pub const DEFAULT_CHUNK_LEN: usize = 256 * 1024;
25
26pub type ContentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
29
30#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
32pub enum ContentError {
33 #[error("content not found: {0}")]
35 NotFound(String),
36 #[error("content access denied: {0}")]
38 PermissionDenied(String),
39 #[error("content i/o failed: {0}")]
41 Io(String),
42 #[error("{0} is not supported on this platform")]
44 Unsupported(&'static str),
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Eq)]
49pub struct ContentMetadata {
50 pub name: String,
52 pub mime_type: Option<String>,
54 pub len: Option<u64>,
56 pub modified_millis: Option<u64>,
58 pub identifier: String,
61}
62
63impl ContentMetadata {
64 pub fn named(name: impl Into<String>) -> Self {
66 let name = name.into();
67 Self {
68 identifier: name.clone(),
69 name,
70 ..Self::default()
71 }
72 }
73
74 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
76 self.mime_type = Some(mime_type.into());
77 self
78 }
79
80 pub fn with_len(mut self, len: u64) -> Self {
82 self.len = Some(len);
83 self
84 }
85
86 pub fn with_modified_millis(mut self, modified_millis: u64) -> Self {
88 self.modified_millis = Some(modified_millis);
89 self
90 }
91
92 pub fn with_identifier(mut self, identifier: impl Into<String>) -> Self {
94 self.identifier = identifier.into();
95 self
96 }
97
98 pub fn extension(&self) -> Option<String> {
100 let (_, extension) = self.name.rsplit_once('.')?;
101 if extension.is_empty() {
102 None
103 } else {
104 Some(extension.to_ascii_lowercase())
105 }
106 }
107}
108
109pub trait ContentReader {
115 fn read_chunk(&self) -> ContentFuture<'_, Result<Option<Vec<u8>>, ContentError>>;
117}
118
119pub type ContentReaderRef = Rc<dyn ContentReader>;
121
122pub trait Content {
124 fn metadata(&self) -> ContentMetadata;
126
127 fn open(&self) -> ContentFuture<'_, Result<ContentReaderRef, ContentError>>;
129
130 fn read_all(&self) -> ContentFuture<'_, Result<Vec<u8>, ContentError>> {
133 Box::pin(async move {
134 let reader = self.open().await?;
135 drain_reader(&reader).await
136 })
137 }
138}
139
140pub type ContentHandle = Rc<dyn Content>;
142
143pub async fn drain_reader(reader: &ContentReaderRef) -> Result<Vec<u8>, ContentError> {
145 let mut bytes = Vec::new();
146 while let Some(chunk) = reader.read_chunk().await? {
147 bytes.extend_from_slice(&chunk);
148 }
149 Ok(bytes)
150}
151
152pub trait ContentSink {
154 fn write_chunk(&self, bytes: Vec<u8>) -> ContentFuture<'_, Result<(), ContentError>>;
156
157 fn finish(&self) -> ContentFuture<'_, Result<(), ContentError>>;
160}
161
162pub type ContentSinkRef = Rc<dyn ContentSink>;
164
165pub async fn write_all(sink: &ContentSinkRef, bytes: Vec<u8>) -> Result<(), ContentError> {
167 sink.write_chunk(bytes).await?;
168 sink.finish().await
169}
170
171pub enum ContentEntry {
173 File(ContentHandle),
175 Folder(ContentFolderRef),
177}
178
179impl ContentEntry {
180 pub fn metadata(&self) -> ContentMetadata {
182 match self {
183 ContentEntry::File(file) => file.metadata(),
184 ContentEntry::Folder(folder) => folder.metadata(),
185 }
186 }
187}
188
189pub trait ContentFolder {
191 fn metadata(&self) -> ContentMetadata;
193
194 fn entries(&self) -> ContentFuture<'_, Result<Vec<ContentEntry>, ContentError>>;
196
197 fn stream_files(&self) -> Option<ContentStreamRef> {
201 None
202 }
203}
204
205pub type ContentFolderRef = Rc<dyn ContentFolder>;
207
208pub trait ContentStream {
213 fn next(&self) -> ContentFuture<'_, Result<Option<ContentHandle>, ContentError>>;
215
216 fn produced(&self) -> Option<usize> {
218 None
219 }
220}
221
222pub type ContentStreamRef = Rc<dyn ContentStream>;
224
225pub fn folder_files(folder: ContentFolderRef) -> ContentStreamRef {
228 folder
229 .stream_files()
230 .unwrap_or_else(|| Rc::new(WalkStream::new(folder)))
231}
232
233pub async fn collect_stream(stream: &ContentStreamRef) -> Result<Vec<ContentHandle>, ContentError> {
236 let mut items = Vec::new();
237 while let Some(item) = stream.next().await? {
238 items.push(item);
239 }
240 Ok(items)
241}
242
243struct WalkStream {
244 pending: RefCell<Vec<ContentFolderRef>>,
245 ready: RefCell<VecDeque<ContentHandle>>,
246 produced: std::cell::Cell<usize>,
247}
248
249impl WalkStream {
250 fn new(root: ContentFolderRef) -> Self {
251 Self {
252 pending: RefCell::new(vec![root]),
253 ready: RefCell::new(VecDeque::new()),
254 produced: std::cell::Cell::new(0),
255 }
256 }
257}
258
259impl ContentStream for WalkStream {
260 fn next(&self) -> ContentFuture<'_, Result<Option<ContentHandle>, ContentError>> {
261 Box::pin(async move {
262 loop {
263 if let Some(file) = self.ready.borrow_mut().pop_front() {
264 self.produced.set(self.produced.get() + 1);
265 return Ok(Some(file));
266 }
267 let Some(folder) = self.pending.borrow_mut().pop() else {
268 return Ok(None);
269 };
270 for entry in folder.entries().await? {
271 match entry {
272 ContentEntry::File(file) => self.ready.borrow_mut().push_back(file),
273 ContentEntry::Folder(child) => self.pending.borrow_mut().push(child),
274 }
275 }
276 }
277 })
278 }
279
280 fn produced(&self) -> Option<usize> {
281 Some(self.produced.get())
282 }
283}
284
285pub struct ContentChannel {
292 shared: Rc<ChannelShared>,
293}
294
295struct ChannelShared {
296 ready: RefCell<VecDeque<ContentHandle>>,
297 error: RefCell<Option<ContentError>>,
298 closed: std::cell::Cell<bool>,
299 produced: std::cell::Cell<usize>,
300 waker: RefCell<Option<Waker>>,
301}
302
303impl ChannelShared {
304 fn wake(&self) {
305 if let Some(waker) = self.waker.borrow_mut().take() {
306 waker.wake();
307 }
308 }
309}
310
311impl Default for ContentChannel {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317impl ContentChannel {
318 pub fn new() -> Self {
320 Self {
321 shared: Rc::new(ChannelShared {
322 ready: RefCell::new(VecDeque::new()),
323 error: RefCell::new(None),
324 closed: std::cell::Cell::new(false),
325 produced: std::cell::Cell::new(0),
326 waker: RefCell::new(None),
327 }),
328 }
329 }
330
331 pub fn stream(&self) -> ContentStreamRef {
333 Rc::new(ChannelStream {
334 shared: Rc::clone(&self.shared),
335 })
336 }
337
338 pub fn push(&self, content: ContentHandle) {
340 if self.shared.closed.get() {
341 return;
342 }
343 self.shared.ready.borrow_mut().push_back(content);
344 self.shared.wake();
345 }
346
347 pub fn fail(&self, error: ContentError) {
349 if self.shared.closed.get() {
350 return;
351 }
352 *self.shared.error.borrow_mut() = Some(error);
353 self.shared.closed.set(true);
354 self.shared.wake();
355 }
356
357 pub fn close(&self) {
359 if self.shared.closed.get() {
360 return;
361 }
362 self.shared.closed.set(true);
363 self.shared.wake();
364 }
365
366 pub fn is_closed(&self) -> bool {
368 self.shared.closed.get()
369 }
370}
371
372struct ChannelStream {
373 shared: Rc<ChannelShared>,
374}
375
376impl ContentStream for ChannelStream {
377 fn next(&self) -> ContentFuture<'_, Result<Option<ContentHandle>, ContentError>> {
378 Box::pin(ChannelNext {
379 shared: Rc::clone(&self.shared),
380 })
381 }
382
383 fn produced(&self) -> Option<usize> {
384 Some(self.shared.produced.get())
385 }
386}
387
388struct ChannelNext {
389 shared: Rc<ChannelShared>,
390}
391
392impl Future for ChannelNext {
393 type Output = Result<Option<ContentHandle>, ContentError>;
394
395 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
396 if let Some(item) = self.shared.ready.borrow_mut().pop_front() {
397 self.shared.produced.set(self.shared.produced.get() + 1);
398 return Poll::Ready(Ok(Some(item)));
399 }
400 if let Some(error) = self.shared.error.borrow_mut().take() {
401 return Poll::Ready(Err(error));
402 }
403 if self.shared.closed.get() {
404 return Poll::Ready(Ok(None));
405 }
406 *self.shared.waker.borrow_mut() = Some(context.waker().clone());
407 Poll::Pending
408 }
409}
410
411pub struct BytesContent {
413 metadata: ContentMetadata,
414 bytes: Rc<Vec<u8>>,
415}
416
417impl BytesContent {
418 pub fn new(metadata: ContentMetadata, bytes: Vec<u8>) -> Self {
420 let len = bytes.len() as u64;
421 Self {
422 metadata: ContentMetadata {
423 len: Some(len),
424 ..metadata
425 },
426 bytes: Rc::new(bytes),
427 }
428 }
429
430 pub fn named(name: impl Into<String>, bytes: Vec<u8>) -> Self {
432 Self::new(ContentMetadata::named(name), bytes)
433 }
434
435 pub fn handle(self) -> ContentHandle {
437 Rc::new(self)
438 }
439}
440
441impl Content for BytesContent {
442 fn metadata(&self) -> ContentMetadata {
443 self.metadata.clone()
444 }
445
446 fn open(&self) -> ContentFuture<'_, Result<ContentReaderRef, ContentError>> {
447 let bytes = Rc::clone(&self.bytes);
448 Box::pin(async move {
449 Ok(Rc::new(BytesReader {
450 bytes,
451 offset: std::cell::Cell::new(0),
452 }) as ContentReaderRef)
453 })
454 }
455
456 fn read_all(&self) -> ContentFuture<'_, Result<Vec<u8>, ContentError>> {
457 let bytes = Rc::clone(&self.bytes);
458 Box::pin(async move { Ok(bytes.as_ref().clone()) })
459 }
460}
461
462struct BytesReader {
463 bytes: Rc<Vec<u8>>,
464 offset: std::cell::Cell<usize>,
465}
466
467impl ContentReader for BytesReader {
468 fn read_chunk(&self) -> ContentFuture<'_, Result<Option<Vec<u8>>, ContentError>> {
469 Box::pin(async move {
470 let start = self.offset.get();
471 if start >= self.bytes.len() {
472 return Ok(None);
473 }
474 let end = (start + DEFAULT_CHUNK_LEN).min(self.bytes.len());
475 self.offset.set(end);
476 Ok(Some(self.bytes[start..end].to_vec()))
477 })
478 }
479}
480
481pub struct ReadyFolder {
483 metadata: ContentMetadata,
484 entries: RefCell<Option<Vec<ContentEntry>>>,
485}
486
487impl ReadyFolder {
488 pub fn new(metadata: ContentMetadata, entries: Vec<ContentEntry>) -> Self {
490 Self {
491 metadata,
492 entries: RefCell::new(Some(entries)),
493 }
494 }
495
496 pub fn handle(self) -> ContentFolderRef {
498 Rc::new(self)
499 }
500}
501
502impl ContentFolder for ReadyFolder {
503 fn metadata(&self) -> ContentMetadata {
504 self.metadata.clone()
505 }
506
507 fn entries(&self) -> ContentFuture<'_, Result<Vec<ContentEntry>, ContentError>> {
508 Box::pin(async move {
509 self.entries
510 .borrow_mut()
511 .take()
512 .ok_or_else(|| ContentError::Io("folder entries already consumed".into()))
513 })
514 }
515}
516
517pub trait ContentResolver {
524 fn resolve(&self, uri: &str) -> Option<ContentHandle>;
526}
527
528pub type ContentResolverRef = Rc<dyn ContentResolver>;
530
531thread_local! {
532 static PLATFORM_RESOLVER: RefCell<Option<ContentResolverRef>> = const { RefCell::new(None) };
533}
534
535pub fn set_platform_content_resolver(resolver: ContentResolverRef) {
538 PLATFORM_RESOLVER.with(|cell| *cell.borrow_mut() = Some(resolver));
539}
540
541pub fn clear_platform_content_resolver() {
543 PLATFORM_RESOLVER.with(|cell| *cell.borrow_mut() = None);
544}
545
546pub fn resolve_content(uri: &str) -> Option<ContentHandle> {
549 if let Some(resolver) = PLATFORM_RESOLVER.with(|cell| cell.borrow().clone())
550 && let Some(content) = resolver.resolve(uri)
551 {
552 return Some(content);
553 }
554 #[cfg(not(target_arch = "wasm32"))]
555 {
556 let path = uri.strip_prefix("file://").unwrap_or(uri);
557 if !path.contains("://") {
558 return Some(file_content(path));
559 }
560 }
561 let _ = uri;
562 None
563}
564
565#[cfg(not(target_arch = "wasm32"))]
566pub use file::{FileContent, FileFolder, FileSink, file_content, file_folder};
567
568#[cfg(not(target_arch = "wasm32"))]
569mod file;
570
571pub fn percent_decode(input: &str) -> Option<String> {
585 let bytes = input.as_bytes();
586 let mut out = Vec::with_capacity(bytes.len());
587 let mut index = 0;
588 while index < bytes.len() {
589 if bytes[index] == b'%' {
590 let high = bytes.get(index + 1).copied().and_then(hex_value)?;
591 let low = bytes.get(index + 2).copied().and_then(hex_value)?;
592 out.push((high << 4) | low);
593 index += 3;
594 } else {
595 out.push(bytes[index]);
596 index += 1;
597 }
598 }
599 String::from_utf8(out).ok()
600}
601
602pub fn percent_decode_lossy(input: &str) -> String {
609 let bytes = input.as_bytes();
610 let mut out = Vec::with_capacity(bytes.len());
611 let mut index = 0;
612 while index < bytes.len() {
613 if bytes[index] == b'%' {
614 let pair = hex_value(bytes.get(index + 1).copied().unwrap_or(0))
615 .zip(hex_value(bytes.get(index + 2).copied().unwrap_or(0)));
616 if let Some((high, low)) = pair {
617 out.push((high << 4) | low);
618 index += 3;
619 continue;
620 }
621 }
622 out.push(bytes[index]);
623 index += 1;
624 }
625 String::from_utf8_lossy(&out).into_owned()
626}
627
628fn hex_value(byte: u8) -> Option<u8> {
629 match byte {
630 b'0'..=b'9' => Some(byte - b'0'),
631 b'a'..=b'f' => Some(byte - b'a' + 10),
632 b'A'..=b'F' => Some(byte - b'A' + 10),
633 _ => None,
634 }
635}
636
637#[cfg(test)]
638mod tests {
639
640 #[test]
641 fn a_uri_that_cannot_be_decoded_exactly_is_refused_but_still_displays() {
642 assert_eq!(
643 percent_decode("Trip%20Photos").as_deref(),
644 Some("Trip Photos")
645 );
646 assert_eq!(percent_decode_lossy("Trip%20Photos"), "Trip Photos");
647
648 assert_eq!(percent_decode("bad%FFname"), None);
649 assert_eq!(percent_decode_lossy("bad%FFname"), "bad\u{fffd}name");
650
651 assert_eq!(percent_decode("cut%4"), None);
652 assert_eq!(percent_decode_lossy("cut%4"), "cut%4");
653
654 assert_eq!(percent_decode("100%zz"), None);
655 assert_eq!(percent_decode_lossy("100%zz"), "100%zz");
656 }
657
658 use super::*;
659
660 fn block<T>(future: impl Future<Output = T>) -> T {
661 pollster::block_on(future)
662 }
663
664 #[test]
665 fn metadata_reports_the_lowercase_extension() {
666 let metadata = ContentMetadata::named("Scan.PNG");
667 assert_eq!(metadata.extension().as_deref(), Some("png"));
668 assert_eq!(ContentMetadata::named("noext").extension(), None);
669 assert_eq!(ContentMetadata::named("trailing.").extension(), None);
670 }
671
672 #[test]
673 fn metadata_carries_what_a_provider_knew_about_the_item() {
674 let metadata = ContentMetadata::named("Scan.png")
675 .with_len(2_048)
676 .with_modified_millis(1_700_000_000_000)
677 .with_identifier("content://provider/17")
678 .with_mime_type("image/png");
679
680 assert_eq!(metadata.name, "Scan.png");
681 assert_eq!(metadata.len, Some(2_048));
682 assert_eq!(metadata.modified_millis, Some(1_700_000_000_000));
683 assert_eq!(metadata.identifier, "content://provider/17");
684 assert_eq!(metadata.mime_type.as_deref(), Some("image/png"));
685 let bare = ContentMetadata::named("Scan.png");
686 assert_eq!(bare.len, None);
687 assert_eq!(bare.modified_millis, None);
688 assert_eq!(bare.mime_type, None);
689 assert_eq!(bare.identifier, "Scan.png");
690 }
691
692 #[test]
693 fn a_channel_reports_when_it_has_ended_and_ignores_what_arrives_after() {
694 let channel = ContentChannel::new();
695 assert!(!channel.is_closed());
696
697 channel.close();
698 assert!(channel.is_closed());
699
700 channel.push(BytesContent::named("late.bin", vec![1]).handle());
701 channel.fail(ContentError::Unsupported("after close"));
702 assert!(channel.is_closed());
703
704 let stream = channel.stream();
705 assert!(
706 block(stream.next())
707 .expect("a closed channel ends cleanly")
708 .is_none(),
709 "a channel closed before anything was pushed must yield nothing"
710 );
711 }
712
713 #[test]
714 fn bytes_content_streams_in_chunks_and_reads_whole() {
715 let payload = vec![7u8; DEFAULT_CHUNK_LEN + 11];
716 let content = BytesContent::named("blob.bin", payload.clone()).handle();
717 assert_eq!(content.metadata().len, Some(payload.len() as u64));
718
719 let chunks = block(async {
720 let reader = content.open().await.unwrap();
721 let mut sizes = Vec::new();
722 while let Some(chunk) = reader.read_chunk().await.unwrap() {
723 sizes.push(chunk.len());
724 }
725 sizes
726 });
727 assert_eq!(chunks, vec![DEFAULT_CHUNK_LEN, 11]);
728 assert_eq!(block(content.read_all()).unwrap(), payload);
729 }
730
731 #[test]
732 fn walking_a_tree_yields_every_file_depth_first() {
733 let nested = ReadyFolder::new(
734 ContentMetadata::named("nested"),
735 vec![ContentEntry::File(
736 BytesContent::named("b.txt", b"b".to_vec()).handle(),
737 )],
738 )
739 .handle();
740 let root = ReadyFolder::new(
741 ContentMetadata::named("root"),
742 vec![
743 ContentEntry::File(BytesContent::named("a.txt", b"a".to_vec()).handle()),
744 ContentEntry::Folder(nested),
745 ],
746 )
747 .handle();
748
749 let stream = folder_files(root);
750 let files = block(collect_stream(&stream)).unwrap();
751 let names: Vec<String> = files.iter().map(|file| file.metadata().name).collect();
752 assert_eq!(names, vec!["a.txt", "b.txt"]);
753 assert_eq!(stream.produced(), Some(2));
754 }
755
756 #[test]
757 fn a_channel_wakes_its_collector_instead_of_being_polled() {
758 let channel = Rc::new(ContentChannel::new());
759 let stream = channel.stream();
760
761 let mut future = Box::pin(stream.next());
762 let waker = Waker::noop().clone();
763 let mut context = Context::from_waker(&waker);
764 assert!(future.as_mut().poll(&mut context).is_pending());
765
766 channel.push(BytesContent::named("late.txt", b"late".to_vec()).handle());
767 let Poll::Ready(Ok(Some(item))) = future.as_mut().poll(&mut context) else {
768 panic!("the pushed item should have completed the pending collector");
769 };
770 assert_eq!(item.metadata().name, "late.txt");
771
772 channel.close();
773 assert!(matches!(block(stream.next()), Ok(None)));
774 assert_eq!(stream.produced(), Some(1));
775 }
776
777 #[test]
778 fn a_failed_channel_reports_the_error_once_then_ends() {
779 let channel = ContentChannel::new();
780 let stream = channel.stream();
781 channel.fail(ContentError::Io("provider died".into()));
782 let Err(failure) = block(stream.next()) else {
783 panic!("a failed channel reports its error to the collector");
784 };
785 assert_eq!(failure, ContentError::Io("provider died".into()));
786 assert!(matches!(block(stream.next()), Ok(None)));
787 }
788}