1use std::io::{self, IoSlice, Read, Seek, SeekFrom, Write};
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12
13use chacha20poly1305::{
14 aead::{AeadInPlace, KeyInit},
15 XChaCha20Poly1305, XNonce,
16};
17use parking_lot::Mutex;
18
19const BLOCK_BYTES: usize = 16 * 1024;
20const NONCE_BYTES: usize = 24;
21const LENGTH_BYTES: usize = 2;
22const TAG_BYTES: usize = 16;
23const SLOT_HEADER_BYTES: usize = NONCE_BYTES + LENGTH_BYTES + TAG_BYTES;
24#[cfg(test)]
25const RECORD_BYTES: usize = 1 + 2 * (SLOT_HEADER_BYTES + BLOCK_BYTES);
26
27pub type TemporaryFile = BlockTemporaryFile<BLOCK_BYTES>;
29
30pub struct BlockTemporaryFile<const BYTES: usize> {
32 owner: Arc<Mutex<Owner<BYTES>>>,
33 path: Arc<PathBuf>,
34 position: u64,
35}
36
37struct Owner<const BYTES: usize> {
38 file: tempfile::NamedTempFile,
39 cipher: XChaCha20Poly1305,
40 length: u64,
41 #[cfg(test)]
42 faults: tests::Faults,
43}
44
45pub struct TemporaryFileMetadata {
46 length: u64,
47}
48
49impl TemporaryFileMetadata {
50 pub fn len(&self) -> u64 {
51 self.length
52 }
53
54 pub fn is_empty(&self) -> bool {
55 self.length == 0
56 }
57}
58
59impl<const BYTES: usize> BlockTemporaryFile<BYTES> {
60 pub fn new() -> io::Result<Self> {
61 Self::from_file(tempfile::NamedTempFile::new()?)
62 }
63
64 pub fn new_in(directory: impl AsRef<Path>) -> io::Result<Self> {
65 Self::from_file(tempfile::NamedTempFile::new_in(directory)?)
66 }
67
68 fn from_file(file: tempfile::NamedTempFile) -> io::Result<Self> {
69 if BYTES == 0 || BYTES > BLOCK_BYTES {
70 return Err(io::Error::new(
71 io::ErrorKind::InvalidInput,
72 "invalid temporary file block width",
73 ));
74 }
75 let mut key = [0_u8; 32];
76 getrandom::fill(&mut key).map_err(|error| io::Error::other(error.to_string()))?;
77 let cipher = XChaCha20Poly1305::new((&key).into());
78 key.fill(0);
79 let path = Arc::new(file.path().to_path_buf());
80 Ok(Self {
81 owner: Arc::new(Mutex::new(Owner {
82 file,
83 cipher,
84 length: 0,
85 #[cfg(test)]
86 faults: tests::Faults::default(),
87 })),
88 path,
89 position: 0,
90 })
91 }
92
93 pub fn path(&self) -> &Path {
94 &self.path
95 }
96
97 pub fn as_file(&self) -> &Self {
98 self
99 }
100
101 pub fn as_file_mut(&mut self) -> &mut Self {
102 self
103 }
104
105 pub fn write_all_vectored(&mut self, mut input: &mut [IoSlice<'_>]) -> io::Result<()> {
107 IoSlice::advance_slices(&mut input, 0);
108 while !input.is_empty() {
109 match self.write_vectored(input) {
110 Ok(0) => {
111 return Err(io::Error::new(
112 io::ErrorKind::WriteZero,
113 "incomplete temporary file write",
114 ));
115 }
116 Ok(written) => IoSlice::advance_slices(&mut input, written),
117 Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
118 Err(error) => return Err(error),
119 }
120 }
121 Ok(())
122 }
123
124 pub fn reopen(&self) -> io::Result<Self> {
125 self.owner.lock().validate_length()?;
126 Ok(Self {
127 owner: Arc::clone(&self.owner),
128 path: Arc::clone(&self.path),
129 position: 0,
130 })
131 }
132
133 pub fn metadata(&self) -> io::Result<TemporaryFileMetadata> {
134 Ok(TemporaryFileMetadata {
135 length: self.owner.lock().length,
136 })
137 }
138
139 pub fn set_len(&self, length: u64) -> io::Result<()> {
140 record_offset::<BYTES>(length.div_ceil(BYTES as u64))?;
141 let mut owner = self.owner.lock();
142 if length >= owner.length {
143 return owner.extend_zeroed(length);
144 }
145 owner.truncate_physical(record_offset::<BYTES>(length.div_ceil(BYTES as u64))?)?;
147 owner.length = length;
148 Ok(())
149 }
150}
151
152fn record_offset<const BYTES: usize>(block: u64) -> io::Result<u64> {
153 block
154 .checked_mul((1 + 2 * (SLOT_HEADER_BYTES + BYTES)) as u64)
155 .ok_or_else(|| {
156 io::Error::new(
157 io::ErrorKind::InvalidInput,
158 "temporary file offset overflow",
159 )
160 })
161}
162
163fn block_aad(block: u64, length: u16) -> [u8; 8 + LENGTH_BYTES] {
164 let mut aad = [0; 8 + LENGTH_BYTES];
165 aad[..8].copy_from_slice(&block.to_le_bytes());
166 aad[8..].copy_from_slice(&length.to_le_bytes());
167 aad
168}
169
170impl<const BYTES: usize> Owner<BYTES> {
171 fn validate_length(&self) -> io::Result<()> {
172 let expected = record_offset::<BYTES>(self.length.div_ceil(BYTES as u64))?;
173 if self.file.as_file().metadata()?.len() != expected {
174 return Err(io::Error::new(
175 io::ErrorKind::InvalidData,
176 "invalid authenticated temporary file length",
177 ));
178 }
179 Ok(())
180 }
181
182 fn active_slot(&mut self, block: u64) -> io::Result<u8> {
183 let file = self.file.as_file_mut();
184 file.seek(SeekFrom::Start(record_offset::<BYTES>(block)?))?;
185 let mut active = [0];
186 file.read_exact(&mut active)?;
187 if active[0] > 1 {
188 return Err(io::Error::new(
189 io::ErrorKind::InvalidData,
190 "invalid temporary file active slot",
191 ));
192 }
193 Ok(active[0])
194 }
195
196 fn read_block(&mut self, block: u64) -> io::Result<[u8; BYTES]> {
197 let mut plaintext = [0_u8; BYTES];
198 if block < self.length.div_ceil(BYTES as u64) {
199 #[cfg(test)]
200 {
201 self.faults.read_blocks += 1;
202 }
203 let active = self.active_slot(block)?;
204 let file = self.file.as_file_mut();
205 file.seek(SeekFrom::Start(slot_offset::<BYTES>(block, active)?))?;
206 let mut nonce = [0_u8; NONCE_BYTES];
207 let mut length = [0_u8; LENGTH_BYTES];
208 let mut tag = [0_u8; TAG_BYTES];
209 file.read_exact(&mut nonce)?;
210 file.read_exact(&mut length)?;
211 let length = u16::from_le_bytes(length);
212 let populated = usize::from(length);
213 let required = (self.length - block * BYTES as u64).min(BYTES as u64) as usize;
214 if populated > BYTES || populated < required {
215 return Err(io::Error::new(
216 io::ErrorKind::InvalidData,
217 "invalid authenticated temporary block length",
218 ));
219 }
220 file.read_exact(&mut tag)?;
221 file.read_exact(&mut plaintext[..populated])?;
222 self.cipher
223 .decrypt_in_place_detached(
224 XNonce::from_slice(&nonce),
225 &block_aad(block, length),
226 &mut plaintext[..populated],
227 (&tag).into(),
228 )
229 .map_err(|_| {
230 io::Error::new(
231 io::ErrorKind::InvalidData,
232 "temporary file authentication failed",
233 )
234 })?;
235 }
236 Ok(plaintext)
237 }
238
239 fn write_block(
240 &mut self,
241 block: u64,
242 ciphertext: &mut [u8; BYTES],
243 populated: usize,
244 ) -> io::Result<()> {
245 let existing = block < self.length.div_ceil(BYTES as u64);
246 let next = if existing {
247 self.active_slot(block)? ^ 1
248 } else {
249 0
250 };
251 let mut nonce = [0_u8; NONCE_BYTES];
252 getrandom::fill(&mut nonce).map_err(|error| io::Error::other(error.to_string()))?;
253 let length = u16::try_from(populated).expect("temporary blocks are at most 16 KiB");
254 let tag = self
255 .cipher
256 .encrypt_in_place_detached(
257 XNonce::from_slice(&nonce),
258 &block_aad(block, length),
259 &mut ciphertext[..populated],
260 )
261 .map_err(|_| io::Error::other("temporary file encryption failed"))?;
262 self.file
264 .as_file_mut()
265 .seek(SeekFrom::Start(slot_offset::<BYTES>(block, next)?))?;
266 self.write_physical(&nonce)?;
267 self.write_physical(&length.to_le_bytes())?;
268 self.write_physical(&tag)?;
269 self.write_physical(&ciphertext[..populated])?;
270 if !existing {
271 self.truncate_physical(record_offset::<BYTES>(block + 1)?)?;
272 }
273 self.file
274 .as_file_mut()
275 .seek(SeekFrom::Start(record_offset::<BYTES>(block)?))?;
276 self.write_physical(&[next])
277 }
278
279 fn write_physical(&mut self, bytes: &[u8]) -> io::Result<()> {
280 #[cfg(test)]
281 self.faults.before_write(self.file.as_file_mut(), bytes)?;
282 self.file.as_file_mut().write_all(bytes)
283 }
284
285 fn truncate_physical(&mut self, length: u64) -> io::Result<()> {
286 #[cfg(test)]
287 self.faults.before_truncate()?;
288 self.file.as_file().set_len(length)
289 }
290
291 fn rollback_length(&mut self, length: u64, cause: &io::Error) -> io::Result<()> {
292 self.length = length;
294 self.truncate_physical(record_offset::<BYTES>(length.div_ceil(BYTES as u64))?)
295 .map_err(|rollback| {
296 io::Error::other(format!(
297 "{cause}; temporary file rollback failed: {rollback}"
298 ))
299 })?;
300 Ok(())
301 }
302
303 fn extend_zeroed(&mut self, length: u64) -> io::Result<()> {
304 let original = self.length;
305 let result = (|| {
306 while self.length < length {
307 let block = self.length / BYTES as u64;
308 let offset = (self.length % BYTES as u64) as usize;
309 let mut bytes = self.read_block(block)?;
310 let count = (length - self.length).min((BYTES - offset) as u64) as usize;
311 bytes[offset..offset + count].fill(0);
312 self.write_block(block, &mut bytes, offset + count)?;
313 self.length += count as u64;
314 }
315 Ok(())
316 })();
317 if let Err(error) = result {
318 self.rollback_length(original, &error)?;
319 return Err(error);
320 }
321 Ok(())
322 }
323}
324
325fn slot_offset<const BYTES: usize>(block: u64, slot: u8) -> io::Result<u64> {
326 record_offset::<BYTES>(block)?
327 .checked_add(1 + u64::from(slot) * (SLOT_HEADER_BYTES + BYTES) as u64)
328 .ok_or_else(|| {
329 io::Error::new(
330 io::ErrorKind::InvalidInput,
331 "temporary file slot offset overflow",
332 )
333 })
334}
335
336impl<const BYTES: usize> Read for BlockTemporaryFile<BYTES> {
337 fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
338 let mut owner = self.owner.lock();
339 if self.position >= owner.length || output.is_empty() {
340 return Ok(0);
341 }
342 let block = self.position / BYTES as u64;
343 let offset = (self.position % BYTES as u64) as usize;
344 let bytes = owner.read_block(block)?;
345 let count =
346 (output.len().min(BYTES - offset) as u64).min(owner.length - self.position) as usize;
347 output[..count].copy_from_slice(&bytes[offset..offset + count]);
348 self.position += count as u64;
349 Ok(count)
350 }
351}
352
353impl<const BYTES: usize> Write for BlockTemporaryFile<BYTES> {
354 fn write(&mut self, input: &[u8]) -> io::Result<usize> {
355 self.write_vectored(&[IoSlice::new(input)])
356 }
357
358 fn write_vectored(&mut self, input: &[IoSlice<'_>]) -> io::Result<usize> {
359 if input.iter().all(|bytes| bytes.is_empty()) {
360 return Ok(0);
361 }
362 let minimum_end = self.position.checked_add(1).ok_or_else(|| {
363 io::Error::new(
364 io::ErrorKind::InvalidInput,
365 "temporary file length overflow",
366 )
367 })?;
368 record_offset::<BYTES>(minimum_end.div_ceil(BYTES as u64))?;
369 let mut owner = self.owner.lock();
370 let original = owner.length;
371 owner.extend_zeroed(self.position)?;
372 let block = self.position / BYTES as u64;
373 let offset = (self.position % BYTES as u64) as usize;
374 let mut bytes = match owner.read_block(block) {
375 Ok(bytes) => bytes,
376 Err(error) => {
377 owner.rollback_length(original, &error)?;
378 return Err(error);
379 }
380 };
381 let mut count = 0;
382 for source in input {
383 let taken = source.len().min(BYTES - offset - count);
384 bytes[offset + count..offset + count + taken].copy_from_slice(&source[..taken]);
385 count += taken;
386 if offset + count == BYTES {
387 break;
388 }
389 }
390 let next_position = self.position.checked_add(count as u64).ok_or_else(|| {
391 io::Error::new(
392 io::ErrorKind::InvalidInput,
393 "temporary file length overflow",
394 )
395 })?;
396 let populated =
397 (owner.length.max(next_position) - block * BYTES as u64).min(BYTES as u64) as usize;
398 if let Err(error) = owner.write_block(block, &mut bytes, populated) {
399 owner.rollback_length(original, &error)?;
400 return Err(error);
401 }
402 owner.length = owner.length.max(next_position);
403 self.position = next_position;
404 Ok(count)
405 }
406
407 fn flush(&mut self) -> io::Result<()> {
408 self.owner.lock().file.as_file_mut().flush()
409 }
410}
411
412impl<const BYTES: usize> Seek for BlockTemporaryFile<BYTES> {
413 fn seek(&mut self, position: SeekFrom) -> io::Result<u64> {
414 self.position = match position {
415 SeekFrom::Start(position) => position,
416 SeekFrom::End(offset) => self
417 .owner
418 .lock()
419 .length
420 .checked_add_signed(offset)
421 .ok_or_else(|| {
422 io::Error::new(io::ErrorKind::InvalidInput, "invalid temporary file seek")
423 })?,
424 SeekFrom::Current(offset) => {
425 self.position.checked_add_signed(offset).ok_or_else(|| {
426 io::Error::new(io::ErrorKind::InvalidInput, "invalid temporary file seek")
427 })?
428 }
429 };
430 Ok(self.position)
431 }
432}
433
434#[cfg(test)]
435mod tests;