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