1use 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#[derive(Clone, Debug, PartialEq, Eq)]
62struct SliceHeader<'a> {
63 offset: u64,
64 length: u64,
65 inner: &'a str,
66}
67
68fn 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..]; 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..]; 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
107fn 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
124pub 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 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 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 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 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); 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 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 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 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}