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