use std::{
cell::RefCell,
collections::VecDeque,
future::Future,
pin::Pin,
rc::Rc,
task::{Context, Poll, Waker},
};
pub const DEFAULT_CHUNK_LEN: usize = 256 * 1024;
pub type ContentFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
pub enum ContentError {
#[error("content not found: {0}")]
NotFound(String),
#[error("content access denied: {0}")]
PermissionDenied(String),
#[error("content i/o failed: {0}")]
Io(String),
#[error("{0} is not supported on this platform")]
Unsupported(&'static str),
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ContentMetadata {
pub name: String,
pub mime_type: Option<String>,
pub len: Option<u64>,
pub modified_millis: Option<u64>,
pub identifier: String,
}
impl ContentMetadata {
pub fn named(name: impl Into<String>) -> Self {
let name = name.into();
Self {
identifier: name.clone(),
name,
..Self::default()
}
}
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
pub fn with_len(mut self, len: u64) -> Self {
self.len = Some(len);
self
}
pub fn with_modified_millis(mut self, modified_millis: u64) -> Self {
self.modified_millis = Some(modified_millis);
self
}
pub fn with_identifier(mut self, identifier: impl Into<String>) -> Self {
self.identifier = identifier.into();
self
}
pub fn extension(&self) -> Option<String> {
let (_, extension) = self.name.rsplit_once('.')?;
if extension.is_empty() {
None
} else {
Some(extension.to_ascii_lowercase())
}
}
}
pub trait ContentReader {
fn read_chunk(&self) -> ContentFuture<'_, Result<Option<Vec<u8>>, ContentError>>;
}
pub type ContentReaderRef = Rc<dyn ContentReader>;
pub trait Content {
fn metadata(&self) -> ContentMetadata;
fn open(&self) -> ContentFuture<'_, Result<ContentReaderRef, ContentError>>;
fn read_all(&self) -> ContentFuture<'_, Result<Vec<u8>, ContentError>> {
Box::pin(async move {
let reader = self.open().await?;
drain_reader(&reader).await
})
}
}
pub type ContentHandle = Rc<dyn Content>;
pub async fn drain_reader(reader: &ContentReaderRef) -> Result<Vec<u8>, ContentError> {
let mut bytes = Vec::new();
while let Some(chunk) = reader.read_chunk().await? {
bytes.extend_from_slice(&chunk);
}
Ok(bytes)
}
pub trait ContentSink {
fn write_chunk(&self, bytes: Vec<u8>) -> ContentFuture<'_, Result<(), ContentError>>;
fn finish(&self) -> ContentFuture<'_, Result<(), ContentError>>;
}
pub type ContentSinkRef = Rc<dyn ContentSink>;
pub async fn write_all(sink: &ContentSinkRef, bytes: Vec<u8>) -> Result<(), ContentError> {
sink.write_chunk(bytes).await?;
sink.finish().await
}
pub enum ContentEntry {
File(ContentHandle),
Folder(ContentFolderRef),
}
impl ContentEntry {
pub fn metadata(&self) -> ContentMetadata {
match self {
ContentEntry::File(file) => file.metadata(),
ContentEntry::Folder(folder) => folder.metadata(),
}
}
}
pub trait ContentFolder {
fn metadata(&self) -> ContentMetadata;
fn entries(&self) -> ContentFuture<'_, Result<Vec<ContentEntry>, ContentError>>;
fn stream_files(&self) -> Option<ContentStreamRef> {
None
}
}
pub type ContentFolderRef = Rc<dyn ContentFolder>;
pub trait ContentStream {
fn next(&self) -> ContentFuture<'_, Result<Option<ContentHandle>, ContentError>>;
fn produced(&self) -> Option<usize> {
None
}
}
pub type ContentStreamRef = Rc<dyn ContentStream>;
pub fn folder_files(folder: ContentFolderRef) -> ContentStreamRef {
folder
.stream_files()
.unwrap_or_else(|| Rc::new(WalkStream::new(folder)))
}
pub async fn collect_stream(stream: &ContentStreamRef) -> Result<Vec<ContentHandle>, ContentError> {
let mut items = Vec::new();
while let Some(item) = stream.next().await? {
items.push(item);
}
Ok(items)
}
struct WalkStream {
pending: RefCell<Vec<ContentFolderRef>>,
ready: RefCell<VecDeque<ContentHandle>>,
produced: std::cell::Cell<usize>,
}
impl WalkStream {
fn new(root: ContentFolderRef) -> Self {
Self {
pending: RefCell::new(vec![root]),
ready: RefCell::new(VecDeque::new()),
produced: std::cell::Cell::new(0),
}
}
}
impl ContentStream for WalkStream {
fn next(&self) -> ContentFuture<'_, Result<Option<ContentHandle>, ContentError>> {
Box::pin(async move {
loop {
if let Some(file) = self.ready.borrow_mut().pop_front() {
self.produced.set(self.produced.get() + 1);
return Ok(Some(file));
}
let Some(folder) = self.pending.borrow_mut().pop() else {
return Ok(None);
};
for entry in folder.entries().await? {
match entry {
ContentEntry::File(file) => self.ready.borrow_mut().push_back(file),
ContentEntry::Folder(child) => self.pending.borrow_mut().push(child),
}
}
}
})
}
fn produced(&self) -> Option<usize> {
Some(self.produced.get())
}
}
pub struct ContentChannel {
shared: Rc<ChannelShared>,
}
struct ChannelShared {
ready: RefCell<VecDeque<ContentHandle>>,
error: RefCell<Option<ContentError>>,
closed: std::cell::Cell<bool>,
produced: std::cell::Cell<usize>,
waker: RefCell<Option<Waker>>,
}
impl ChannelShared {
fn wake(&self) {
if let Some(waker) = self.waker.borrow_mut().take() {
waker.wake();
}
}
}
impl Default for ContentChannel {
fn default() -> Self {
Self::new()
}
}
impl ContentChannel {
pub fn new() -> Self {
Self {
shared: Rc::new(ChannelShared {
ready: RefCell::new(VecDeque::new()),
error: RefCell::new(None),
closed: std::cell::Cell::new(false),
produced: std::cell::Cell::new(0),
waker: RefCell::new(None),
}),
}
}
pub fn stream(&self) -> ContentStreamRef {
Rc::new(ChannelStream {
shared: Rc::clone(&self.shared),
})
}
pub fn push(&self, content: ContentHandle) {
if self.shared.closed.get() {
return;
}
self.shared.ready.borrow_mut().push_back(content);
self.shared.wake();
}
pub fn fail(&self, error: ContentError) {
if self.shared.closed.get() {
return;
}
*self.shared.error.borrow_mut() = Some(error);
self.shared.closed.set(true);
self.shared.wake();
}
pub fn close(&self) {
if self.shared.closed.get() {
return;
}
self.shared.closed.set(true);
self.shared.wake();
}
pub fn is_closed(&self) -> bool {
self.shared.closed.get()
}
}
struct ChannelStream {
shared: Rc<ChannelShared>,
}
impl ContentStream for ChannelStream {
fn next(&self) -> ContentFuture<'_, Result<Option<ContentHandle>, ContentError>> {
Box::pin(ChannelNext {
shared: Rc::clone(&self.shared),
})
}
fn produced(&self) -> Option<usize> {
Some(self.shared.produced.get())
}
}
struct ChannelNext {
shared: Rc<ChannelShared>,
}
impl Future for ChannelNext {
type Output = Result<Option<ContentHandle>, ContentError>;
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(item) = self.shared.ready.borrow_mut().pop_front() {
self.shared.produced.set(self.shared.produced.get() + 1);
return Poll::Ready(Ok(Some(item)));
}
if let Some(error) = self.shared.error.borrow_mut().take() {
return Poll::Ready(Err(error));
}
if self.shared.closed.get() {
return Poll::Ready(Ok(None));
}
*self.shared.waker.borrow_mut() = Some(context.waker().clone());
Poll::Pending
}
}
pub struct BytesContent {
metadata: ContentMetadata,
bytes: Rc<Vec<u8>>,
}
impl BytesContent {
pub fn new(metadata: ContentMetadata, bytes: Vec<u8>) -> Self {
let len = bytes.len() as u64;
Self {
metadata: ContentMetadata {
len: Some(len),
..metadata
},
bytes: Rc::new(bytes),
}
}
pub fn named(name: impl Into<String>, bytes: Vec<u8>) -> Self {
Self::new(ContentMetadata::named(name), bytes)
}
pub fn handle(self) -> ContentHandle {
Rc::new(self)
}
}
impl Content for BytesContent {
fn metadata(&self) -> ContentMetadata {
self.metadata.clone()
}
fn open(&self) -> ContentFuture<'_, Result<ContentReaderRef, ContentError>> {
let bytes = Rc::clone(&self.bytes);
Box::pin(async move {
Ok(Rc::new(BytesReader {
bytes,
offset: std::cell::Cell::new(0),
}) as ContentReaderRef)
})
}
fn read_all(&self) -> ContentFuture<'_, Result<Vec<u8>, ContentError>> {
let bytes = Rc::clone(&self.bytes);
Box::pin(async move { Ok(bytes.as_ref().clone()) })
}
}
struct BytesReader {
bytes: Rc<Vec<u8>>,
offset: std::cell::Cell<usize>,
}
impl ContentReader for BytesReader {
fn read_chunk(&self) -> ContentFuture<'_, Result<Option<Vec<u8>>, ContentError>> {
Box::pin(async move {
let start = self.offset.get();
if start >= self.bytes.len() {
return Ok(None);
}
let end = (start + DEFAULT_CHUNK_LEN).min(self.bytes.len());
self.offset.set(end);
Ok(Some(self.bytes[start..end].to_vec()))
})
}
}
pub struct ReadyFolder {
metadata: ContentMetadata,
entries: RefCell<Option<Vec<ContentEntry>>>,
}
impl ReadyFolder {
pub fn new(metadata: ContentMetadata, entries: Vec<ContentEntry>) -> Self {
Self {
metadata,
entries: RefCell::new(Some(entries)),
}
}
pub fn handle(self) -> ContentFolderRef {
Rc::new(self)
}
}
impl ContentFolder for ReadyFolder {
fn metadata(&self) -> ContentMetadata {
self.metadata.clone()
}
fn entries(&self) -> ContentFuture<'_, Result<Vec<ContentEntry>, ContentError>> {
Box::pin(async move {
self.entries
.borrow_mut()
.take()
.ok_or_else(|| ContentError::Io("folder entries already consumed".into()))
})
}
}
pub trait ContentResolver {
fn resolve(&self, uri: &str) -> Option<ContentHandle>;
}
pub type ContentResolverRef = Rc<dyn ContentResolver>;
thread_local! {
static PLATFORM_RESOLVER: RefCell<Option<ContentResolverRef>> = const { RefCell::new(None) };
}
pub fn set_platform_content_resolver(resolver: ContentResolverRef) {
PLATFORM_RESOLVER.with(|cell| *cell.borrow_mut() = Some(resolver));
}
pub fn clear_platform_content_resolver() {
PLATFORM_RESOLVER.with(|cell| *cell.borrow_mut() = None);
}
pub fn resolve_content(uri: &str) -> Option<ContentHandle> {
if let Some(resolver) = PLATFORM_RESOLVER.with(|cell| cell.borrow().clone())
&& let Some(content) = resolver.resolve(uri)
{
return Some(content);
}
#[cfg(not(target_arch = "wasm32"))]
{
let path = uri.strip_prefix("file://").unwrap_or(uri);
if !path.contains("://") {
return Some(file_content(path));
}
}
let _ = uri;
None
}
#[cfg(not(target_arch = "wasm32"))]
pub use file::{FileContent, FileFolder, FileSink, file_content, file_folder};
#[cfg(not(target_arch = "wasm32"))]
mod file;
pub fn percent_decode(input: &str) -> Option<String> {
let bytes = input.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%' {
let high = bytes.get(index + 1).copied().and_then(hex_value)?;
let low = bytes.get(index + 2).copied().and_then(hex_value)?;
out.push((high << 4) | low);
index += 3;
} else {
out.push(bytes[index]);
index += 1;
}
}
String::from_utf8(out).ok()
}
pub fn percent_decode_lossy(input: &str) -> String {
let bytes = input.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
if bytes[index] == b'%' {
let pair = hex_value(bytes.get(index + 1).copied().unwrap_or(0))
.zip(hex_value(bytes.get(index + 2).copied().unwrap_or(0)));
if let Some((high, low)) = pair {
out.push((high << 4) | low);
index += 3;
continue;
}
}
out.push(bytes[index]);
index += 1;
}
String::from_utf8_lossy(&out).into_owned()
}
fn hex_value(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
#[path = "tests/content_tests.rs"]
mod tests;