oxideav_core/registry/source.rs
1//! Generic source registry.
2//!
3//! Containers in oxideav take a `Box<dyn ReadSeek>`; this module is what
4//! turns a URI into one. External drivers (e.g. `oxideav-http`)
5//! register themselves into a [`SourceRegistry`] for additional
6//! schemes. The built-in `file` driver is provided by the
7//! `oxideav-source` re-export shim crate (so this core module stays
8//! free of `std::fs` use cases that callers may want to swap out).
9
10use std::collections::HashMap;
11
12use super::container::ReadSeek;
13use crate::{Error, Result};
14
15/// Function signature for a source driver. Receives the full URI string
16/// and returns an opened reader.
17pub type OpenSourceFn = fn(uri: &str) -> Result<Box<dyn ReadSeek>>;
18
19/// Registry mapping URI schemes to opener functions.
20#[derive(Default)]
21pub struct SourceRegistry {
22 schemes: HashMap<String, OpenSourceFn>,
23}
24
25impl SourceRegistry {
26 /// Empty registry. Callers must register at least the `file` driver
27 /// before calling [`open`](Self::open).
28 pub fn new() -> Self {
29 Self::default()
30 }
31
32 /// Register an opener for a scheme. Schemes are normalised to ASCII
33 /// lowercase. Replaces any prior registration.
34 pub fn register(&mut self, scheme: &str, opener: OpenSourceFn) {
35 self.schemes.insert(scheme.to_ascii_lowercase(), opener);
36 }
37
38 /// Open a URI. The URI's scheme determines which opener runs; bare
39 /// paths (no scheme) and unrecognised schemes both fall back to the
40 /// `file` driver if it is registered.
41 pub fn open(&self, uri_str: &str) -> Result<Box<dyn ReadSeek>> {
42 let (scheme, _) = split_scheme(uri_str);
43 let scheme = scheme.to_ascii_lowercase();
44 if let Some(opener) = self.schemes.get(&scheme) {
45 return opener(uri_str);
46 }
47 // Fall back to file driver for unknown schemes.
48 if let Some(opener) = self.schemes.get("file") {
49 return opener(uri_str);
50 }
51 Err(Error::Unsupported(format!(
52 "no source driver for scheme '{scheme}' (URI: {uri_str})"
53 )))
54 }
55
56 /// Iterate the registered schemes (for diagnostics).
57 pub fn schemes(&self) -> impl Iterator<Item = &str> {
58 self.schemes.keys().map(|s| s.as_str())
59 }
60}
61
62/// Split a URI into `(scheme, rest)`. Bare paths (no scheme) report scheme
63/// `"file"` and `rest = uri`. Path-like inputs that happen to start with
64/// `c:` on Windows are treated as bare paths.
65pub(crate) fn split_scheme(uri: &str) -> (&str, &str) {
66 if let Some(idx) = uri.find(':') {
67 let (scheme, rest) = uri.split_at(idx);
68 let rest = &rest[1..]; // skip ':'
69
70 // Reject single-letter scheme that looks like a Windows drive letter.
71 if scheme.len() == 1 && scheme.chars().next().unwrap().is_ascii_alphabetic() {
72 return ("file", uri);
73 }
74
75 // Scheme must be ASCII alphanumeric / `+` / `-` / `.`, starting with a letter.
76 let valid = !scheme.is_empty()
77 && scheme.chars().next().unwrap().is_ascii_alphabetic()
78 && scheme
79 .chars()
80 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '-' | '.'));
81
82 if !valid {
83 return ("file", uri);
84 }
85
86 // Strip leading `//` from rest if present.
87 let rest = rest.strip_prefix("//").unwrap_or(rest);
88 return (scheme, rest);
89 }
90 ("file", uri)
91}