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