Skip to main content

slate_kv/
file_flash.rs

1use crate::io_util::{read_exact_at, write_all_at};
2use slate_kv_hal::Flash;
3use std::fs::File;
4#[cfg(target_os = "macos")]
5use std::os::unix::io::AsRawFd;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Durability {
9    Full,
10    OsCache,
11}
12
13#[cfg(target_os = "macos")]
14fn flush_durable(f: &File, durability: Durability) -> std::io::Result<()> {
15    match durability {
16        Durability::Full => {
17            // SAFETY-equivalent of §2.3(2) ordering on Darwin: fsync alone may leave
18            // writes in the drive cache; F_FULLFSYNC forces them to stable media.
19            let rc = unsafe { libc::fcntl(f.as_raw_fd(), libc::F_FULLFSYNC) };
20            if rc == -1 {
21                Err(std::io::Error::last_os_error())
22            } else {
23                Ok(())
24            }
25        }
26        Durability::OsCache => f.sync_data(),
27    }
28}
29
30#[cfg(not(target_os = "macos"))]
31fn flush_durable(f: &File, _durability: Durability) -> std::io::Result<()> {
32    f.sync_data()
33}
34
35#[derive(Debug)]
36pub enum FileFlashError {
37    Io(std::io::Error),
38    Unaligned,
39    OutOfBounds,
40    ProgramWithoutErase,
41}
42
43impl From<std::io::Error> for FileFlashError {
44    fn from(err: std::io::Error) -> Self {
45        FileFlashError::Io(err)
46    }
47}
48
49/// Device-level I/O tallies for one [`FileFlash`].
50///
51/// Counted at the HAL boundary rather than derived from the engine's own
52/// `Metrics`: `Metrics` records only bytes the engine *programs*, so it cannot
53/// say anything about read cost, which is what mount is made of. `read_pages` is
54/// the number of distinct `page_size` pages a read touched, so a 28-byte header
55/// read that straddles a page boundary counts as two — that is what the device
56/// actually fetches.
57#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
58pub struct FlashCounters {
59    pub read_ops: u64,
60    pub read_bytes: u64,
61    pub read_pages: u64,
62    pub program_ops: u64,
63    pub program_bytes: u64,
64    pub erase_ops: u64,
65}
66
67pub struct FileFlash {
68    file: File,
69    capacity: u32,
70    page_size: usize,
71    block_size: usize,
72    durability: Durability,
73    counters: FlashCounters,
74}
75
76impl FileFlash {
77    pub fn new(
78        file: File,
79        capacity: u32,
80        page_size: usize,
81        block_size: usize,
82        durability: Durability,
83    ) -> Result<Self, std::io::Error> {
84        let meta = file.metadata()?;
85        if meta.len() != capacity as u64 {
86            file.set_len(capacity as u64)?;
87            // Fill with 0xFF if new
88            let ff = vec![0xFF; capacity as usize];
89            write_all_at(&file, &ff, 0)?;
90            flush_durable(&file, durability)?;
91        }
92        Ok(Self {
93            file,
94            capacity,
95            page_size,
96            block_size,
97            durability,
98            counters: FlashCounters::default(),
99        })
100    }
101
102    /// Device I/O tallies accumulated since this handle was created.
103    pub fn counters(&self) -> FlashCounters {
104        self.counters
105    }
106
107    /// Pages spanned by a `len`-byte access starting at `addr`.
108    fn pages_spanned(&self, addr: usize, len: usize) -> u64 {
109        if len == 0 {
110            return 0;
111        }
112        let first = addr / self.page_size;
113        let last = (addr + len - 1) / self.page_size;
114        (last - first + 1) as u64
115    }
116}
117
118impl Flash for FileFlash {
119    type Error = FileFlashError;
120
121    fn page_size(&self) -> usize {
122        self.page_size
123    }
124
125    fn block_size(&self) -> usize {
126        self.block_size
127    }
128
129    fn capacity(&self) -> u32 {
130        self.capacity
131    }
132
133    fn read(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), Self::Error> {
134        let addr = addr as usize;
135        if addr + buf.len() > self.capacity as usize {
136            return Err(FileFlashError::OutOfBounds);
137        }
138        read_exact_at(&self.file, buf, addr as u64)?;
139        self.counters.read_ops += 1;
140        self.counters.read_bytes += buf.len() as u64;
141        self.counters.read_pages += self.pages_spanned(addr, buf.len());
142        Ok(())
143    }
144
145    fn program(&mut self, addr: u32, buf: &[u8]) -> Result<(), Self::Error> {
146        let addr = addr as usize;
147        if addr % self.page_size != 0 || buf.len() % self.page_size != 0 {
148            return Err(FileFlashError::Unaligned);
149        }
150        if addr + buf.len() > self.capacity as usize {
151            return Err(FileFlashError::OutOfBounds);
152        }
153
154        // Verify erased
155        let mut check_buf = vec![0u8; buf.len()];
156        read_exact_at(&self.file, &mut check_buf, addr as u64)?;
157        if !check_buf.iter().all(|&b| b == 0xFF) {
158            return Err(FileFlashError::ProgramWithoutErase);
159        }
160
161        // Write and sync
162        write_all_at(&self.file, buf, addr as u64)?;
163        flush_durable(&self.file, self.durability)?;
164        self.counters.program_ops += 1;
165        self.counters.program_bytes += buf.len() as u64;
166        Ok(())
167    }
168
169    fn erase(&mut self, block_addr: u32) -> Result<(), Self::Error> {
170        let block_addr = block_addr as usize;
171        if block_addr % self.block_size != 0 {
172            return Err(FileFlashError::Unaligned);
173        }
174        if block_addr + self.block_size > self.capacity as usize {
175            return Err(FileFlashError::OutOfBounds);
176        }
177
178        let ff = vec![0xFF; self.block_size];
179        write_all_at(&self.file, &ff, block_addr as u64)?;
180        flush_durable(&self.file, self.durability)?;
181        self.counters.erase_ops += 1;
182        Ok(())
183    }
184}