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