Skip to main content

git_zero_rustix/
cache.rs

1//! Synchronous selected-pack cache in a caller-owned directory and byte buffer.
2//!
3//! Authorize and select before every lookup using `cache::selected::write`.
4//! Choose a private directory for each authorization namespace. Published files
5//! are replaced atomically, never modified in place. Directory durability and
6//! distributed producer coordination belong to the host.
7
8use core::ffi::CStr;
9use git_zero::{
10    Error, Result,
11    cache::selected::{Key, PackCache, Statistics},
12};
13use rustix::{
14    fd::{AsFd, BorrowedFd},
15    fs::{self, AtFlags, Mode, OFlags},
16    io::{self, Errno},
17};
18
19#[cfg(not(any(
20    target_os = "espidf",
21    target_os = "horizon",
22    target_os = "solaris",
23    target_os = "vita",
24    target_os = "wasi"
25)))]
26pub mod coordination;
27
28/// One bounded resident buffer backed by immutable cache files. Each `get`
29/// reloads its file so eviction and corrupt entries remain visible. The core
30/// selected writer validates every returned pack before output. Oversized files
31/// fail before reading into the buffer; no pack-sized allocation or mapping is
32/// performed here. A host may open independent instances for concurrent readers.
33pub struct DiskCache<'directory, 'buffer> {
34    directory: BorrowedFd<'directory>,
35    buffer: &'buffer mut [u8],
36    nonce: [u8; 16],
37    sequence: u64,
38    enabled: bool,
39    statistics: Statistics,
40}
41
42impl<'directory, 'buffer> DiskCache<'directory, 'buffer> {
43    /// Supply a trusted directory descriptor, fixed resident bytes, and a
44    /// host-chosen nonce unique among concurrent producers in that directory.
45    /// Exclusive staging-file creation safely rejects nonce/name collisions.
46    pub fn new(
47        directory: BorrowedFd<'directory>,
48        buffer: &'buffer mut [u8],
49        nonce: [u8; 16],
50    ) -> Self {
51        Self {
52            directory,
53            buffer,
54            nonce,
55            sequence: 0,
56            enabled: true,
57            statistics: Statistics::default(),
58        }
59    }
60
61    pub fn set_enabled(&mut self, enabled: bool) {
62        self.enabled = enabled;
63    }
64    pub fn statistics(&self) -> Statistics {
65        self.statistics
66    }
67
68    /// Unlink only this exact key. Readers that already copied a verified pack
69    /// remain independent. Missing entries return false. No directory scan or
70    /// automatic eviction of other authorization namespaces is performed.
71    pub fn evict(&mut self, key: Key) -> Result<bool> {
72        let name = key_name(key);
73        match fs::unlinkat(self.directory, cstr(&name), AtFlags::empty()) {
74            Ok(()) => {
75                self.statistics.evictions = self.statistics.evictions.saturating_add(1);
76                Ok(true)
77            }
78            Err(Errno::NOENT) => Ok(false),
79            Err(_) => Err(Error::Backend),
80        }
81    }
82
83    fn publish(&mut self, key: Key, bytes: &[u8]) -> Result<()> {
84        // Check every recoverable condition before publication. In particular,
85        // report directory durability separately: a failed post-rename fsync
86        // could not promise preservation of the previous cache entry.
87        if bytes.len() > self.buffer.len() {
88            return Err(Error::BufferTooSmall);
89        }
90        let sequence = self.sequence;
91        self.sequence = sequence.checked_add(1).ok_or(Error::LimitExceeded)?;
92        let name = temporary_name(self.nonce, sequence);
93        let file = fs::openat(
94            self.directory,
95            cstr(&name),
96            OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
97            Mode::RUSR | Mode::WUSR,
98        )
99        .map_err(|_| Error::Backend)?;
100        let mut staging = Staging {
101            directory: self.directory,
102            name: &name,
103            armed: true,
104        };
105        write_file(&file, bytes)?;
106        fs::fsync(&file).map_err(|_| Error::Backend)?;
107        let target = key_name(key);
108        // Last fallible publication operation: failed rename preserves the old
109        // target. Successful rename exposes a complete already-synced file.
110        fs::renameat(self.directory, cstr(&name), self.directory, cstr(&target))
111            .map_err(|_| Error::Backend)?;
112        staging.armed = false;
113        Ok(())
114    }
115}
116
117impl PackCache for DiskCache<'_, '_> {
118    fn get(&mut self, key: Key) -> Result<Option<&[u8]>> {
119        if !self.enabled {
120            self.statistics.misses = self.statistics.misses.saturating_add(1);
121            return Ok(None);
122        }
123        let name = key_name(key);
124        let file = match fs::openat(
125            self.directory,
126            cstr(&name),
127            OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
128            Mode::empty(),
129        ) {
130            Ok(file) => file,
131            Err(Errno::NOENT) => {
132                self.statistics.misses = self.statistics.misses.saturating_add(1);
133                return Ok(None);
134            }
135            Err(_) => return Err(Error::Backend),
136        };
137        let length = crate::storage::file_size(&file)?;
138        let length = usize::try_from(length).map_err(|_| Error::LimitExceeded)?;
139        let bytes = self.buffer.get_mut(..length).ok_or(Error::BufferTooSmall)?;
140        read_file(&file, bytes)?;
141        self.statistics.hits = self.statistics.hits.saturating_add(1);
142        Ok(Some(bytes))
143    }
144
145    fn put(&mut self, key: Key, bytes: &[u8]) -> Result<()> {
146        if self.enabled {
147            self.publish(key, bytes)?;
148            self.statistics.insertions = self.statistics.insertions.saturating_add(1);
149        }
150        Ok(())
151    }
152}
153
154fn read_file(file: &impl AsFd, bytes: &mut [u8]) -> Result<()> {
155    let mut offset = 0;
156    while offset < bytes.len() {
157        match io::pread(file, &mut bytes[offset..], offset as u64) {
158            Ok(0) => return Err(Error::CorruptPack),
159            Ok(count) => offset += count,
160            Err(Errno::INTR) => continue,
161            Err(_) => return Err(Error::Backend),
162        }
163    }
164    // Catch a file that grew while being copied. Afterwards the private buffer
165    // is immutable for the core verifier regardless of later file replacement.
166    loop {
167        match io::pread(file, &mut [0], offset as u64) {
168            Ok(0) => return Ok(()),
169            Ok(_) => return Err(Error::CorruptPack),
170            Err(Errno::INTR) => continue,
171            Err(_) => return Err(Error::Backend),
172        }
173    }
174}
175
176fn write_file(file: &impl AsFd, mut bytes: &[u8]) -> Result<()> {
177    while !bytes.is_empty() {
178        match io::write(file, bytes) {
179            Ok(0) => return Err(Error::Backend),
180            Ok(count) => bytes = &bytes[count..],
181            Err(Errno::INTR) => continue,
182            Err(_) => return Err(Error::Backend),
183        }
184    }
185    Ok(())
186}
187
188struct Staging<'directory, 'name> {
189    directory: BorrowedFd<'directory>,
190    name: &'name [u8; 59],
191    armed: bool,
192}
193impl Drop for Staging<'_, '_> {
194    fn drop(&mut self) {
195        // Before rename this removes the incomplete private file. Afterwards
196        // the staging name is absent. Never unlink another producer's file.
197        if self.armed {
198            let _ = fs::unlinkat(self.directory, cstr(self.name), AtFlags::empty());
199        }
200    }
201}
202
203fn key_name(key: Key) -> [u8; 102] {
204    let mut name = [0; 102];
205    hexadecimal(key.as_bytes(), &mut name[..96]);
206    name[96..101].copy_from_slice(b".pack");
207    name
208}
209fn temporary_name(nonce: [u8; 16], sequence: u64) -> [u8; 59] {
210    let mut name = [0; 59];
211    name[..5].copy_from_slice(b".tmp-");
212    hexadecimal(&nonce, &mut name[5..37]);
213    hexadecimal(&sequence.to_be_bytes(), &mut name[37..53]);
214    name[53..58].copy_from_slice(b".pack");
215    name
216}
217fn hexadecimal(bytes: &[u8], output: &mut [u8]) {
218    const HEX: &[u8; 16] = b"0123456789abcdef";
219    for (byte, pair) in bytes.iter().zip(output.chunks_exact_mut(2)) {
220        pair[0] = HEX[(byte >> 4) as usize];
221        pair[1] = HEX[(byte & 15) as usize];
222    }
223}
224fn cstr(bytes: &[u8]) -> &CStr {
225    CStr::from_bytes_with_nul(bytes).unwrap()
226}