Skip to main content

hermes_core/directories/
directory.rs

1//! Async Directory abstraction for IO operations
2//!
3//! Supports network, local filesystem, and in-memory storage.
4//! All reads are async to minimize blocking on network latency.
5
6use async_trait::async_trait;
7use parking_lot::RwLock;
8use std::collections::HashMap;
9use std::io;
10use std::ops::Range;
11use std::path::{Path, PathBuf};
12use std::sync::Arc;
13
14/// Callback type for lazy range reading
15#[cfg(not(target_arch = "wasm32"))]
16pub type RangeReadFn = Arc<
17    dyn Fn(
18            Range<u64>,
19        )
20            -> std::pin::Pin<Box<dyn std::future::Future<Output = io::Result<OwnedBytes>> + Send>>
21        + Send
22        + Sync,
23>;
24
25#[cfg(target_arch = "wasm32")]
26pub type RangeReadFn = Arc<
27    dyn Fn(
28        Range<u64>,
29    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = io::Result<OwnedBytes>>>>,
30>;
31
32/// Unified file handle for both inline (mmap/RAM) and lazy (HTTP/filesystem) access.
33///
34/// Replaces the previous `FileSlice`, `LazyFileHandle`, and `LazyFileSlice` types.
35/// - **Inline**: data is available synchronously (mmap, RAM). Sync reads via `read_bytes_range_sync`.
36/// - **Lazy**: data is fetched on-demand via async callback (HTTP, filesystem).
37///
38/// Use `.slice()` to create sub-range views (zero-copy for Inline, offset-adjusted for Lazy).
39#[derive(Clone)]
40pub struct FileHandle {
41    inner: FileHandleInner,
42}
43
44#[derive(Clone)]
45enum FileHandleInner {
46    /// Data available inline — sync reads possible (mmap, RAM)
47    Inline {
48        data: OwnedBytes,
49        offset: u64,
50        len: u64,
51    },
52    /// Data fetched on-demand via async callback (HTTP, filesystem)
53    Lazy {
54        read_fn: RangeReadFn,
55        offset: u64,
56        len: u64,
57        /// Index name for the `hermes_directory_read_*` metric labels.
58        label: Arc<str>,
59    },
60}
61
62/// Late-bound index name for Directory-layer metric labels
63/// (`hermes_directory_read_*`, `hermes_cold_write_bytes_total`).
64///
65/// Directories are constructed before the schema is loaded, so the label is
66/// attached afterwards: `Index::open`/`create` call
67/// `Directory::set_index_label(schema.index_label())` on the index's
68/// directory instance. Reads happen at handle/writer creation, not per IO.
69#[derive(Clone, Debug)]
70pub struct IndexLabel(Arc<std::sync::RwLock<Arc<str>>>);
71
72impl Default for IndexLabel {
73    fn default() -> Self {
74        Self(Arc::new(std::sync::RwLock::new(Arc::from("unknown"))))
75    }
76}
77
78impl IndexLabel {
79    /// Current label ("unknown" until set).
80    pub fn get(&self) -> Arc<str> {
81        self.0.read().expect("IndexLabel lock poisoned").clone()
82    }
83
84    /// Set the label (idempotent; last write wins).
85    pub fn set(&self, label: &str) {
86        *self.0.write().expect("IndexLabel lock poisoned") = Arc::from(label);
87    }
88}
89
90impl std::fmt::Debug for FileHandle {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match &self.inner {
93            FileHandleInner::Inline { len, offset, .. } => f
94                .debug_struct("FileHandle::Inline")
95                .field("offset", offset)
96                .field("len", len)
97                .finish(),
98            FileHandleInner::Lazy { len, offset, .. } => f
99                .debug_struct("FileHandle::Lazy")
100                .field("offset", offset)
101                .field("len", len)
102                .finish(),
103        }
104    }
105}
106
107impl FileHandle {
108    /// Create an inline file handle from owned bytes (mmap, RAM).
109    /// Sync reads are available.
110    pub fn from_bytes(data: OwnedBytes) -> Self {
111        let len = data.len() as u64;
112        Self {
113            inner: FileHandleInner::Inline {
114                data,
115                offset: 0,
116                len,
117            },
118        }
119    }
120
121    /// Create an empty file handle.
122    pub fn empty() -> Self {
123        Self::from_bytes(OwnedBytes::empty())
124    }
125
126    /// Create a lazy file handle from an async range-read callback.
127    /// Only async reads are available. Reads emit `hermes_directory_read_*`
128    /// with `index="unknown"` — use [`FileHandle::lazy_labeled`] when the
129    /// owning index is known.
130    pub fn lazy(len: u64, read_fn: RangeReadFn) -> Self {
131        Self::lazy_labeled(len, read_fn, Arc::from("unknown"))
132    }
133
134    /// [`FileHandle::lazy`] with an index name for metric labels.
135    pub fn lazy_labeled(len: u64, read_fn: RangeReadFn, label: Arc<str>) -> Self {
136        Self {
137            inner: FileHandleInner::Lazy {
138                read_fn,
139                offset: 0,
140                len,
141                label,
142            },
143        }
144    }
145
146    /// Total length in bytes.
147    #[inline]
148    pub fn len(&self) -> u64 {
149        match &self.inner {
150            FileHandleInner::Inline { len, .. } => *len,
151            FileHandleInner::Lazy { len, .. } => *len,
152        }
153    }
154
155    /// Check if empty.
156    #[inline]
157    pub fn is_empty(&self) -> bool {
158        self.len() == 0
159    }
160
161    /// Whether synchronous reads are available (inline/mmap data).
162    #[inline]
163    pub fn is_sync(&self) -> bool {
164        matches!(&self.inner, FileHandleInner::Inline { .. })
165    }
166
167    /// Create a sub-range view. Zero-copy for Inline, offset-adjusted for Lazy.
168    pub fn slice(&self, range: Range<u64>) -> Self {
169        match &self.inner {
170            FileHandleInner::Inline { data, offset, len } => {
171                let new_offset = offset + range.start;
172                let new_len = range.end - range.start;
173                debug_assert!(
174                    new_offset + new_len <= offset + len,
175                    "slice out of bounds: {}+{} > {}+{}",
176                    new_offset,
177                    new_len,
178                    offset,
179                    len
180                );
181                Self {
182                    inner: FileHandleInner::Inline {
183                        data: data.clone(),
184                        offset: new_offset,
185                        len: new_len,
186                    },
187                }
188            }
189            FileHandleInner::Lazy {
190                read_fn,
191                offset,
192                len,
193                label,
194            } => {
195                let new_offset = offset + range.start;
196                let new_len = range.end - range.start;
197                debug_assert!(
198                    new_offset + new_len <= offset + len,
199                    "slice out of bounds: {}+{} > {}+{}",
200                    new_offset,
201                    new_len,
202                    offset,
203                    len
204                );
205                Self {
206                    inner: FileHandleInner::Lazy {
207                        read_fn: Arc::clone(read_fn),
208                        offset: new_offset,
209                        len: new_len,
210                        label: Arc::clone(label),
211                    },
212                }
213            }
214        }
215    }
216
217    /// Advise the kernel about the access pattern for a byte range of this handle.
218    ///
219    /// Only effective for Inline handles backed by mmap; no-op for Lazy
220    /// handles (HTTP, filesystem callbacks) and heap-backed data.
221    #[cfg(feature = "native")]
222    pub fn madvise_range(&self, range: Range<u64>, advice: libc::c_int) {
223        if let FileHandleInner::Inline { data, offset, len } = &self.inner {
224            let end = range.end.min(*len);
225            if range.start >= end {
226                return;
227            }
228            let start = (*offset + range.start) as usize;
229            let end = (*offset + end) as usize;
230            data.madvise_range(start..end, advice);
231        }
232    }
233
234    /// Async range read — works for both Inline and Lazy.
235    pub async fn read_bytes_range(&self, range: Range<u64>) -> io::Result<OwnedBytes> {
236        match &self.inner {
237            FileHandleInner::Inline { data, offset, len } => {
238                if range.end > *len {
239                    return Err(io::Error::new(
240                        io::ErrorKind::InvalidInput,
241                        format!("Range {:?} out of bounds (len: {})", range, len),
242                    ));
243                }
244                let start = (*offset + range.start) as usize;
245                let end = (*offset + range.end) as usize;
246                Ok(data.slice(start..end))
247            }
248            FileHandleInner::Lazy {
249                read_fn,
250                offset,
251                len,
252                label,
253            } => {
254                if range.end > *len {
255                    return Err(io::Error::new(
256                        io::ErrorKind::InvalidInput,
257                        format!("Range {:?} out of bounds (len: {})", range, len),
258                    ));
259                }
260                let abs_start = offset + range.start;
261                let abs_end = offset + range.end;
262                // Real IO (HTTP / custom read_fn) — mmap-backed Inline handles
263                // above are zero-copy slices whose latency materializes as
264                // page faults inside the query-phase histograms instead.
265                let t = crate::observe::Timer::start();
266                let result = (read_fn)(abs_start..abs_end).await;
267                if let Ok(bytes) = &result {
268                    crate::observe::directory_read(label, "lazy_range", t.secs(), bytes.len());
269                }
270                result
271            }
272        }
273    }
274
275    /// Read all bytes.
276    pub async fn read_bytes(&self) -> io::Result<OwnedBytes> {
277        self.read_bytes_range(0..self.len()).await
278    }
279
280    /// Synchronous range read — only works for Inline handles.
281    /// Returns `Err` if the handle is Lazy.
282    #[inline]
283    pub fn read_bytes_range_sync(&self, range: Range<u64>) -> io::Result<OwnedBytes> {
284        match &self.inner {
285            FileHandleInner::Inline { data, offset, len } => {
286                if range.end > *len {
287                    return Err(io::Error::new(
288                        io::ErrorKind::InvalidInput,
289                        format!("Range {:?} out of bounds (len: {})", range, len),
290                    ));
291                }
292                let start = (*offset + range.start) as usize;
293                let end = (*offset + range.end) as usize;
294                Ok(data.slice(start..end))
295            }
296            FileHandleInner::Lazy { .. } => Err(io::Error::new(
297                io::ErrorKind::Unsupported,
298                "Synchronous read not available on lazy file handle",
299            )),
300        }
301    }
302
303    /// Synchronous read of all bytes — only works for Inline handles.
304    #[inline]
305    pub fn read_bytes_sync(&self) -> io::Result<OwnedBytes> {
306        self.read_bytes_range_sync(0..self.len())
307    }
308}
309
310/// Backing store for OwnedBytes — supports both heap Vec and mmap.
311#[derive(Clone)]
312enum SharedBytes {
313    Vec(Arc<Vec<u8>>),
314    #[cfg(feature = "native")]
315    Mmap(Arc<memmap2::Mmap>),
316}
317
318impl SharedBytes {
319    #[inline]
320    fn as_bytes(&self) -> &[u8] {
321        match self {
322            SharedBytes::Vec(v) => v.as_slice(),
323            #[cfg(feature = "native")]
324            SharedBytes::Mmap(m) => m.as_ref(),
325        }
326    }
327}
328
329impl std::fmt::Debug for SharedBytes {
330    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
331        match self {
332            SharedBytes::Vec(v) => write!(f, "Vec(len={})", v.len()),
333            #[cfg(feature = "native")]
334            SharedBytes::Mmap(m) => write!(f, "Mmap(len={})", m.len()),
335        }
336    }
337}
338
339/// Owned bytes with cheap cloning (Arc-backed)
340///
341/// Supports two backing stores:
342/// - `Vec<u8>` for owned data (RamDirectory, FsDirectory, decompressed blocks)
343/// - `Mmap` for zero-copy memory-mapped files (MmapDirectory, native only)
344#[derive(Debug, Clone)]
345pub struct OwnedBytes {
346    data: SharedBytes,
347    range: Range<usize>,
348}
349
350impl OwnedBytes {
351    pub fn new(data: Vec<u8>) -> Self {
352        let len = data.len();
353        Self {
354            data: SharedBytes::Vec(Arc::new(data)),
355            range: 0..len,
356        }
357    }
358
359    pub fn empty() -> Self {
360        Self {
361            data: SharedBytes::Vec(Arc::new(Vec::new())),
362            range: 0..0,
363        }
364    }
365
366    /// Create from a pre-existing Arc<Vec<u8>> with a sub-range.
367    /// Used by RamDirectory and CachingDirectory to share data without copying.
368    pub(crate) fn from_arc_vec(data: Arc<Vec<u8>>, range: Range<usize>) -> Self {
369        Self {
370            data: SharedBytes::Vec(data),
371            range,
372        }
373    }
374
375    /// Create from a memory-mapped file (zero-copy).
376    #[cfg(feature = "native")]
377    pub(crate) fn from_mmap(mmap: Arc<memmap2::Mmap>) -> Self {
378        let len = mmap.len();
379        Self {
380            data: SharedBytes::Mmap(mmap),
381            range: 0..len,
382        }
383    }
384
385    /// Create from a memory-mapped file with a sub-range (zero-copy).
386    #[cfg(feature = "native")]
387    pub(crate) fn from_mmap_range(mmap: Arc<memmap2::Mmap>, range: Range<usize>) -> Self {
388        Self {
389            data: SharedBytes::Mmap(mmap),
390            range,
391        }
392    }
393
394    pub fn len(&self) -> usize {
395        self.range.len()
396    }
397
398    pub fn is_empty(&self) -> bool {
399        self.range.is_empty()
400    }
401
402    pub fn slice(&self, range: Range<usize>) -> Self {
403        let start = self.range.start + range.start;
404        let end = self.range.start + range.end;
405        Self {
406            data: self.data.clone(),
407            range: start..end,
408        }
409    }
410
411    pub fn as_slice(&self) -> &[u8] {
412        &self.data.as_bytes()[self.range.clone()]
413    }
414
415    /// Returns `true` if the backing store is a memory-mapped file.
416    ///
417    /// Used to guard `madvise` calls: `MADV_DONTNEED` on heap memory
418    /// zeroes pages on Linux and corrupts allocator metadata.
419    #[cfg(feature = "native")]
420    #[inline]
421    pub fn is_mmap(&self) -> bool {
422        matches!(self.data, SharedBytes::Mmap(_))
423    }
424
425    /// Advise the kernel about the access pattern for these bytes.
426    ///
427    /// No-op unless the backing store is mmap (heap memory must never be
428    /// madvised: `MADV_DONTNEED` on heap zeroes pages and corrupts allocator
429    /// metadata) or the range is empty.
430    #[cfg(feature = "native")]
431    pub fn madvise(&self, advice: libc::c_int) {
432        self.madvise_range(0..self.len(), advice);
433    }
434
435    /// Pin these bytes in physical memory (`mlock`). mmap-backed only —
436    /// heap memory is not evictable by the page cache. Returns whether the
437    /// lock succeeded; failure (e.g. RLIMIT_MEMLOCK) is not fatal.
438    /// Locks are released automatically when the mapping is unmapped.
439    #[cfg(feature = "native")]
440    pub fn mlock(&self) -> bool {
441        if !self.is_mmap() {
442            return false;
443        }
444        let slice = self.as_slice();
445        if slice.is_empty() {
446            return true;
447        }
448        let ptr = slice.as_ptr();
449        let len = slice.len();
450        let page_size = 4096usize;
451        let aligned_ptr = (ptr as usize) & !(page_size - 1);
452        let aligned_len = len + (ptr as usize - aligned_ptr);
453        unsafe { libc::mlock(aligned_ptr as *const libc::c_void, aligned_len) == 0 }
454    }
455
456    /// Advise the kernel about the access pattern for a sub-range.
457    ///
458    /// The range is relative to these bytes. Same mmap-only guard as
459    /// [`Self::madvise`]. The pointer is aligned down to a page boundary
460    /// as required by `madvise`.
461    #[cfg(feature = "native")]
462    pub fn madvise_range(&self, range: Range<usize>, advice: libc::c_int) {
463        if !self.is_mmap() {
464            return;
465        }
466        let slice = &self.as_slice()[range];
467        if slice.is_empty() {
468            return;
469        }
470        let ptr = slice.as_ptr();
471        let len = slice.len();
472        let page_size = 4096usize;
473        let aligned_ptr = (ptr as usize) & !(page_size - 1);
474        let aligned_len = len + (ptr as usize - aligned_ptr);
475        unsafe {
476            libc::madvise(aligned_ptr as *mut libc::c_void, aligned_len, advice);
477        }
478    }
479
480    pub fn to_vec(&self) -> Vec<u8> {
481        self.as_slice().to_vec()
482    }
483}
484
485impl AsRef<[u8]> for OwnedBytes {
486    fn as_ref(&self) -> &[u8] {
487        self.as_slice()
488    }
489}
490
491impl std::ops::Deref for OwnedBytes {
492    type Target = [u8];
493
494    fn deref(&self) -> &Self::Target {
495        self.as_slice()
496    }
497}
498
499/// Async directory trait for reading index files
500#[cfg(not(target_arch = "wasm32"))]
501#[async_trait]
502pub trait Directory: Send + Sync + 'static {
503    /// Check if a file exists
504    async fn exists(&self, path: &Path) -> io::Result<bool>;
505
506    /// Get file size
507    async fn file_size(&self, path: &Path) -> io::Result<u64>;
508
509    /// Open a file for reading (loads entire file into an inline FileHandle)
510    async fn open_read(&self, path: &Path) -> io::Result<FileHandle>;
511
512    /// Read a specific byte range from a file (optimized for network)
513    async fn read_range(&self, path: &Path, range: Range<u64>) -> io::Result<OwnedBytes>;
514
515    /// List files in directory
516    async fn list_files(&self, prefix: &Path) -> io::Result<Vec<PathBuf>>;
517
518    /// Open a file handle that fetches ranges on demand.
519    /// For mmap directories this returns an Inline handle (sync-capable).
520    /// For HTTP/filesystem directories this returns a Lazy handle.
521    async fn open_lazy(&self, path: &Path) -> io::Result<FileHandle>;
522
523    /// Attach the owning index's name for Directory-layer metric labels
524    /// (`hermes_directory_read_*`, `hermes_cold_write_bytes_total`).
525    /// Called by `Index::open`/`create` once the schema is loaded; wrappers
526    /// forward to their inner directory. Default: no-op (directories that
527    /// emit no Directory-layer metrics, e.g. RamDirectory).
528    fn set_index_label(&self, _label: &str) {}
529
530    /// Resolve a directory-relative file to a native filesystem path.
531    ///
532    /// Local backends expose this so large, short-lived merge scratch files
533    /// can live beside the index instead of silently spilling to the
534    /// container's root filesystem. Remote and in-memory backends return
535    /// `None`.
536    fn local_path(&self, _path: &Path) -> Option<PathBuf> {
537        None
538    }
539}
540
541/// Async directory trait for reading index files (WASM version - no Send requirement)
542#[cfg(target_arch = "wasm32")]
543#[async_trait(?Send)]
544pub trait Directory: 'static {
545    /// Check if a file exists
546    async fn exists(&self, path: &Path) -> io::Result<bool>;
547
548    /// Get file size
549    async fn file_size(&self, path: &Path) -> io::Result<u64>;
550
551    /// Open a file for reading (loads entire file into an inline FileHandle)
552    async fn open_read(&self, path: &Path) -> io::Result<FileHandle>;
553
554    /// Read a specific byte range from a file (optimized for network)
555    async fn read_range(&self, path: &Path, range: Range<u64>) -> io::Result<OwnedBytes>;
556
557    /// List files in directory
558    async fn list_files(&self, prefix: &Path) -> io::Result<Vec<PathBuf>>;
559
560    /// Open a file handle that fetches ranges on demand.
561    async fn open_lazy(&self, path: &Path) -> io::Result<FileHandle>;
562
563    /// Attach the owning index's name for Directory-layer metric labels.
564    /// No-op default; metrics are native-only but the label is harmless.
565    fn set_index_label(&self, _label: &str) {}
566
567    /// WASM backends do not expose a native filesystem path.
568    fn local_path(&self, _path: &Path) -> Option<PathBuf> {
569        None
570    }
571}
572
573/// A writer for incrementally writing data to a directory file.
574///
575/// Avoids buffering entire files in memory during merge. File-backed
576/// directories write directly to disk; memory directories collect to Vec.
577pub trait StreamingWriter: io::Write + Send {
578    /// Finalize the write, making data available for reading.
579    fn finish(self: Box<Self>) -> io::Result<()>;
580
581    /// Bytes written so far.
582    fn bytes_written(&self) -> u64;
583
584    /// Copy one local-file range at the current output position without
585    /// routing bytes through a userspace buffer.
586    ///
587    /// Filesystem writers implement this with Linux `copy_file_range`.
588    /// Other backends return `Unsupported`, allowing merge code to fall back
589    /// to its portable mmap/read + write path before any bytes are copied.
590    #[cfg(feature = "native")]
591    fn copy_from_file_range(
592        &mut self,
593        _source: &std::fs::File,
594        _source_offset: &mut u64,
595        _len: usize,
596    ) -> io::Result<usize> {
597        Err(io::Error::new(
598            io::ErrorKind::Unsupported,
599            "streaming writer does not support kernel-assisted range copies",
600        ))
601    }
602}
603
604/// StreamingWriter backed by Vec<u8>, finalized via DirectoryWriter::write.
605/// Used as default/fallback and for RamDirectory.
606struct BufferedStreamingWriter {
607    path: PathBuf,
608    buffer: Vec<u8>,
609    /// Callback to write the buffer to the directory on finish.
610    /// We store the files Arc directly for RamDirectory.
611    files: Arc<RwLock<HashMap<PathBuf, Arc<Vec<u8>>>>>,
612}
613
614impl io::Write for BufferedStreamingWriter {
615    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
616        self.buffer.extend_from_slice(buf);
617        Ok(buf.len())
618    }
619
620    fn flush(&mut self) -> io::Result<()> {
621        Ok(())
622    }
623}
624
625impl StreamingWriter for BufferedStreamingWriter {
626    fn finish(self: Box<Self>) -> io::Result<()> {
627        self.files.write().insert(self.path, Arc::new(self.buffer));
628        Ok(())
629    }
630
631    fn bytes_written(&self) -> u64 {
632        self.buffer.len() as u64
633    }
634}
635
636/// Buffer size for FileStreamingWriter (8 MB).
637/// Large enough to coalesce millions of tiny writes (e.g. per-vector doc_id writes)
638/// into efficient sequential I/O.
639#[cfg(feature = "native")]
640const FILE_STREAMING_BUF_SIZE: usize = 8 * 1024 * 1024;
641
642/// StreamingWriter backed by a buffered std::fs::File for filesystem directories.
643#[cfg(feature = "native")]
644pub(crate) struct FileStreamingWriter {
645    pub(crate) file: io::BufWriter<std::fs::File>,
646    pub(crate) written: u64,
647}
648
649#[cfg(feature = "native")]
650impl FileStreamingWriter {
651    pub(crate) fn new(file: std::fs::File) -> Self {
652        Self {
653            file: io::BufWriter::with_capacity(FILE_STREAMING_BUF_SIZE, file),
654            written: 0,
655        }
656    }
657}
658
659#[cfg(feature = "native")]
660impl io::Write for FileStreamingWriter {
661    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
662        let n = self.file.write(buf)?;
663        self.written += n as u64;
664        Ok(n)
665    }
666
667    fn flush(&mut self) -> io::Result<()> {
668        self.file.flush()
669    }
670}
671
672#[cfg(feature = "native")]
673impl StreamingWriter for FileStreamingWriter {
674    fn finish(self: Box<Self>) -> io::Result<()> {
675        let file = self.file.into_inner().map_err(|e| e.into_error())?;
676        file.sync_all()?;
677        Ok(())
678    }
679
680    fn bytes_written(&self) -> u64 {
681        self.written
682    }
683
684    fn copy_from_file_range(
685        &mut self,
686        source: &std::fs::File,
687        source_offset: &mut u64,
688        len: usize,
689    ) -> io::Result<usize> {
690        io::Write::flush(&mut self.file)?;
691        let copied = copy_file_range_once(source, source_offset, self.file.get_ref(), len)?;
692        self.written = self
693            .written
694            .checked_add(copied as u64)
695            .ok_or_else(|| io::Error::other("streaming-writer byte count overflow"))?;
696        Ok(copied)
697    }
698}
699
700#[cfg(feature = "native")]
701pub(crate) fn copy_file_range_once(
702    source: &std::fs::File,
703    source_offset: &mut u64,
704    destination: &std::fs::File,
705    len: usize,
706) -> io::Result<usize> {
707    #[cfg(target_os = "linux")]
708    {
709        use std::os::fd::AsRawFd;
710
711        let mut offset = libc::loff_t::try_from(*source_offset).map_err(|_| {
712            io::Error::new(io::ErrorKind::InvalidInput, "source offset exceeds i64")
713        })?;
714        let copied = unsafe {
715            libc::copy_file_range(
716                source.as_raw_fd(),
717                &mut offset,
718                destination.as_raw_fd(),
719                std::ptr::null_mut(),
720                len,
721                0,
722            )
723        };
724        if copied < 0 {
725            let error = io::Error::last_os_error();
726            let unsupported = error.raw_os_error().is_some_and(|code| {
727                code == libc::ENOSYS
728                    || code == libc::EXDEV
729                    || code == libc::EOPNOTSUPP
730                    || code == libc::EINVAL
731            });
732            return if unsupported {
733                Err(io::Error::new(io::ErrorKind::Unsupported, error))
734            } else {
735                Err(error)
736            };
737        }
738        *source_offset = u64::try_from(offset)
739            .map_err(|_| io::Error::other("copy_file_range returned a negative source offset"))?;
740        Ok(copied as usize)
741    }
742    #[cfg(not(target_os = "linux"))]
743    {
744        let _ = (source, source_offset, destination, len);
745        Err(io::Error::new(
746            io::ErrorKind::Unsupported,
747            "kernel-assisted range copies are only available on Linux",
748        ))
749    }
750}
751
752/// Async directory trait for writing index files
753#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
754#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
755pub trait DirectoryWriter: Directory {
756    /// Create/overwrite a file with data
757    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()>;
758
759    /// Create/overwrite a file with data, durably.
760    ///
761    /// [`Self::write`] does not guarantee the bytes reach stable storage
762    /// before returning (filesystem implementations leave them in the OS
763    /// page cache). Any file that durably-published metadata will reference
764    /// (e.g. segment `.meta`) must be written through this method instead:
765    /// it routes through [`Self::streaming_writer`], whose `finish()` fsyncs
766    /// on filesystem implementations.
767    async fn write_durable(&self, path: &Path, data: &[u8]) -> io::Result<()> {
768        use io::Write as _;
769        let mut writer = self.streaming_writer(path).await?;
770        writer.write_all(data)?;
771        writer.finish()
772    }
773
774    /// Delete a file
775    async fn delete(&self, path: &Path) -> io::Result<()>;
776
777    /// Atomic rename
778    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
779
780    /// Create another immutable name for an existing file without copying its
781    /// contents when the backend supports it. Segment rewrites use this to
782    /// retain unchanged multi-gigabyte files while replacing only one index
783    /// payload. Backends without link semantics return `Unsupported`; callers
784    /// then fall back to a streaming copy.
785    async fn link(&self, _from: &Path, _to: &Path) -> io::Result<()> {
786        Err(io::Error::new(
787            io::ErrorKind::Unsupported,
788            "directory backend does not support immutable file links",
789        ))
790    }
791
792    /// Sync all pending writes
793    async fn sync(&self) -> io::Result<()>;
794
795    /// Create a streaming writer for incremental file writes.
796    /// Call finish() on the returned writer to finalize.
797    async fn streaming_writer(&self, path: &Path) -> io::Result<Box<dyn StreamingWriter>>;
798
799    /// Streaming writer for **bulk one-shot data** (merge/reorder outputs).
800    ///
801    /// Filesystem directories return a page-cache-dropping writer (see
802    /// `docs/cold-io.md`) so multi-GB merge writes cannot evict the serving
803    /// segments' warm pages. Output is byte-identical to the buffered
804    /// writer. Default impl delegates to [`Self::streaming_writer`].
805    async fn streaming_writer_cold(&self, path: &Path) -> io::Result<Box<dyn StreamingWriter>> {
806        self.streaming_writer(path).await
807    }
808}
809
810/// In-memory directory for testing and small indexes
811#[derive(Debug, Default)]
812pub struct RamDirectory {
813    files: Arc<RwLock<HashMap<PathBuf, Arc<Vec<u8>>>>>,
814}
815
816impl Clone for RamDirectory {
817    fn clone(&self) -> Self {
818        Self {
819            files: Arc::clone(&self.files),
820        }
821    }
822}
823
824impl RamDirectory {
825    pub fn new() -> Self {
826        Self::default()
827    }
828
829    /// Synchronous file listing (for serialization).
830    pub fn list_files_sync(&self, prefix: &Path) -> io::Result<Vec<PathBuf>> {
831        let files = self.files.read();
832        Ok(files
833            .keys()
834            .filter(|p| p.starts_with(prefix))
835            .cloned()
836            .collect())
837    }
838
839    /// Synchronous file read (for serialization).
840    pub fn read_file_sync(&self, path: &Path) -> io::Result<Vec<u8>> {
841        let files = self.files.read();
842        files
843            .get(path)
844            .map(|data| data.as_ref().clone())
845            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "File not found"))
846    }
847
848    /// Synchronous file write (for deserialization).
849    pub fn write_sync(&self, path: &Path, data: &[u8]) -> io::Result<()> {
850        self.files
851            .write()
852            .insert(path.to_path_buf(), Arc::new(data.to_vec()));
853        Ok(())
854    }
855}
856
857#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
858#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
859impl Directory for RamDirectory {
860    async fn exists(&self, path: &Path) -> io::Result<bool> {
861        Ok(self.files.read().contains_key(path))
862    }
863
864    async fn file_size(&self, path: &Path) -> io::Result<u64> {
865        self.files
866            .read()
867            .get(path)
868            .map(|data| data.len() as u64)
869            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "File not found"))
870    }
871
872    async fn open_read(&self, path: &Path) -> io::Result<FileHandle> {
873        let files = self.files.read();
874        let data = files
875            .get(path)
876            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "File not found"))?;
877
878        Ok(FileHandle::from_bytes(OwnedBytes::from_arc_vec(
879            Arc::clone(data),
880            0..data.len(),
881        )))
882    }
883
884    async fn read_range(&self, path: &Path, range: Range<u64>) -> io::Result<OwnedBytes> {
885        let files = self.files.read();
886        let data = files
887            .get(path)
888            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "File not found"))?;
889
890        let start = range.start as usize;
891        let end = range.end as usize;
892
893        if end > data.len() {
894            return Err(io::Error::new(
895                io::ErrorKind::InvalidInput,
896                "Range out of bounds",
897            ));
898        }
899
900        Ok(OwnedBytes::from_arc_vec(Arc::clone(data), start..end))
901    }
902
903    async fn list_files(&self, prefix: &Path) -> io::Result<Vec<PathBuf>> {
904        let files = self.files.read();
905        Ok(files
906            .keys()
907            .filter(|p| p.starts_with(prefix))
908            .cloned()
909            .collect())
910    }
911
912    async fn open_lazy(&self, path: &Path) -> io::Result<FileHandle> {
913        // RAM data is always available synchronously — return Inline handle
914        self.open_read(path).await
915    }
916}
917
918#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
919#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
920impl DirectoryWriter for RamDirectory {
921    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
922        self.files
923            .write()
924            .insert(path.to_path_buf(), Arc::new(data.to_vec()));
925        Ok(())
926    }
927
928    async fn delete(&self, path: &Path) -> io::Result<()> {
929        self.files.write().remove(path);
930        Ok(())
931    }
932
933    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
934        let mut files = self.files.write();
935        if let Some(data) = files.remove(from) {
936            files.insert(to.to_path_buf(), data);
937        }
938        Ok(())
939    }
940
941    async fn link(&self, from: &Path, to: &Path) -> io::Result<()> {
942        let mut files = self.files.write();
943        let data = files.get(from).cloned().ok_or_else(|| {
944            io::Error::new(
945                io::ErrorKind::NotFound,
946                format!("source file {from:?} does not exist"),
947            )
948        })?;
949        files.insert(to.to_path_buf(), data);
950        Ok(())
951    }
952
953    async fn sync(&self) -> io::Result<()> {
954        Ok(())
955    }
956
957    async fn streaming_writer(&self, path: &Path) -> io::Result<Box<dyn StreamingWriter>> {
958        Ok(Box::new(BufferedStreamingWriter {
959            path: path.to_path_buf(),
960            buffer: Vec::new(),
961            files: Arc::clone(&self.files),
962        }))
963    }
964}
965
966/// Local filesystem directory with async IO via tokio
967#[cfg(feature = "native")]
968#[derive(Debug, Clone)]
969pub struct FsDirectory {
970    root: PathBuf,
971    label: IndexLabel,
972}
973
974/// Positional exact read that does not move the shared file cursor, so one
975/// `File` can serve concurrent range reads.
976#[cfg(all(feature = "native", unix))]
977fn read_exact_at(file: &std::fs::File, buffer: &mut [u8], offset: u64) -> io::Result<()> {
978    use std::os::unix::fs::FileExt;
979    file.read_exact_at(buffer, offset)
980}
981
982#[cfg(all(feature = "native", windows))]
983fn read_exact_at(file: &std::fs::File, mut buffer: &mut [u8], mut offset: u64) -> io::Result<()> {
984    use std::os::windows::fs::FileExt;
985    while !buffer.is_empty() {
986        match file.seek_read(buffer, offset) {
987            Ok(0) => {
988                return Err(io::Error::new(
989                    io::ErrorKind::UnexpectedEof,
990                    "failed to fill whole buffer",
991                ));
992            }
993            Ok(read) => {
994                buffer = &mut buffer[read..];
995                offset += read as u64;
996            }
997            Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
998            Err(error) => return Err(error),
999        }
1000    }
1001    Ok(())
1002}
1003
1004#[cfg(feature = "native")]
1005impl FsDirectory {
1006    pub fn new(root: impl AsRef<Path>) -> Self {
1007        Self {
1008            root: root.as_ref().to_path_buf(),
1009            label: IndexLabel::default(),
1010        }
1011    }
1012
1013    fn resolve(&self, path: &Path) -> PathBuf {
1014        self.root.join(path)
1015    }
1016}
1017
1018#[cfg(feature = "native")]
1019#[async_trait]
1020impl Directory for FsDirectory {
1021    async fn exists(&self, path: &Path) -> io::Result<bool> {
1022        let full_path = self.resolve(path);
1023        // `try_exists` maps NotFound to Ok(false); any other stat failure
1024        // (EACCES, EIO, ...) must propagate so callers can distinguish a
1025        // genuinely missing file from a transient IO error — swallowing it
1026        // as `false` quarantines a healthy segment as "missing mandatory
1027        // files" instead of retrying.
1028        tokio::fs::try_exists(&full_path).await
1029    }
1030
1031    async fn file_size(&self, path: &Path) -> io::Result<u64> {
1032        let full_path = self.resolve(path);
1033        let metadata = tokio::fs::metadata(&full_path).await?;
1034        Ok(metadata.len())
1035    }
1036
1037    async fn open_read(&self, path: &Path) -> io::Result<FileHandle> {
1038        let full_path = self.resolve(path);
1039        let data = tokio::fs::read(&full_path).await?;
1040        Ok(FileHandle::from_bytes(OwnedBytes::new(data)))
1041    }
1042
1043    async fn read_range(&self, path: &Path, range: Range<u64>) -> io::Result<OwnedBytes> {
1044        use tokio::io::{AsyncReadExt, AsyncSeekExt};
1045
1046        let full_path = self.resolve(path);
1047        let mut file = tokio::fs::File::open(&full_path).await?;
1048
1049        file.seek(std::io::SeekFrom::Start(range.start)).await?;
1050
1051        let len = (range.end - range.start) as usize;
1052        let mut buffer = vec![0u8; len];
1053        file.read_exact(&mut buffer).await?;
1054
1055        Ok(OwnedBytes::new(buffer))
1056    }
1057
1058    async fn list_files(&self, prefix: &Path) -> io::Result<Vec<PathBuf>> {
1059        let full_path = self.resolve(prefix);
1060        let mut entries = tokio::fs::read_dir(&full_path).await?;
1061        let mut files = Vec::new();
1062
1063        while let Some(entry) = entries.next_entry().await? {
1064            if entry.file_type().await?.is_file() {
1065                files.push(entry.path().strip_prefix(&self.root).unwrap().to_path_buf());
1066            }
1067        }
1068
1069        Ok(files)
1070    }
1071
1072    async fn open_lazy(&self, path: &Path) -> io::Result<FileHandle> {
1073        // Open once and keep the descriptor in the handle. Each range read is
1074        // then a single positional read on one blocking thread instead of
1075        // open + seek + read (three `spawn_blocking` hops) per range.
1076        let full_path = self.resolve(path);
1077        let (file, file_size) = tokio::task::spawn_blocking(move || {
1078            let file = std::fs::File::open(&full_path)?;
1079            let file_size = file.metadata()?.len();
1080            Ok::<_, io::Error>((file, file_size))
1081        })
1082        .await
1083        .map_err(io::Error::other)??;
1084        let file = Arc::new(file);
1085
1086        let read_fn: RangeReadFn = Arc::new(move |range: Range<u64>| {
1087            let file = Arc::clone(&file);
1088            Box::pin(async move {
1089                tokio::task::spawn_blocking(move || {
1090                    let len = (range.end - range.start) as usize;
1091                    let mut buffer = vec![0u8; len];
1092                    read_exact_at(&file, &mut buffer, range.start)?;
1093                    Ok(OwnedBytes::new(buffer))
1094                })
1095                .await
1096                .map_err(io::Error::other)?
1097            })
1098        });
1099
1100        Ok(FileHandle::lazy_labeled(
1101            file_size,
1102            read_fn,
1103            self.label.get(),
1104        ))
1105    }
1106
1107    fn set_index_label(&self, label: &str) {
1108        self.label.set(label);
1109    }
1110
1111    fn local_path(&self, path: &Path) -> Option<PathBuf> {
1112        Some(self.resolve(path))
1113    }
1114}
1115
1116#[cfg(feature = "native")]
1117#[async_trait]
1118impl DirectoryWriter for FsDirectory {
1119    async fn write(&self, path: &Path, data: &[u8]) -> io::Result<()> {
1120        let full_path = self.resolve(path);
1121
1122        // Ensure parent directory exists
1123        if let Some(parent) = full_path.parent() {
1124            tokio::fs::create_dir_all(parent).await?;
1125        }
1126
1127        tokio::fs::write(&full_path, data).await
1128    }
1129
1130    async fn delete(&self, path: &Path) -> io::Result<()> {
1131        let full_path = self.resolve(path);
1132        tokio::fs::remove_file(&full_path).await
1133    }
1134
1135    async fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
1136        let from_path = self.resolve(from);
1137        let to_path = self.resolve(to);
1138        // Metadata publication is the only rename user. Keep the atomic
1139        // filesystem operation in a single future poll: tokio::fs::rename is
1140        // backed by a cancellable await around spawn_blocking, so a dropped
1141        // commit future could observe neither completion nor failure even
1142        // though the rename later succeeded. The caller must update its
1143        // in-memory metadata in the same poll after this returns.
1144        std::fs::rename(&from_path, &to_path)
1145    }
1146
1147    async fn link(&self, from: &Path, to: &Path) -> io::Result<()> {
1148        std::fs::hard_link(self.resolve(from), self.resolve(to))
1149    }
1150
1151    async fn sync(&self) -> io::Result<()> {
1152        // fsync the directory
1153        let dir = std::fs::File::open(&self.root)?;
1154        dir.sync_all()?;
1155        Ok(())
1156    }
1157
1158    async fn streaming_writer(&self, path: &Path) -> io::Result<Box<dyn StreamingWriter>> {
1159        let full_path = self.resolve(path);
1160        if let Some(parent) = full_path.parent() {
1161            tokio::fs::create_dir_all(parent).await?;
1162        }
1163        let file = std::fs::File::create(&full_path)?;
1164        Ok(Box::new(FileStreamingWriter::new(file)))
1165    }
1166
1167    async fn streaming_writer_cold(&self, path: &Path) -> io::Result<Box<dyn StreamingWriter>> {
1168        let full_path = self.resolve(path);
1169        if let Some(parent) = full_path.parent() {
1170            tokio::fs::create_dir_all(parent).await?;
1171        }
1172        let file = std::fs::File::create(&full_path)?;
1173        Ok(Box::new(super::ColdStreamingWriter::new(
1174            file,
1175            self.label.get(),
1176        )))
1177    }
1178}
1179
1180/// Caching wrapper for any Directory - caches file reads
1181pub struct CachingDirectory<D: Directory> {
1182    inner: D,
1183    cache: RwLock<HashMap<PathBuf, Arc<Vec<u8>>>>,
1184    max_cached_bytes: usize,
1185    current_bytes: RwLock<usize>,
1186}
1187
1188impl<D: Directory> CachingDirectory<D> {
1189    pub fn new(inner: D, max_cached_bytes: usize) -> Self {
1190        Self {
1191            inner,
1192            cache: RwLock::new(HashMap::new()),
1193            max_cached_bytes,
1194            current_bytes: RwLock::new(0),
1195        }
1196    }
1197
1198    fn try_cache(&self, path: &Path, data: &[u8]) {
1199        let mut current = self.current_bytes.write();
1200        if *current + data.len() <= self.max_cached_bytes {
1201            self.cache
1202                .write()
1203                .insert(path.to_path_buf(), Arc::new(data.to_vec()));
1204            *current += data.len();
1205        }
1206    }
1207}
1208
1209#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
1210#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
1211impl<D: Directory> Directory for CachingDirectory<D> {
1212    async fn exists(&self, path: &Path) -> io::Result<bool> {
1213        if self.cache.read().contains_key(path) {
1214            return Ok(true);
1215        }
1216        self.inner.exists(path).await
1217    }
1218
1219    async fn file_size(&self, path: &Path) -> io::Result<u64> {
1220        if let Some(data) = self.cache.read().get(path) {
1221            return Ok(data.len() as u64);
1222        }
1223        self.inner.file_size(path).await
1224    }
1225
1226    async fn open_read(&self, path: &Path) -> io::Result<FileHandle> {
1227        // Check cache first
1228        if let Some(data) = self.cache.read().get(path) {
1229            return Ok(FileHandle::from_bytes(OwnedBytes::from_arc_vec(
1230                Arc::clone(data),
1231                0..data.len(),
1232            )));
1233        }
1234
1235        // Read from inner and potentially cache
1236        let handle = self.inner.open_read(path).await?;
1237        let bytes = handle.read_bytes().await?;
1238
1239        self.try_cache(path, bytes.as_slice());
1240
1241        Ok(FileHandle::from_bytes(bytes))
1242    }
1243
1244    async fn read_range(&self, path: &Path, range: Range<u64>) -> io::Result<OwnedBytes> {
1245        // Check cache first
1246        if let Some(data) = self.cache.read().get(path) {
1247            let start = range.start as usize;
1248            let end = range.end as usize;
1249            return Ok(OwnedBytes::from_arc_vec(Arc::clone(data), start..end));
1250        }
1251
1252        self.inner.read_range(path, range).await
1253    }
1254
1255    async fn list_files(&self, prefix: &Path) -> io::Result<Vec<PathBuf>> {
1256        self.inner.list_files(prefix).await
1257    }
1258
1259    async fn open_lazy(&self, path: &Path) -> io::Result<FileHandle> {
1260        // For caching directory, delegate to inner - caching happens at read_range level
1261        self.inner.open_lazy(path).await
1262    }
1263
1264    fn set_index_label(&self, label: &str) {
1265        self.inner.set_index_label(label);
1266    }
1267
1268    fn local_path(&self, path: &Path) -> Option<PathBuf> {
1269        self.inner.local_path(path)
1270    }
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275    use super::*;
1276
1277    #[tokio::test]
1278    async fn test_ram_directory() {
1279        let dir = RamDirectory::new();
1280
1281        // Write file
1282        dir.write(Path::new("test.txt"), b"hello world")
1283            .await
1284            .unwrap();
1285
1286        // Check exists
1287        assert!(dir.exists(Path::new("test.txt")).await.unwrap());
1288        assert!(!dir.exists(Path::new("nonexistent.txt")).await.unwrap());
1289
1290        // Read file
1291        let slice = dir.open_read(Path::new("test.txt")).await.unwrap();
1292        let data = slice.read_bytes().await.unwrap();
1293        assert_eq!(data.as_slice(), b"hello world");
1294
1295        // Read range
1296        let range_data = dir.read_range(Path::new("test.txt"), 0..5).await.unwrap();
1297        assert_eq!(range_data.as_slice(), b"hello");
1298
1299        // Delete
1300        dir.delete(Path::new("test.txt")).await.unwrap();
1301        assert!(!dir.exists(Path::new("test.txt")).await.unwrap());
1302    }
1303
1304    /// A transient stat failure (EACCES here, EIO on flaky storage) must
1305    /// surface as `Err`, not `Ok(false)`: callers classify a missing
1306    /// mandatory segment file as deterministic corruption and quarantine
1307    /// the segment until restart.
1308    #[cfg(all(unix, feature = "native"))]
1309    #[tokio::test]
1310    async fn test_fs_exists_propagates_stat_errors_instead_of_reporting_missing() {
1311        use std::os::unix::fs::PermissionsExt;
1312
1313        let temp_dir = tempfile::TempDir::new().unwrap();
1314        let dir = FsDirectory::new(temp_dir.path());
1315        dir.write(Path::new("locked/seg.meta"), b"data")
1316            .await
1317            .unwrap();
1318
1319        // Removing search permission from the parent makes stat on the child
1320        // fail with EACCES while the file itself still exists.
1321        let locked = temp_dir.path().join("locked");
1322        let original = std::fs::metadata(&locked).unwrap().permissions();
1323        std::fs::set_permissions(&locked, std::fs::Permissions::from_mode(0o000)).unwrap();
1324        if std::fs::metadata(locked.join("seg.meta")).is_ok() {
1325            // Running as root: directory permissions are not enforced, so the
1326            // stat failure cannot be provoked.
1327            std::fs::set_permissions(&locked, original).unwrap();
1328            return;
1329        }
1330        let result = dir.exists(Path::new("locked/seg.meta")).await;
1331        std::fs::set_permissions(&locked, original).unwrap();
1332
1333        let error =
1334            result.expect_err("stat failure must propagate as Err, not be misreported as missing");
1335        assert_ne!(error.kind(), io::ErrorKind::NotFound);
1336        // Once stat succeeds again the file is reported present.
1337        assert!(dir.exists(Path::new("locked/seg.meta")).await.unwrap());
1338    }
1339
1340    #[tokio::test]
1341    async fn test_file_handle() {
1342        let data = OwnedBytes::new(b"hello world".to_vec());
1343        let handle = FileHandle::from_bytes(data);
1344
1345        assert_eq!(handle.len(), 11);
1346        assert!(handle.is_sync());
1347
1348        let sub = handle.slice(0..5);
1349        let bytes = sub.read_bytes().await.unwrap();
1350        assert_eq!(bytes.as_slice(), b"hello");
1351
1352        let sub2 = handle.slice(6..11);
1353        let bytes2 = sub2.read_bytes().await.unwrap();
1354        assert_eq!(bytes2.as_slice(), b"world");
1355
1356        // Sync reads work on inline handles
1357        let sync_bytes = handle.read_bytes_range_sync(0..5).unwrap();
1358        assert_eq!(sync_bytes.as_slice(), b"hello");
1359    }
1360
1361    #[tokio::test]
1362    async fn test_owned_bytes() {
1363        let bytes = OwnedBytes::new(vec![1, 2, 3, 4, 5]);
1364
1365        assert_eq!(bytes.len(), 5);
1366        assert_eq!(bytes.as_slice(), &[1, 2, 3, 4, 5]);
1367
1368        let sliced = bytes.slice(1..4);
1369        assert_eq!(sliced.as_slice(), &[2, 3, 4]);
1370
1371        // Original unchanged
1372        assert_eq!(bytes.as_slice(), &[1, 2, 3, 4, 5]);
1373    }
1374}