Skip to main content

oxideav_source/
slice.rs

1//! Built-in `slice:` driver — URI-level windowed view over an inner source.
2//!
3//! `slice:<offset>+<length>!<inner-uri>` opens `<inner-uri>` with one of
4//! the bundled in-process openers and then wraps it in a [`SubSource`]
5//! that re-projects `[offset, offset + length)` onto `[0, length)`. The
6//! returned reader satisfies `BytesSource`, so a codec hands the slice
7//! to a demuxer that thinks it owns a complete file.
8//!
9//! This is the URI-level analogue of constructing [`SubSource`]
10//! programmatically: a pipeline that takes a URI on the command line can
11//! address a sub-range without first materialising the inner stream as a
12//! file or a `mem://` blob.
13//!
14//! Grammar (no on-wire spec — internal to OxideAV):
15//!
16//! ```text
17//! sliceurl  = "slice:" offset "+" length "!" inner-uri
18//! offset    = 1*DIGIT          ; decimal u64
19//! length    = 1*DIGIT          ; decimal u64
20//! inner-uri = <a file path or any supported scheme except "slice:" itself
21//!              when "!"-encoded inside the slice payload would re-enter>
22//! ```
23//!
24//! The `!` separator was chosen because it is unreserved in RFC 3986
25//! sub-delims, never appears in `file://` paths in practice, and is not
26//! used by the other bundled schemes — so the split is unambiguous even
27//! when the inner URI carries its own `:` and `://`. A literal `!` inside
28//! an inner URI is not supported; the first `!` after the `length` token
29//! is treated as the separator.
30//!
31//! Supported inner schemes:
32//!
33//! - `file://` and bare paths (delegates to [`open_file`]).
34//! - `mem://<id>` (delegates to [`open_mem`]).
35//! - `data:` (delegates to [`open_data`]).
36//! - `slice:` (recursive — composition works as one would expect:
37//!   `slice:5+3!slice:10+8!file:///x.bin` first windows the inner file
38//!   to bytes `[10, 18)`, then slices that to its bytes `[5, 8)` which
39//!   are file bytes `[15, 18)`).
40//!
41//! The driver does **not** dispatch through a [`SourceRegistry`] — the
42//! registry's opener API takes a plain `fn` pointer with no captured
43//! context. Drivers that need registry-mediated inner resolution (HTTP,
44//! custom schemes) should compose [`SubSource`] programmatically after
45//! resolving the inner URI themselves.
46//!
47//! Clean-room note: no external `slice:`-like URL implementation was
48//! consulted. The grammar is a straightforward composition of two
49//! decimal integers, a separator, and the existing `data:` / `mem://`
50//! / `file://` openers.
51
52use oxideav_core::{BytesSource, Error, Result};
53
54use crate::data::open_data;
55use crate::file::open_file;
56use crate::mem::open_mem;
57use crate::sub::SubSource;
58use crate::uri;
59
60/// Parsed `slice:` URI header.
61#[derive(Clone, Debug, PartialEq, Eq)]
62struct SliceHeader<'a> {
63    offset: u64,
64    length: u64,
65    inner: &'a str,
66}
67
68/// Parse the payload that follows `slice:` (i.e. the value of `uri::split`'s
69/// `rest`). Returns the offset, length, and the inner URI string.
70fn parse_header(rest: &str) -> Result<SliceHeader<'_>> {
71    let bang = rest
72        .find('!')
73        .ok_or_else(|| Error::invalid("slice: URI missing '!' separator before inner URI"))?;
74    let (range, inner_with_bang) = rest.split_at(bang);
75    let inner = &inner_with_bang[1..]; // skip '!'
76
77    let plus = range
78        .find('+')
79        .ok_or_else(|| Error::invalid("slice: URI range missing '+' between offset and length"))?;
80    let (off_s, len_with_plus) = range.split_at(plus);
81    let len_s = &len_with_plus[1..]; // skip '+'
82
83    let offset: u64 = off_s.parse().map_err(|e| {
84        Error::invalid(format!(
85            "slice: offset {off_s:?} is not a non-negative decimal u64: {e}"
86        ))
87    })?;
88    let length: u64 = len_s.parse().map_err(|e| {
89        Error::invalid(format!(
90            "slice: length {len_s:?} is not a non-negative decimal u64: {e}"
91        ))
92    })?;
93
94    if inner.is_empty() {
95        return Err(Error::invalid(
96            "slice: URI inner reference is empty after '!'",
97        ));
98    }
99
100    Ok(SliceHeader {
101        offset,
102        length,
103        inner,
104    })
105}
106
107/// Resolve an inner URI by dispatching to one of the bundled openers.
108/// Limited to schemes whose opener has no captured state: `file://`,
109/// bare paths, `mem://`, `data:`, and recursive `slice:`.
110fn open_inner(inner: &str) -> Result<Box<dyn BytesSource>> {
111    let (scheme, _) = uri::split(inner);
112    match scheme {
113        "file" => open_file(inner),
114        "mem" => open_mem(inner),
115        "data" => open_data(inner),
116        "slice" => open_slice(inner),
117        other => Err(Error::invalid(format!(
118            "slice: inner URI uses unsupported scheme {other:?}; \
119             only file/mem/data/slice are accepted as inner sources"
120        ))),
121    }
122}
123
124/// Open a `slice:<offset>+<length>!<inner-uri>` URI.
125pub fn open_slice(uri_str: &str) -> Result<Box<dyn BytesSource>> {
126    let (scheme, rest) = uri::split(uri_str);
127    if scheme != "slice" {
128        return Err(Error::invalid(format!(
129            "slice driver invoked on non-slice URI: {uri_str}"
130        )));
131    }
132    let header = parse_header(rest)?;
133    let inner = open_inner(header.inner)?;
134    let sub = SubSource::new(inner, header.offset, header.length)?;
135    Ok(Box::new(sub))
136}
137
138#[cfg(test)]
139mod tests {
140    use std::io::{Read, Seek, SeekFrom, Write};
141
142    use crate::mem;
143
144    use super::*;
145
146    fn ramp(n: usize) -> Vec<u8> {
147        (0..n).map(|i| (i & 0xff) as u8).collect()
148    }
149
150    fn temp_ramp(n: usize) -> std::path::PathBuf {
151        use std::sync::atomic::{AtomicU64, Ordering};
152        static N: AtomicU64 = AtomicU64::new(0);
153        let mut path = std::env::temp_dir();
154        let pid = std::process::id();
155        let k = N.fetch_add(1, Ordering::Relaxed);
156        path.push(format!("oxideav-slice-test-{pid}-{k}.bin"));
157        let mut f = std::fs::File::create(&path).unwrap();
158        f.write_all(&ramp(n)).unwrap();
159        f.flush().unwrap();
160        path
161    }
162
163    #[test]
164    fn parse_basic() {
165        let h = parse_header("10+20!file:///tmp/x").unwrap();
166        assert_eq!(h.offset, 10);
167        assert_eq!(h.length, 20);
168        assert_eq!(h.inner, "file:///tmp/x");
169    }
170
171    #[test]
172    fn parse_zero_length_is_ok() {
173        // SubSource itself accepts a zero-length window.
174        let h = parse_header("0+0!mem://x").unwrap();
175        assert_eq!(h.offset, 0);
176        assert_eq!(h.length, 0);
177    }
178
179    #[test]
180    fn parse_missing_bang_rejected() {
181        assert!(parse_header("10+20").is_err());
182    }
183
184    #[test]
185    fn parse_missing_plus_rejected() {
186        assert!(parse_header("10!file:///x").is_err());
187    }
188
189    #[test]
190    fn parse_non_numeric_offset_rejected() {
191        assert!(parse_header("abc+20!file:///x").is_err());
192    }
193
194    #[test]
195    fn parse_non_numeric_length_rejected() {
196        assert!(parse_header("10+abc!file:///x").is_err());
197    }
198
199    #[test]
200    fn parse_empty_inner_rejected() {
201        assert!(parse_header("10+20!").is_err());
202    }
203
204    #[test]
205    fn parse_negative_offset_rejected() {
206        // u64 doesn't accept '-'; ensure the error path triggers.
207        assert!(parse_header("-1+20!mem://x").is_err());
208    }
209
210    #[test]
211    fn wrong_scheme_rejected() {
212        assert!(open_slice("file:///tmp/x").is_err());
213        assert!(open_slice("mem://x").is_err());
214    }
215
216    #[test]
217    fn slices_a_file() {
218        let p = temp_ramp(256);
219        let uri = format!("slice:50+40!file://{}", p.display());
220        let mut r = open_slice(&uri).unwrap();
221        let mut out = vec![0u8; 40];
222        r.read_exact(&mut out).unwrap();
223        assert_eq!(out, ramp(256)[50..90]);
224        std::fs::remove_file(p).ok();
225    }
226
227    #[test]
228    fn slices_a_mem_buffer() {
229        mem::put("slice-r178-mem-a", ramp(128));
230        let mut r = open_slice("slice:32+16!mem://slice-r178-mem-a").unwrap();
231        let mut out = vec![0u8; 16];
232        r.read_exact(&mut out).unwrap();
233        assert_eq!(out, ramp(128)[32..48]);
234        mem::remove("slice-r178-mem-a");
235    }
236
237    #[test]
238    fn slices_a_data_uri() {
239        // data:,ABCDEFGHIJ → 10 bytes; slice [3, 5) → "DE".
240        let mut r = open_slice("slice:3+2!data:,ABCDEFGHIJ").unwrap();
241        let mut out = vec![0u8; 2];
242        r.read_exact(&mut out).unwrap();
243        assert_eq!(&out, b"DE");
244    }
245
246    #[test]
247    fn slices_a_base64_data_uri() {
248        // "Hello" base64 = "SGVsbG8=" → bytes [1, 4) = "ell".
249        let mut r = open_slice("slice:1+3!data:;base64,SGVsbG8=").unwrap();
250        let mut out = vec![0u8; 3];
251        r.read_exact(&mut out).unwrap();
252        assert_eq!(&out, b"ell");
253    }
254
255    #[test]
256    fn slice_seek_within_window() {
257        let p = temp_ramp(256);
258        let uri = format!("slice:100+50!file://{}", p.display());
259        let mut r = open_slice(&uri).unwrap();
260        r.seek(SeekFrom::Start(20)).unwrap();
261        let mut byte = [0u8; 1];
262        r.read_exact(&mut byte).unwrap();
263        assert_eq!(byte[0], 120); // ramp[100 + 20]
264        let end = r.seek(SeekFrom::End(0)).unwrap();
265        assert_eq!(end, 50);
266        std::fs::remove_file(p).ok();
267    }
268
269    #[test]
270    fn window_past_inner_rejected() {
271        mem::put("slice-r178-past", ramp(64));
272        let r = open_slice("slice:50+50!mem://slice-r178-past");
273        assert!(r.is_err());
274        mem::remove("slice-r178-past");
275    }
276
277    #[test]
278    fn nested_slice_recursive() {
279        // Outer slice maps mem[10..30] to window[0..20].
280        // Inner slice further maps window[5..15] (== mem[15..25]) to [0..10].
281        mem::put("slice-r178-nest", ramp(64));
282        let uri = "slice:5+10!slice:10+20!mem://slice-r178-nest";
283        let mut r = open_slice(uri).unwrap();
284        let mut out = vec![0u8; 10];
285        r.read_exact(&mut out).unwrap();
286        assert_eq!(out, ramp(64)[15..25]);
287        mem::remove("slice-r178-nest");
288    }
289
290    #[test]
291    fn inner_unsupported_scheme_rejected() {
292        // "http://" inner is not dispatchable without registry context.
293        let r = open_slice("slice:0+10!http://example.com/x");
294        assert!(r.is_err());
295    }
296
297    #[test]
298    fn inner_bare_path_accepted() {
299        // `file` is the bare-path fallback in uri::split.
300        let p = temp_ramp(32);
301        let uri = format!("slice:4+8!{}", p.display());
302        let mut r = open_slice(&uri).unwrap();
303        let mut out = vec![0u8; 8];
304        r.read_exact(&mut out).unwrap();
305        assert_eq!(out, ramp(32)[4..12]);
306        std::fs::remove_file(p).ok();
307    }
308
309    #[test]
310    fn zero_length_window_returns_eof_immediately() {
311        let p = temp_ramp(16);
312        let uri = format!("slice:4+0!file://{}", p.display());
313        let mut r = open_slice(&uri).unwrap();
314        let mut byte = [0u8; 1];
315        assert_eq!(r.read(&mut byte).unwrap(), 0);
316        std::fs::remove_file(p).ok();
317    }
318
319    #[test]
320    fn missing_inner_file_errors() {
321        let uri = "slice:0+10!/no/such/path/xyzzy-oxideav-slice-r178";
322        assert!(open_slice(uri).is_err());
323    }
324}