Skip to main content

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}