metrale_storage/tier/
pio.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Positional file reads and writes at an explicit offset:
4//! `FileExt::read_at`/`write_at` on unix, `seek_read`/`seek_write` on Windows.
5//! Bounds checks and record layout stay with the callers (`backend/posix.rs`,
6//! `expert_pack_fs.rs`, `expert_tier.rs`, `ngram_cache_fault.rs`).
7//!
8//! Owner: storage (tier).
9//! Invariants: a transfer shorter than requested is retried from where it
10//! stopped; a zero-byte transfer ends the loop.
11
12use std::fs::File;
13use std::io;
14
15/// 2026-09-25: Write all of `buf` at `offset`. A write of zero bytes is a
16/// `WriteZero` error.
17pub fn write_all_at(f: &File, buf: &[u8], offset: u64) -> io::Result<()> {
18    let (mut off, mut done) = (offset, 0usize);
19    while done < buf.len() {
20        let n = write_at(f, &buf[done..], off)?;
21        if n == 0 {
22            return Err(io::Error::new(
23                io::ErrorKind::WriteZero,
24                format!("positional write returned 0 bytes at offset {off}"),
25            ));
26        }
27        done += n;
28        off += n as u64;
29    }
30    Ok(())
31}
32
33/// 2026-09-25: Fill `buf` from `offset`. A zero-byte read before `buf` is full
34/// is an `UnexpectedEof` error, so the tail of `buf` is never left stale.
35pub fn read_exact_at(f: &File, buf: &mut [u8], offset: u64) -> io::Result<()> {
36    let (mut off, mut done) = (offset, 0usize);
37    let total = buf.len();
38    while done < total {
39        let n = read_at(f, &mut buf[done..], off)?;
40        if n == 0 {
41            return Err(io::Error::new(
42                io::ErrorKind::UnexpectedEof,
43                format!("positional read hit EOF after {done} of {total} bytes at offset {off}"),
44            ));
45        }
46        done += n;
47        off += n as u64;
48    }
49    Ok(())
50}
51
52/// 2026-09-25: Fill as much of `buf` as the file holds from `offset`, and
53/// succeed if at least the first `needed` bytes arrived. Returns the byte count
54/// read. Fewer than `needed` is an `UnexpectedEof` error.
55///
56/// For whole-block reads at the end of a file (`ngram_cache_fault.rs`): a
57/// safetensors file is `8 + header + tensors` long and nothing pads it to a
58/// block, so the block holding its last rows runs past EOF, where
59/// [`read_exact_at`] would fail although every requested row is in the file.
60pub fn read_at_least_at(f: &File, buf: &mut [u8], offset: u64, needed: usize) -> io::Result<usize> {
61    debug_assert!(needed <= buf.len());
62    let (mut off, mut done) = (offset, 0usize);
63    while done < buf.len() {
64        let n = read_at(f, &mut buf[done..], off)?;
65        if n == 0 {
66            break;
67        }
68        done += n;
69        off += n as u64;
70    }
71    if done < needed {
72        return Err(io::Error::new(
73            io::ErrorKind::UnexpectedEof,
74            format!(
75                "positional read got {done} bytes at offset {offset}, needed {needed} \
76                 — the file is shorter than the data it is supposed to hold"
77            ),
78        ));
79    }
80    Ok(done)
81}
82
83#[cfg(unix)]
84fn write_at(f: &File, buf: &[u8], offset: u64) -> io::Result<usize> {
85    use std::os::unix::fs::FileExt;
86    f.write_at(buf, offset)
87}
88
89#[cfg(unix)]
90fn read_at(f: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
91    use std::os::unix::fs::FileExt;
92    f.read_at(buf, offset)
93}
94
95#[cfg(windows)]
96fn write_at(f: &File, buf: &[u8], offset: u64) -> io::Result<usize> {
97    use std::os::windows::fs::FileExt;
98    f.seek_write(buf, offset)
99}
100
101#[cfg(windows)]
102fn read_at(f: &File, buf: &mut [u8], offset: u64) -> io::Result<usize> {
103    use std::os::windows::fs::FileExt;
104    f.seek_read(buf, offset)
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    /// 2026-09-25: A file whose length is not a multiple of the block size,
112    /// read in whole blocks: `read_at_least_at` succeeds when the needed bytes
113    /// exist and fails when they do not, and `read_exact_at` fails.
114    #[test]
115    fn a_block_read_past_eof_succeeds_for_the_bytes_that_exist() {
116        let dir = std::env::temp_dir().join(format!("metrale_pio_tail_{}", std::process::id()));
117        std::fs::create_dir_all(&dir).unwrap();
118        let path = dir.join("tail.bin");
119        let len = 4096usize + 288;
120        std::fs::write(&path, vec![7u8; len]).unwrap();
121        let f = std::fs::File::open(&path).unwrap();
122
123        let mut buf = vec![0u8; 8192];
124        let n = read_at_least_at(&f, &mut buf, 4096, 288).unwrap();
125        assert_eq!(n, 288, "should read to EOF and no further");
126        assert!(
127            buf[..288].iter().all(|&b| b == 7),
128            "the row's bytes must arrive"
129        );
130
131        let e = read_at_least_at(&f, &mut buf, 4096, 289).unwrap_err();
132        assert_eq!(e.kind(), io::ErrorKind::UnexpectedEof);
133
134        assert!(read_exact_at(&f, &mut buf[..8192], 4096).is_err());
135
136        drop(f);
137        let _ = std::fs::remove_dir_all(&dir);
138    }
139
140    // 2026-09-25: Writes and reads at non-zero offsets, and checks after each
141    // call that the file cursor is still where `seek` put it.
142    #[test]
143    fn round_trip_at_offset() {
144        use std::io::{Seek, SeekFrom};
145
146        let dir = std::env::temp_dir().join(format!("metrale_pio_{}", std::process::id()));
147        std::fs::create_dir_all(&dir).unwrap();
148        let path = dir.join("pio.bin");
149        let mut f = std::fs::OpenOptions::new()
150            .read(true)
151            .write(true)
152            .create(true)
153            .truncate(true)
154            .open(&path)
155            .unwrap();
156        f.seek(SeekFrom::Start(137)).unwrap();
157
158        write_all_at(&f, &[0u8; 4096], 0).unwrap();
159        assert_eq!(f.stream_position().unwrap(), 137);
160        let payload: Vec<u8> = (0..1024u32).map(|i| (i % 251) as u8).collect();
161        write_all_at(&f, &payload, 2048).unwrap();
162        assert_eq!(f.stream_position().unwrap(), 137);
163
164        let mut out = vec![0u8; payload.len()];
165        read_exact_at(&f, &mut out, 2048).unwrap();
166        assert_eq!(out, payload);
167        assert_eq!(f.stream_position().unwrap(), 137);
168
169        let mut past = vec![0u8; 8192];
170        assert!(read_exact_at(&f, &mut past, 4096).is_err());
171        assert_eq!(f.stream_position().unwrap(), 137);
172
173        drop(f);
174        let _ = std::fs::remove_dir_all(&dir);
175    }
176}