#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TransportError {
NoSuchFile(String),
ReadOnly(String),
Io {
kind: std::io::ErrorKind,
message: String,
},
Other(String),
}
impl From<std::io::Error> for TransportError {
fn from(e: std::io::Error) -> Self {
if e.kind() == std::io::ErrorKind::NotFound {
TransportError::NoSuchFile(e.to_string())
} else {
TransportError::Io {
kind: e.kind(),
message: e.to_string(),
}
}
}
}
impl std::fmt::Display for TransportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransportError::NoSuchFile(p) => write!(f, "No such file: {}", p),
TransportError::ReadOnly(p) => write!(f, "Read-only transport: {}", p),
TransportError::Io { kind, message } => {
write!(f, "I/O error ({:?}): {}", kind, message)
}
TransportError::Other(s) => write!(f, "Transport error: {}", s),
}
}
}
impl std::error::Error for TransportError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadRange {
pub offset: u64,
pub length: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadResult {
pub offset: u64,
pub length: usize,
pub bytes: Vec<u8>,
}
pub trait Transport {
fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError>;
fn put_file_non_atomic(
&self,
path: &str,
bytes: &[u8],
create_parent_dir: bool,
) -> Result<(), TransportError>;
fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError>;
fn mkdir(&self, path: &str) -> Result<(), TransportError>;
fn has(&self, path: &str) -> Result<bool, TransportError>;
fn readv(&self, path: &str, ranges: &[ReadRange]) -> Result<Vec<ReadResult>, TransportError> {
let data = self.get_bytes(path)?;
let mut out = Vec::with_capacity(ranges.len());
for r in ranges {
let start = r.offset as usize;
let end = start.checked_add(r.length).ok_or_else(|| {
TransportError::Other(format!(
"readv range overflow: offset={} length={}",
r.offset, r.length
))
})?;
if end > data.len() {
return Err(TransportError::Other(format!(
"readv range past end: offset={} length={} data_len={}",
r.offset,
r.length,
data.len()
)));
}
out.push(ReadResult {
offset: r.offset,
length: r.length,
bytes: data[start..end].to_vec(),
});
}
Ok(out)
}
fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError>;
fn abspath(&self, path: &str) -> Result<String, TransportError>;
}
#[cfg(test)]
pub(crate) mod testing {
use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
#[derive(Default)]
pub struct MemoryTransport {
files: Mutex<HashMap<String, Vec<u8>>>,
root: String,
}
impl MemoryTransport {
pub fn new() -> Self {
Self {
files: Mutex::new(HashMap::new()),
root: "memory:///".to_string(),
}
}
}
impl Transport for MemoryTransport {
fn get_bytes(&self, path: &str) -> Result<Vec<u8>, TransportError> {
let files = self.files.lock().unwrap();
files
.get(path)
.cloned()
.ok_or_else(|| TransportError::NoSuchFile(path.to_string()))
}
fn put_file_non_atomic(
&self,
path: &str,
bytes: &[u8],
_create_parent_dir: bool,
) -> Result<(), TransportError> {
let mut files = self.files.lock().unwrap();
files.insert(path.to_string(), bytes.to_vec());
Ok(())
}
fn mkdir(&self, _path: &str) -> Result<(), TransportError> {
Ok(())
}
fn append_bytes(&self, path: &str, bytes: &[u8]) -> Result<u64, TransportError> {
let mut files = self.files.lock().unwrap();
let entry = files.entry(path.to_string()).or_default();
let offset = entry.len() as u64;
entry.extend_from_slice(bytes);
Ok(offset)
}
fn has(&self, path: &str) -> Result<bool, TransportError> {
let files = self.files.lock().unwrap();
Ok(files.contains_key(path))
}
fn iter_files_recursive(&self) -> Result<Vec<String>, TransportError> {
let files = self.files.lock().unwrap();
Ok(files.keys().cloned().collect())
}
fn abspath(&self, path: &str) -> Result<String, TransportError> {
Ok(format!("{}{}", self.root, path))
}
}
#[test]
fn memory_transport_basic_round_trip() {
let t = MemoryTransport::new();
assert!(!t.has("foo").unwrap());
t.put_file_non_atomic("foo", b"hello", false).unwrap();
assert!(t.has("foo").unwrap());
assert_eq!(t.get_bytes("foo").unwrap(), b"hello".to_vec());
}
#[test]
fn memory_transport_append_returns_offset() {
let t = MemoryTransport::new();
assert_eq!(t.append_bytes("log", b"first ").unwrap(), 0);
assert_eq!(t.append_bytes("log", b"second").unwrap(), 6);
assert_eq!(t.get_bytes("log").unwrap(), b"first second".to_vec());
}
#[test]
fn memory_transport_get_bytes_missing_is_error() {
let t = MemoryTransport::new();
assert_eq!(
t.get_bytes("nope").unwrap_err(),
TransportError::NoSuchFile("nope".to_string())
);
}
#[test]
fn default_readv_slices_via_get_bytes() {
let t = MemoryTransport::new();
t.put_file_non_atomic("data", b"0123456789", false).unwrap();
let ranges = vec![
ReadRange {
offset: 0,
length: 3,
},
ReadRange {
offset: 5,
length: 2,
},
];
let results = t.readv("data", &ranges).unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].bytes, b"012".to_vec());
assert_eq!(results[1].bytes, b"56".to_vec());
}
#[test]
fn default_readv_rejects_past_end() {
let t = MemoryTransport::new();
t.put_file_non_atomic("data", b"hi", false).unwrap();
let err = t
.readv(
"data",
&[ReadRange {
offset: 0,
length: 100,
}],
)
.unwrap_err();
assert!(matches!(err, TransportError::Other(_)));
}
}