use oxideav_core::{BytesSource, Error, Result};
use crate::data::open_data;
use crate::file::open_file;
use crate::mem::open_mem;
use crate::sub::SubSource;
use crate::uri;
#[derive(Clone, Debug, PartialEq, Eq)]
struct SliceHeader<'a> {
offset: u64,
length: u64,
inner: &'a str,
}
fn parse_header(rest: &str) -> Result<SliceHeader<'_>> {
let bang = rest
.find('!')
.ok_or_else(|| Error::invalid("slice: URI missing '!' separator before inner URI"))?;
let (range, inner_with_bang) = rest.split_at(bang);
let inner = &inner_with_bang[1..];
let plus = range
.find('+')
.ok_or_else(|| Error::invalid("slice: URI range missing '+' between offset and length"))?;
let (off_s, len_with_plus) = range.split_at(plus);
let len_s = &len_with_plus[1..];
let offset: u64 = off_s.parse().map_err(|e| {
Error::invalid(format!(
"slice: offset {off_s:?} is not a non-negative decimal u64: {e}"
))
})?;
let length: u64 = len_s.parse().map_err(|e| {
Error::invalid(format!(
"slice: length {len_s:?} is not a non-negative decimal u64: {e}"
))
})?;
if inner.is_empty() {
return Err(Error::invalid(
"slice: URI inner reference is empty after '!'",
));
}
Ok(SliceHeader {
offset,
length,
inner,
})
}
fn open_inner(inner: &str) -> Result<Box<dyn BytesSource>> {
let (scheme, _) = uri::split(inner);
match scheme {
"file" => open_file(inner),
"mem" => open_mem(inner),
"data" => open_data(inner),
"slice" => open_slice(inner),
other => Err(Error::invalid(format!(
"slice: inner URI uses unsupported scheme {other:?}; \
only file/mem/data/slice are accepted as inner sources"
))),
}
}
pub fn open_slice(uri_str: &str) -> Result<Box<dyn BytesSource>> {
let (scheme, rest) = uri::split(uri_str);
if scheme != "slice" {
return Err(Error::invalid(format!(
"slice driver invoked on non-slice URI: {uri_str}"
)));
}
let header = parse_header(rest)?;
let inner = open_inner(header.inner)?;
let sub = SubSource::new(inner, header.offset, header.length)?;
Ok(Box::new(sub))
}
#[cfg(test)]
mod tests {
use std::io::{Read, Seek, SeekFrom, Write};
use crate::mem;
use super::*;
fn ramp(n: usize) -> Vec<u8> {
(0..n).map(|i| (i & 0xff) as u8).collect()
}
fn temp_ramp(n: usize) -> std::path::PathBuf {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let mut path = std::env::temp_dir();
let pid = std::process::id();
let k = N.fetch_add(1, Ordering::Relaxed);
path.push(format!("oxideav-slice-test-{pid}-{k}.bin"));
let mut f = std::fs::File::create(&path).unwrap();
f.write_all(&ramp(n)).unwrap();
f.flush().unwrap();
path
}
#[test]
fn parse_basic() {
let h = parse_header("10+20!file:///tmp/x").unwrap();
assert_eq!(h.offset, 10);
assert_eq!(h.length, 20);
assert_eq!(h.inner, "file:///tmp/x");
}
#[test]
fn parse_zero_length_is_ok() {
let h = parse_header("0+0!mem://x").unwrap();
assert_eq!(h.offset, 0);
assert_eq!(h.length, 0);
}
#[test]
fn parse_missing_bang_rejected() {
assert!(parse_header("10+20").is_err());
}
#[test]
fn parse_missing_plus_rejected() {
assert!(parse_header("10!file:///x").is_err());
}
#[test]
fn parse_non_numeric_offset_rejected() {
assert!(parse_header("abc+20!file:///x").is_err());
}
#[test]
fn parse_non_numeric_length_rejected() {
assert!(parse_header("10+abc!file:///x").is_err());
}
#[test]
fn parse_empty_inner_rejected() {
assert!(parse_header("10+20!").is_err());
}
#[test]
fn parse_negative_offset_rejected() {
assert!(parse_header("-1+20!mem://x").is_err());
}
#[test]
fn wrong_scheme_rejected() {
assert!(open_slice("file:///tmp/x").is_err());
assert!(open_slice("mem://x").is_err());
}
#[test]
fn slices_a_file() {
let p = temp_ramp(256);
let uri = format!("slice:50+40!file://{}", p.display());
let mut r = open_slice(&uri).unwrap();
let mut out = vec![0u8; 40];
r.read_exact(&mut out).unwrap();
assert_eq!(out, ramp(256)[50..90]);
std::fs::remove_file(p).ok();
}
#[test]
fn slices_a_mem_buffer() {
mem::put("slice-r178-mem-a", ramp(128));
let mut r = open_slice("slice:32+16!mem://slice-r178-mem-a").unwrap();
let mut out = vec![0u8; 16];
r.read_exact(&mut out).unwrap();
assert_eq!(out, ramp(128)[32..48]);
mem::remove("slice-r178-mem-a");
}
#[test]
fn slices_a_data_uri() {
let mut r = open_slice("slice:3+2!data:,ABCDEFGHIJ").unwrap();
let mut out = vec![0u8; 2];
r.read_exact(&mut out).unwrap();
assert_eq!(&out, b"DE");
}
#[test]
fn slices_a_base64_data_uri() {
let mut r = open_slice("slice:1+3!data:;base64,SGVsbG8=").unwrap();
let mut out = vec![0u8; 3];
r.read_exact(&mut out).unwrap();
assert_eq!(&out, b"ell");
}
#[test]
fn slice_seek_within_window() {
let p = temp_ramp(256);
let uri = format!("slice:100+50!file://{}", p.display());
let mut r = open_slice(&uri).unwrap();
r.seek(SeekFrom::Start(20)).unwrap();
let mut byte = [0u8; 1];
r.read_exact(&mut byte).unwrap();
assert_eq!(byte[0], 120); let end = r.seek(SeekFrom::End(0)).unwrap();
assert_eq!(end, 50);
std::fs::remove_file(p).ok();
}
#[test]
fn window_past_inner_rejected() {
mem::put("slice-r178-past", ramp(64));
let r = open_slice("slice:50+50!mem://slice-r178-past");
assert!(r.is_err());
mem::remove("slice-r178-past");
}
#[test]
fn nested_slice_recursive() {
mem::put("slice-r178-nest", ramp(64));
let uri = "slice:5+10!slice:10+20!mem://slice-r178-nest";
let mut r = open_slice(uri).unwrap();
let mut out = vec![0u8; 10];
r.read_exact(&mut out).unwrap();
assert_eq!(out, ramp(64)[15..25]);
mem::remove("slice-r178-nest");
}
#[test]
fn inner_unsupported_scheme_rejected() {
let r = open_slice("slice:0+10!http://example.com/x");
assert!(r.is_err());
}
#[test]
fn inner_bare_path_accepted() {
let p = temp_ramp(32);
let uri = format!("slice:4+8!{}", p.display());
let mut r = open_slice(&uri).unwrap();
let mut out = vec![0u8; 8];
r.read_exact(&mut out).unwrap();
assert_eq!(out, ramp(32)[4..12]);
std::fs::remove_file(p).ok();
}
#[test]
fn zero_length_window_returns_eof_immediately() {
let p = temp_ramp(16);
let uri = format!("slice:4+0!file://{}", p.display());
let mut r = open_slice(&uri).unwrap();
let mut byte = [0u8; 1];
assert_eq!(r.read(&mut byte).unwrap(), 0);
std::fs::remove_file(p).ok();
}
#[test]
fn missing_inner_file_errors() {
let uri = "slice:0+10!/no/such/path/xyzzy-oxideav-slice-r178";
assert!(open_slice(uri).is_err());
}
}