1use std::fmt;
2use std::io::{Read, Seek, SeekFrom};
3use std::ops::Range;
4use std::path::Path;
5use std::sync::Mutex;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct ByteRange {
14 pub offset: u64,
15 pub len: u64,
16}
17
18impl ByteRange {
19 pub fn new(offset: u64, len: u64) -> Result<Self, ReadAtError> {
20 offset
21 .checked_add(len)
22 .ok_or(ReadAtError::RangeOverflow { offset, len })
23 .map(|_| Self { offset, len })
24 }
25
26 pub fn from_usize(offset: usize, len: usize) -> Result<Self, ReadAtError> {
27 Self::new(offset as u64, len as u64)
28 }
29
30 pub fn is_empty(&self) -> bool {
31 self.len == 0
32 }
33
34 pub fn end_exclusive(&self) -> u64 {
35 self.offset + self.len
36 }
37
38 fn len_usize(&self) -> Result<usize, ReadAtError> {
39 usize::try_from(self.len).map_err(|_| ReadAtError::RangeTooLarge { len: self.len })
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
45pub enum ReadAtError {
46 RangeOverflow { offset: u64, len: u64 },
47 RangeTooLarge { len: u64 },
48 OffsetOutOfBounds { offset: u64, source_len: u64 },
49 RangeOutOfBounds { offset: u64, len: u64, source_len: u64 },
50 DestinationTooSmall { requested: usize, available: usize },
51 UnavailableWindow,
52 Io { message: String },
53}
54
55impl From<std::io::Error> for ReadAtError {
56 fn from(value: std::io::Error) -> Self {
57 ReadAtError::Io { message: value.to_string() }
58 }
59}
60
61impl fmt::Display for ReadAtError {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 ReadAtError::RangeOverflow { offset, len } => {
65 write!(f, "byte range overflows: offset={offset}, len={len}")
66 }
67 ReadAtError::RangeTooLarge { len } => {
68 write!(f, "byte range length does not fit in memory: len={len}")
69 }
70 ReadAtError::OffsetOutOfBounds { offset, source_len } => {
71 write!(f, "offset {offset} is outside source length {source_len}")
72 }
73 ReadAtError::RangeOutOfBounds { offset, len, source_len } => {
74 write!(f, "range offset={offset}, len={len} exceeds source length {source_len}")
75 }
76 ReadAtError::DestinationTooSmall { requested, available } => {
77 write!(f, "destination too small: requested={requested}, available={available}")
78 }
79 ReadAtError::UnavailableWindow => write!(f, "requested byte window is unavailable"),
80 ReadAtError::Io { message } => write!(f, "source I/O error: {message}"),
81 }
82 }
83}
84
85impl std::error::Error for ReadAtError {}
86
87#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
89pub struct SourceStats {
90 pub read_at_calls: u64,
91 pub window_at_calls: u64,
92 pub bytes_requested: u64,
93 pub bytes_returned: u64,
94 pub max_request_len: u64,
95}
96
97pub trait MediaReadAt: std::fmt::Debug {
99 fn len_u64(&self) -> u64;
101
102 fn is_empty(&self) -> bool {
103 self.len_u64() == 0
104 }
105
106 fn read_at(&self, range: ByteRange, dst: &mut [u8]) -> Result<usize, ReadAtError>;
108
109 fn window_at(&self, range: ByteRange) -> Result<&[u8], ReadAtError>;
111
112 fn window_at_partial(&self, range: ByteRange) -> Result<&[u8], ReadAtError> {
114 self.window_at(range)
115 }
116
117 fn as_contiguous(&self) -> Option<&[u8]> {
119 None
120 }
121
122 fn stats(&self) -> SourceStats {
123 SourceStats::default()
124 }
125}
126
127pub trait ByteSource: std::fmt::Debug {
146 fn len(&self) -> usize;
148
149 fn is_empty(&self) -> bool {
151 self.len() == 0
152 }
153
154 fn byte_at(&self, offset: usize) -> Option<u8>;
156
157 fn slice_at(&self, offset: usize, len: usize) -> Option<&[u8]>;
160}
161
162#[derive(Debug)]
168pub struct FileBackend {
169 file: Mutex<std::fs::File>,
170 len: u64,
171}
172
173impl FileBackend {
174 pub fn open(path: impl AsRef<Path>) -> Result<Self, ReadAtError> {
175 let file = std::fs::File::open(path).map_err(ReadAtError::from)?;
176 Self::from_file(file)
177 }
178
179 pub fn from_file(file: std::fs::File) -> Result<Self, ReadAtError> {
180 let len = file.metadata().map_err(ReadAtError::from)?.len();
181 Ok(Self { file: Mutex::new(file), len })
182 }
183}
184
185impl MediaReadAt for FileBackend {
186 fn len_u64(&self) -> u64 {
187 self.len
188 }
189
190 fn read_at(&self, range: ByteRange, dst: &mut [u8]) -> Result<usize, ReadAtError> {
191 let requested_len = range.len_usize()?;
192 if dst.len() < requested_len {
193 return Err(ReadAtError::DestinationTooSmall {
194 requested: requested_len,
195 available: dst.len(),
196 });
197 }
198 if range.end_exclusive() > self.len {
199 return Err(ReadAtError::RangeOutOfBounds {
200 offset: range.offset,
201 len: range.len,
202 source_len: self.len,
203 });
204 }
205 let mut file = self
206 .file
207 .lock()
208 .map_err(|_| ReadAtError::Io { message: "file backend mutex poisoned".to_string() })?;
209 file.seek(SeekFrom::Start(range.offset)).map_err(ReadAtError::from)?;
210 file.read_exact(&mut dst[..requested_len]).map_err(ReadAtError::from)?;
211 Ok(requested_len)
212 }
213
214 fn window_at(&self, _range: ByteRange) -> Result<&[u8], ReadAtError> {
215 Err(ReadAtError::UnavailableWindow)
216 }
217}
218
219#[derive(Debug, Clone, Copy)]
221pub struct SliceBackend<'a> {
222 bytes: &'a [u8],
223}
224
225impl<'a> SliceBackend<'a> {
226 pub fn new(bytes: &'a [u8]) -> Self {
227 Self { bytes }
228 }
229
230 pub fn as_slice(&self) -> &'a [u8] {
231 self.bytes
232 }
233}
234
235impl MediaReadAt for SliceBackend<'_> {
236 fn len_u64(&self) -> u64 {
237 self.bytes.len() as u64
238 }
239
240 fn read_at(&self, range: ByteRange, dst: &mut [u8]) -> Result<usize, ReadAtError> {
241 let source = ReadBackend::Slice(self.bytes);
242 source.read_at(range, dst)
243 }
244
245 fn window_at(&self, range: ByteRange) -> Result<&[u8], ReadAtError> {
246 let bounds = exact_bounds(range, self.bytes.len())?;
247 Ok(&self.bytes[bounds])
248 }
249
250 fn window_at_partial(&self, range: ByteRange) -> Result<&[u8], ReadAtError> {
251 let bounds = partial_bounds(range, self.bytes.len())?;
252 Ok(&self.bytes[bounds])
253 }
254
255 fn as_contiguous(&self) -> Option<&[u8]> {
256 Some(self.bytes)
257 }
258}
259
260#[cfg(feature = "mmap")]
262#[derive(Debug, Clone, Copy)]
263pub struct MmapBackend<'a> {
264 mmap: &'a memmap2::Mmap,
265}
266
267#[cfg(feature = "mmap")]
268impl<'a> MmapBackend<'a> {
269 pub fn new(mmap: &'a memmap2::Mmap) -> Self {
270 Self { mmap }
271 }
272
273 pub fn as_slice(&self) -> &'a [u8] {
274 self.mmap.as_ref()
275 }
276}
277
278#[cfg(feature = "mmap")]
279impl MediaReadAt for MmapBackend<'_> {
280 fn len_u64(&self) -> u64 {
281 self.mmap.len() as u64
282 }
283
284 fn read_at(&self, range: ByteRange, dst: &mut [u8]) -> Result<usize, ReadAtError> {
285 let source = ReadBackend::Mapped(self.mmap);
286 source.read_at(range, dst)
287 }
288
289 fn window_at(&self, range: ByteRange) -> Result<&[u8], ReadAtError> {
290 let bytes = self.mmap.as_ref();
291 let bounds = exact_bounds(range, bytes.len())?;
292 Ok(&bytes[bounds])
293 }
294
295 fn window_at_partial(&self, range: ByteRange) -> Result<&[u8], ReadAtError> {
296 let bytes = self.mmap.as_ref();
297 let bounds = partial_bounds(range, bytes.len())?;
298 Ok(&bytes[bounds])
299 }
300
301 fn as_contiguous(&self) -> Option<&[u8]> {
302 Some(self.mmap.as_ref())
303 }
304}
305
306#[derive(Debug, Clone, Copy)]
311pub enum ReadBackend<'a> {
312 Slice(&'a [u8]),
314 #[cfg(feature = "mmap")]
320 Mapped(&'a memmap2::Mmap),
321}
322
323impl<'a> ReadBackend<'a> {
324 #[inline]
329 pub fn as_slice(&self) -> &[u8] {
330 match self {
331 ReadBackend::Slice(s) => s,
332 #[cfg(feature = "mmap")]
333 ReadBackend::Mapped(m) => m.as_ref(),
334 }
335 }
336
337 #[inline]
338 pub fn len(&self) -> usize {
339 self.as_slice().len()
340 }
341
342 #[inline]
343 pub fn is_empty(&self) -> bool {
344 self.len() == 0
345 }
346}
347
348fn exact_bounds(range: ByteRange, source_len: usize) -> Result<Range<usize>, ReadAtError> {
349 bounds(range, source_len, false)
350}
351
352fn partial_bounds(range: ByteRange, source_len: usize) -> Result<Range<usize>, ReadAtError> {
353 bounds(range, source_len, true)
354}
355
356fn bounds(
357 range: ByteRange,
358 source_len: usize,
359 allow_partial: bool,
360) -> Result<Range<usize>, ReadAtError> {
361 let source_len_u64 = source_len as u64;
362 let start = usize::try_from(range.offset).map_err(|_| ReadAtError::OffsetOutOfBounds {
363 offset: range.offset,
364 source_len: source_len_u64,
365 })?;
366 if start > source_len || (start == source_len && !range.is_empty()) {
367 return Err(ReadAtError::OffsetOutOfBounds {
368 offset: range.offset,
369 source_len: source_len_u64,
370 });
371 }
372 let requested_len = range.len_usize()?;
373 let requested_end = start
374 .checked_add(requested_len)
375 .ok_or(ReadAtError::RangeOverflow { offset: range.offset, len: range.len })?;
376 if requested_end <= source_len {
377 Ok(start..requested_end)
378 } else if allow_partial {
379 Ok(start..source_len)
380 } else {
381 Err(ReadAtError::RangeOutOfBounds {
382 offset: range.offset,
383 len: range.len,
384 source_len: source_len_u64,
385 })
386 }
387}
388
389impl MediaReadAt for ReadBackend<'_> {
390 #[inline]
391 fn len_u64(&self) -> u64 {
392 self.as_slice().len() as u64
393 }
394
395 fn read_at(&self, range: ByteRange, dst: &mut [u8]) -> Result<usize, ReadAtError> {
396 let requested_len = range.len_usize()?;
397 if dst.len() < requested_len {
398 return Err(ReadAtError::DestinationTooSmall {
399 requested: requested_len,
400 available: dst.len(),
401 });
402 }
403 let window = self.window_at(range)?;
404 dst[..requested_len].copy_from_slice(window);
405 Ok(requested_len)
406 }
407
408 fn window_at(&self, range: ByteRange) -> Result<&[u8], ReadAtError> {
409 let bounds = exact_bounds(range, self.as_slice().len())?;
410 Ok(&self.as_slice()[bounds])
411 }
412
413 fn window_at_partial(&self, range: ByteRange) -> Result<&[u8], ReadAtError> {
414 let bounds = partial_bounds(range, self.as_slice().len())?;
415 Ok(&self.as_slice()[bounds])
416 }
417
418 fn as_contiguous(&self) -> Option<&[u8]> {
419 Some(self.as_slice())
420 }
421}
422
423impl ByteSource for ReadBackend<'_> {
424 #[inline]
425 fn len(&self) -> usize {
426 self.as_slice().len()
427 }
428
429 #[inline]
430 fn byte_at(&self, offset: usize) -> Option<u8> {
431 self.as_slice().get(offset).copied()
432 }
433
434 #[inline]
435 fn slice_at(&self, offset: usize, len: usize) -> Option<&[u8]> {
436 let range = ByteRange::from_usize(offset, len).ok()?;
437 self.window_at(range).ok()
438 }
439}
440
441impl<'a> From<&'a [u8]> for ReadBackend<'a> {
442 #[inline]
443 fn from(slice: &'a [u8]) -> Self {
444 ReadBackend::Slice(slice)
445 }
446}
447
448impl<'a> From<SliceBackend<'a>> for ReadBackend<'a> {
449 #[inline]
450 fn from(slice: SliceBackend<'a>) -> Self {
451 ReadBackend::Slice(slice.as_slice())
452 }
453}
454
455impl<'a> From<&'a Vec<u8>> for ReadBackend<'a> {
456 #[inline]
457 fn from(v: &'a Vec<u8>) -> Self {
458 ReadBackend::Slice(v.as_slice())
459 }
460}
461
462#[cfg(feature = "mmap")]
463impl<'a> From<&'a memmap2::Mmap> for ReadBackend<'a> {
464 #[inline]
465 fn from(mmap: &'a memmap2::Mmap) -> Self {
466 ReadBackend::Mapped(mmap)
467 }
468}
469
470#[cfg(feature = "mmap")]
471impl<'a> From<MmapBackend<'a>> for ReadBackend<'a> {
472 #[inline]
473 fn from(mmap: MmapBackend<'a>) -> Self {
474 ReadBackend::Mapped(mmap.mmap)
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn byte_range_rejects_offset_overflow() {
484 assert_eq!(
485 ByteRange::new(u64::MAX, 1),
486 Err(ReadAtError::RangeOverflow { offset: u64::MAX, len: 1 })
487 );
488 }
489
490 #[test]
491 fn read_backend_windows_exact_ranges() {
492 let bytes = [0, 1, 2, 3, 4, 5];
493 let source = ReadBackend::Slice(&bytes);
494
495 assert_eq!(source.len_u64(), 6);
496 assert_eq!(source.window_at(ByteRange::new(2, 3).unwrap()).unwrap(), &[2, 3, 4]);
497 assert_eq!(
498 source.window_at(ByteRange::new(4, 4).unwrap()),
499 Err(ReadAtError::RangeOutOfBounds { offset: 4, len: 4, source_len: 6 })
500 );
501 }
502
503 #[test]
504 fn read_backend_windows_partial_ranges() {
505 let bytes = [0, 1, 2, 3, 4, 5];
506 let source = ReadBackend::Slice(&bytes);
507
508 assert_eq!(source.window_at_partial(ByteRange::new(4, 4).unwrap()).unwrap(), &[4, 5]);
509 assert_eq!(
510 source.window_at_partial(ByteRange::new(6, 1).unwrap()),
511 Err(ReadAtError::OffsetOutOfBounds { offset: 6, source_len: 6 })
512 );
513 }
514
515 #[test]
516 fn read_backend_copies_exact_ranges() {
517 let bytes = [10, 11, 12, 13];
518 let source = ReadBackend::Slice(&bytes);
519 let mut out = [0u8; 2];
520
521 assert_eq!(source.read_at(ByteRange::new(1, 2).unwrap(), &mut out), Ok(2));
522 assert_eq!(out, [11, 12]);
523 assert_eq!(
524 source.read_at(ByteRange::new(1, 3).unwrap(), &mut out),
525 Err(ReadAtError::DestinationTooSmall { requested: 3, available: 2 })
526 );
527 }
528
529 #[test]
530 fn slice_backend_matches_read_backend_semantics() {
531 let bytes = [20, 21, 22, 23, 24];
532 let source = SliceBackend::new(&bytes);
533
534 assert_eq!(source.len_u64(), 5);
535 assert_eq!(source.window_at(ByteRange::new(1, 3).unwrap()).unwrap(), &[21, 22, 23]);
536
537 let mut copied = [0; 2];
538 assert_eq!(source.read_at(ByteRange::new(3, 2).unwrap(), &mut copied), Ok(2));
539 assert_eq!(copied, [23, 24]);
540 }
541
542 #[test]
543 fn file_backend_copies_exact_ranges_without_windows() {
544 use std::io::Write;
545
546 let path = std::env::temp_dir().join(format!(
547 "revelo-core-file-backend-{}-{}.bin",
548 std::process::id(),
549 "range"
550 ));
551 let mut file = std::fs::File::create(&path).unwrap();
552 file.write_all(&[50, 51, 52, 53, 54, 55]).unwrap();
553 file.sync_all().unwrap();
554 drop(file);
555
556 let source = FileBackend::open(&path).unwrap();
557 let mut copied = [0; 3];
558
559 assert_eq!(source.len_u64(), 6);
560 assert_eq!(source.read_at(ByteRange::new(2, 3).unwrap(), &mut copied), Ok(3));
561 assert_eq!(copied, [52, 53, 54]);
562 assert_eq!(
563 source.window_at(ByteRange::new(0, 1).unwrap()),
564 Err(ReadAtError::UnavailableWindow)
565 );
566 assert_eq!(
567 source.read_at(ByteRange::new(5, 2).unwrap(), &mut copied),
568 Err(ReadAtError::RangeOutOfBounds { offset: 5, len: 2, source_len: 6 })
569 );
570
571 std::fs::remove_file(path).unwrap();
572 }
573}