1use 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
28pub 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 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 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 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 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 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 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}