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 {
245 pending: RefCell<Vec<ContentFolderRef>>,
246 ready: RefCell<VecDeque<ContentHandle>>,
247 produced: std::cell::Cell<usize>,
248}
249
250impl WalkStream {
251 fn new(root: ContentFolderRef) -> Self {
252 Self {
253 pending: RefCell::new(vec![root]),
254 ready: RefCell::new(VecDeque::new()),
255 produced: std::cell::Cell::new(0),
256 }
257 }
258}
259
260impl ContentStream for WalkStream {
261 fn next(&self) -> ContentFuture<'_, Result<Option<ContentHandle>, ContentError>> {
262 Box::pin(async move {
263 loop {
264 if let Some(file) = self.ready.borrow_mut().pop_front() {
265 self.produced.set(self.produced.get() + 1);
266 return Ok(Some(file));
267 }
268 let Some(folder) = self.pending.borrow_mut().pop() else {
269 return Ok(None);
270 };
271 for entry in folder.entries().await? {
272 match entry {
273 ContentEntry::File(file) => self.ready.borrow_mut().push_back(file),
274 ContentEntry::Folder(child) => self.pending.borrow_mut().push(child),
275 }
276 }
277 }
278 })
279 }
280
281 fn produced(&self) -> Option<usize> {
282 Some(self.produced.get())
283 }
284}
285
286pub struct ContentChannel {
293 shared: Rc<ChannelShared>,
294}
295
296struct ChannelShared {
297 ready: RefCell<VecDeque<ContentHandle>>,
298 error: RefCell<Option<ContentError>>,
299 closed: std::cell::Cell<bool>,
300 produced: std::cell::Cell<usize>,
301 waker: RefCell<Option<Waker>>,
302}
303
304impl ChannelShared {
305 fn wake(&self) {
306 if let Some(waker) = self.waker.borrow_mut().take() {
307 waker.wake();
308 }
309 }
310}
311
312impl Default for ContentChannel {
313 fn default() -> Self {
314 Self::new()
315 }
316}
317
318impl ContentChannel {
319 pub fn new() -> Self {
321 Self {
322 shared: Rc::new(ChannelShared {
323 ready: RefCell::new(VecDeque::new()),
324 error: RefCell::new(None),
325 closed: std::cell::Cell::new(false),
326 produced: std::cell::Cell::new(0),
327 waker: RefCell::new(None),
328 }),
329 }
330 }
331
332 pub fn stream(&self) -> ContentStreamRef {
334 Rc::new(ChannelStream {
335 shared: Rc::clone(&self.shared),
336 })
337 }
338
339 pub fn push(&self, content: ContentHandle) {
341 if self.shared.closed.get() {
342 return;
343 }
344 self.shared.ready.borrow_mut().push_back(content);
345 self.shared.wake();
346 }
347
348 pub fn fail(&self, error: ContentError) {
350 if self.shared.closed.get() {
351 return;
352 }
353 *self.shared.error.borrow_mut() = Some(error);
354 self.shared.closed.set(true);
355 self.shared.wake();
356 }
357
358 pub fn close(&self) {
360 if self.shared.closed.get() {
361 return;
362 }
363 self.shared.closed.set(true);
364 self.shared.wake();
365 }
366
367 pub fn is_closed(&self) -> bool {
369 self.shared.closed.get()
370 }
371}
372
373struct ChannelStream {
374 shared: Rc<ChannelShared>,
375}
376
377impl ContentStream for ChannelStream {
378 fn next(&self) -> ContentFuture<'_, Result<Option<ContentHandle>, ContentError>> {
379 Box::pin(ChannelNext {
380 shared: Rc::clone(&self.shared),
381 })
382 }
383
384 fn produced(&self) -> Option<usize> {
385 Some(self.shared.produced.get())
386 }
387}
388
389struct ChannelNext {
390 shared: Rc<ChannelShared>,
391}
392
393impl Future for ChannelNext {
394 type Output = Result<Option<ContentHandle>, ContentError>;
395
396 fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
397 if let Some(item) = self.shared.ready.borrow_mut().pop_front() {
398 self.shared.produced.set(self.shared.produced.get() + 1);
399 return Poll::Ready(Ok(Some(item)));
400 }
401 if let Some(error) = self.shared.error.borrow_mut().take() {
402 return Poll::Ready(Err(error));
403 }
404 if self.shared.closed.get() {
405 return Poll::Ready(Ok(None));
406 }
407 *self.shared.waker.borrow_mut() = Some(context.waker().clone());
408 Poll::Pending
409 }
410}
411
412pub struct BytesContent {
414 metadata: ContentMetadata,
415 bytes: Rc<Vec<u8>>,
416}
417
418impl BytesContent {
419 pub fn new(metadata: ContentMetadata, bytes: Vec<u8>) -> Self {
421 let len = bytes.len() as u64;
422 Self {
423 metadata: ContentMetadata {
424 len: Some(len),
425 ..metadata
426 },
427 bytes: Rc::new(bytes),
428 }
429 }
430
431 pub fn named(name: impl Into<String>, bytes: Vec<u8>) -> Self {
433 Self::new(ContentMetadata::named(name), bytes)
434 }
435
436 pub fn handle(self) -> ContentHandle {
438 Rc::new(self)
439 }
440}
441
442impl Content for BytesContent {
443 fn metadata(&self) -> ContentMetadata {
444 self.metadata.clone()
445 }
446
447 fn open(&self) -> ContentFuture<'_, Result<ContentReaderRef, ContentError>> {
448 let bytes = Rc::clone(&self.bytes);
449 Box::pin(async move {
450 Ok(Rc::new(BytesReader {
451 bytes,
452 offset: std::cell::Cell::new(0),
453 }) as ContentReaderRef)
454 })
455 }
456
457 fn read_all(&self) -> ContentFuture<'_, Result<Vec<u8>, ContentError>> {
458 let bytes = Rc::clone(&self.bytes);
459 Box::pin(async move { Ok(bytes.as_ref().clone()) })
460 }
461}
462
463struct BytesReader {
464 bytes: Rc<Vec<u8>>,
465 offset: std::cell::Cell<usize>,
466}
467
468impl ContentReader for BytesReader {
469 fn read_chunk(&self) -> ContentFuture<'_, Result<Option<Vec<u8>>, ContentError>> {
470 Box::pin(async move {
471 let start = self.offset.get();
472 if start >= self.bytes.len() {
473 return Ok(None);
474 }
475 let end = (start + DEFAULT_CHUNK_LEN).min(self.bytes.len());
476 self.offset.set(end);
477 Ok(Some(self.bytes[start..end].to_vec()))
478 })
479 }
480}
481
482pub struct ReadyFolder {
484 metadata: ContentMetadata,
485 entries: RefCell<Option<Vec<ContentEntry>>>,
486}
487
488impl ReadyFolder {
489 pub fn new(metadata: ContentMetadata, entries: Vec<ContentEntry>) -> Self {
491 Self {
492 metadata,
493 entries: RefCell::new(Some(entries)),
494 }
495 }
496
497 pub fn handle(self) -> ContentFolderRef {
499 Rc::new(self)
500 }
501}
502
503impl ContentFolder for ReadyFolder {
504 fn metadata(&self) -> ContentMetadata {
505 self.metadata.clone()
506 }
507
508 fn entries(&self) -> ContentFuture<'_, Result<Vec<ContentEntry>, ContentError>> {
509 Box::pin(async move {
510 self.entries
511 .borrow_mut()
512 .take()
513 .ok_or_else(|| ContentError::Io("folder entries already consumed".into()))
514 })
515 }
516}
517
518pub trait ContentResolver {
525 fn resolve(&self, uri: &str) -> Option<ContentHandle>;
527}
528
529pub type ContentResolverRef = Rc<dyn ContentResolver>;
531
532thread_local! {
533 static PLATFORM_RESOLVER: RefCell<Option<ContentResolverRef>> = const { RefCell::new(None) };
534}
535
536pub fn set_platform_content_resolver(resolver: ContentResolverRef) {
539 PLATFORM_RESOLVER.with(|cell| *cell.borrow_mut() = Some(resolver));
540}
541
542pub fn clear_platform_content_resolver() {
544 PLATFORM_RESOLVER.with(|cell| *cell.borrow_mut() = None);
545}
546
547pub fn resolve_content(uri: &str) -> Option<ContentHandle> {
550 if let Some(resolver) = PLATFORM_RESOLVER.with(|cell| cell.borrow().clone()) {
551 if let Some(content) = resolver.resolve(uri) {
552 return Some(content);
553 }
554 }
555 #[cfg(not(target_arch = "wasm32"))]
556 {
557 let path = uri.strip_prefix("file://").unwrap_or(uri);
558 if !path.contains("://") {
559 return Some(file_content(path));
560 }
561 }
562 let _ = uri;
563 None
564}
565
566#[cfg(not(target_arch = "wasm32"))]
567pub use file::{file_content, file_folder, FileContent, FileFolder, FileSink};
568
569#[cfg(not(target_arch = "wasm32"))]
570mod file;
571
572pub fn percent_decode(input: &str) -> Option<String> {
586 let bytes = input.as_bytes();
587 let mut out = Vec::with_capacity(bytes.len());
588 let mut index = 0;
589 while index < bytes.len() {
590 if bytes[index] == b'%' {
591 let high = bytes.get(index + 1).copied().and_then(hex_value)?;
592 let low = bytes.get(index + 2).copied().and_then(hex_value)?;
593 out.push((high << 4) | low);
594 index += 3;
595 } else {
596 out.push(bytes[index]);
597 index += 1;
598 }
599 }
600 String::from_utf8(out).ok()
601}
602
603pub fn percent_decode_lossy(input: &str) -> String {
610 let bytes = input.as_bytes();
611 let mut out = Vec::with_capacity(bytes.len());
612 let mut index = 0;
613 while index < bytes.len() {
614 if bytes[index] == b'%' {
615 let pair = hex_value(bytes.get(index + 1).copied().unwrap_or(0))
616 .zip(hex_value(bytes.get(index + 2).copied().unwrap_or(0)));
617 if let Some((high, low)) = pair {
618 out.push((high << 4) | low);
619 index += 3;
620 continue;
621 }
622 }
623 out.push(bytes[index]);
624 index += 1;
625 }
626 String::from_utf8_lossy(&out).into_owned()
627}
628
629fn hex_value(byte: u8) -> Option<u8> {
630 match byte {
631 b'0'..=b'9' => Some(byte - b'0'),
632 b'a'..=b'f' => Some(byte - b'a' + 10),
633 b'A'..=b'F' => Some(byte - b'A' + 10),
634 _ => None,
635 }
636}
637
638#[cfg(test)]
639mod tests {
640
641 #[test]
648 fn a_uri_that_cannot_be_decoded_exactly_is_refused_but_still_displays() {
649 assert_eq!(
650 percent_decode("Trip%20Photos").as_deref(),
651 Some("Trip Photos")
652 );
653 assert_eq!(percent_decode_lossy("Trip%20Photos"), "Trip Photos");
654
655 assert_eq!(percent_decode("bad%FFname"), None);
657 assert_eq!(percent_decode_lossy("bad%FFname"), "bad\u{fffd}name");
658
659 assert_eq!(percent_decode("cut%4"), None);
661 assert_eq!(percent_decode_lossy("cut%4"), "cut%4");
662
663 assert_eq!(percent_decode("100%zz"), None);
665 assert_eq!(percent_decode_lossy("100%zz"), "100%zz");
666 }
667
668 use super::*;
669
670 fn block<T>(future: impl Future<Output = T>) -> T {
671 pollster::block_on(future)
672 }
673
674 #[test]
675 fn metadata_reports_the_lowercase_extension() {
676 let metadata = ContentMetadata::named("Scan.PNG");
677 assert_eq!(metadata.extension().as_deref(), Some("png"));
678 assert_eq!(ContentMetadata::named("noext").extension(), None);
679 assert_eq!(ContentMetadata::named("trailing.").extension(), None);
680 }
681
682 #[test]
683 fn metadata_carries_what_a_provider_knew_about_the_item() {
684 let metadata = ContentMetadata::named("Scan.png")
685 .with_len(2_048)
686 .with_modified_millis(1_700_000_000_000)
687 .with_identifier("content://provider/17")
688 .with_mime_type("image/png");
689
690 assert_eq!(metadata.name, "Scan.png");
691 assert_eq!(metadata.len, Some(2_048));
692 assert_eq!(metadata.modified_millis, Some(1_700_000_000_000));
693 assert_eq!(metadata.identifier, "content://provider/17");
694 assert_eq!(metadata.mime_type.as_deref(), Some("image/png"));
695 let bare = ContentMetadata::named("Scan.png");
699 assert_eq!(bare.len, None);
700 assert_eq!(bare.modified_millis, None);
701 assert_eq!(bare.mime_type, None);
702 assert_eq!(bare.identifier, "Scan.png");
703 }
704
705 #[test]
706 fn a_channel_reports_when_it_has_ended_and_ignores_what_arrives_after() {
707 let channel = ContentChannel::new();
708 assert!(!channel.is_closed());
709
710 channel.close();
711 assert!(channel.is_closed());
712
713 channel.push(BytesContent::named("late.bin", vec![1]).handle());
715 channel.fail(ContentError::Unsupported("after close"));
716 assert!(channel.is_closed());
717
718 let stream = channel.stream();
719 assert!(
720 block(stream.next())
721 .expect("a closed channel ends cleanly")
722 .is_none(),
723 "a channel closed before anything was pushed must yield nothing"
724 );
725 }
726
727 #[test]
728 fn bytes_content_streams_in_chunks_and_reads_whole() {
729 let payload = vec![7u8; DEFAULT_CHUNK_LEN + 11];
730 let content = BytesContent::named("blob.bin", payload.clone()).handle();
731 assert_eq!(content.metadata().len, Some(payload.len() as u64));
732
733 let chunks = block(async {
734 let reader = content.open().await.unwrap();
735 let mut sizes = Vec::new();
736 while let Some(chunk) = reader.read_chunk().await.unwrap() {
737 sizes.push(chunk.len());
738 }
739 sizes
740 });
741 assert_eq!(chunks, vec![DEFAULT_CHUNK_LEN, 11]);
742 assert_eq!(block(content.read_all()).unwrap(), payload);
743 }
744
745 #[test]
746 fn walking_a_tree_yields_every_file_depth_first() {
747 let nested = ReadyFolder::new(
748 ContentMetadata::named("nested"),
749 vec![ContentEntry::File(
750 BytesContent::named("b.txt", b"b".to_vec()).handle(),
751 )],
752 )
753 .handle();
754 let root = ReadyFolder::new(
755 ContentMetadata::named("root"),
756 vec![
757 ContentEntry::File(BytesContent::named("a.txt", b"a".to_vec()).handle()),
758 ContentEntry::Folder(nested),
759 ],
760 )
761 .handle();
762
763 let stream = folder_files(root);
764 let files = block(collect_stream(&stream)).unwrap();
765 let names: Vec<String> = files.iter().map(|file| file.metadata().name).collect();
766 assert_eq!(names, vec!["a.txt", "b.txt"]);
767 assert_eq!(stream.produced(), Some(2));
768 }
769
770 #[test]
771 fn a_channel_wakes_its_collector_instead_of_being_polled() {
772 let channel = Rc::new(ContentChannel::new());
773 let stream = channel.stream();
774
775 let mut future = Box::pin(stream.next());
776 let waker = Waker::noop().clone();
777 let mut context = Context::from_waker(&waker);
778 assert!(future.as_mut().poll(&mut context).is_pending());
779
780 channel.push(BytesContent::named("late.txt", b"late".to_vec()).handle());
781 let Poll::Ready(Ok(Some(item))) = future.as_mut().poll(&mut context) else {
782 panic!("the pushed item should have completed the pending collector");
783 };
784 assert_eq!(item.metadata().name, "late.txt");
785
786 channel.close();
787 assert!(matches!(block(stream.next()), Ok(None)));
788 assert_eq!(stream.produced(), Some(1));
789 }
790
791 #[test]
792 fn a_failed_channel_reports_the_error_once_then_ends() {
793 let channel = ContentChannel::new();
794 let stream = channel.stream();
795 channel.fail(ContentError::Io("provider died".into()));
796 let Err(failure) = block(stream.next()) else {
797 panic!("a failed channel reports its error to the collector");
798 };
799 assert_eq!(failure, ContentError::Io("provider died".into()));
800 assert!(matches!(block(stream.next()), Ok(None)));
801 }
802}