Coverage Report

Created: 2025-07-18 06:03

/rust/registry/src/index.crates.io-6f17d22bba15001f/tempfile-3.20.0/src/spooled.rs
Line
Count
Source (jump to first uncovered line)
1
use crate::file::tempfile;
2
use crate::tempfile_in;
3
use std::fs::File;
4
use std::io::{self, Cursor, Read, Seek, SeekFrom, Write};
5
use std::path::{Path, PathBuf};
6
7
/// A wrapper for the two states of a [`SpooledTempFile`]. Either:
8
///
9
/// 1. An in-memory [`Cursor`] representing the state of the file.
10
/// 2. A temporary [`File`].
11
#[derive(Debug)]
12
pub enum SpooledData {
13
    InMemory(Cursor<Vec<u8>>),
14
    OnDisk(File),
15
}
16
17
/// An object that behaves like a regular temporary file, but keeps data in
18
/// memory until it reaches a configured size, at which point the data is
19
/// written to a temporary file on disk, and further operations use the file
20
/// on disk.
21
#[derive(Debug)]
22
pub struct SpooledTempFile {
23
    max_size: usize,
24
    dir: Option<PathBuf>,
25
    inner: SpooledData,
26
}
27
28
/// Create a new [`SpooledTempFile`]. Also see [`spooled_tempfile_in`].
29
///
30
/// # Security
31
///
32
/// This variant is secure/reliable in the presence of a pathological temporary
33
/// file cleaner.
34
///
35
/// # Backing Storage
36
///
37
/// By default, the underlying temporary file will be created in your operating system's temporary
38
/// file directory which is _often_ an in-memory filesystem. You may want to consider using
39
/// [`spooled_tempfile_in`] instead, passing a storage-backed filesystem (e.g., `/var/tmp` on
40
/// Linux).
41
///
42
/// # Resource Leaking
43
///
44
/// The temporary file will be automatically removed by the OS when the last
45
/// handle to it is closed. This doesn't rely on Rust destructors being run, so
46
/// will (almost) never fail to clean up the temporary file.
47
///
48
/// # Examples
49
///
50
/// ```
51
/// use tempfile::spooled_tempfile;
52
/// use std::io::Write;
53
///
54
/// let mut file = spooled_tempfile(15);
55
///
56
/// writeln!(file, "short line")?;
57
/// assert!(!file.is_rolled());
58
///
59
/// // as a result of this write call, the size of the data will exceed
60
/// // `max_size` (15), so it will be written to a temporary file on disk,
61
/// // and the in-memory buffer will be dropped
62
/// writeln!(file, "marvin gardens")?;
63
/// assert!(file.is_rolled());
64
/// # Ok::<(), std::io::Error>(())
65
/// ```
66
#[inline]
67
0
pub fn spooled_tempfile(max_size: usize) -> SpooledTempFile {
68
0
    SpooledTempFile::new(max_size)
69
0
}
70
71
/// Construct a new [`SpooledTempFile`], backed by a file in the specified directory. Use this when,
72
/// e.g., you need the temporary file to be backed by a specific filesystem (e.g., when your default
73
/// temporary directory is in-memory). Also see [`spooled_tempfile`].
74
///
75
/// **NOTE:** The specified path isn't checked until the temporary file is "rolled over" into a real
76
/// temporary file. If the specified directory isn't writable, writes to the temporary file will
77
/// fail once the `max_size` is reached.
78
#[inline]
79
0
pub fn spooled_tempfile_in<P: AsRef<Path>>(max_size: usize, dir: P) -> SpooledTempFile {
80
0
    SpooledTempFile::new_in(max_size, dir)
81
0
}
82
83
/// Write a cursor into a temporary file, returning the temporary file.
84
0
fn cursor_to_tempfile(cursor: &Cursor<Vec<u8>>, p: &Option<PathBuf>) -> io::Result<File> {
85
0
    let mut file = match p {
86
0
        Some(p) => tempfile_in(p)?,
87
0
        None => tempfile()?,
88
    };
89
0
    file.write_all(cursor.get_ref())?;
90
0
    file.seek(SeekFrom::Start(cursor.position()))?;
91
0
    Ok(file)
92
0
}
93
94
impl SpooledTempFile {
95
    /// Construct a new [`SpooledTempFile`].
96
    #[must_use]
97
0
    pub fn new(max_size: usize) -> SpooledTempFile {
98
0
        SpooledTempFile {
99
0
            max_size,
100
0
            dir: None,
101
0
            inner: SpooledData::InMemory(Cursor::new(Vec::new())),
102
0
        }
103
0
    }
104
105
    /// Construct a new [`SpooledTempFile`], backed by a file in the specified directory.
106
    #[must_use]
107
0
    pub fn new_in<P: AsRef<Path>>(max_size: usize, dir: P) -> SpooledTempFile {
108
0
        SpooledTempFile {
109
0
            max_size,
110
0
            dir: Some(dir.as_ref().to_owned()),
111
0
            inner: SpooledData::InMemory(Cursor::new(Vec::new())),
112
0
        }
113
0
    }
114
115
    /// Returns true if the file has been rolled over to disk.
116
    #[must_use]
117
0
    pub fn is_rolled(&self) -> bool {
118
0
        match self.inner {
119
0
            SpooledData::InMemory(_) => false,
120
0
            SpooledData::OnDisk(_) => true,
121
        }
122
0
    }
123
124
    /// Rolls over to a file on disk, regardless of current size. Does nothing
125
    /// if already rolled over.
126
0
    pub fn roll(&mut self) -> io::Result<()> {
127
0
        if let SpooledData::InMemory(cursor) = &mut self.inner {
128
0
            self.inner = SpooledData::OnDisk(cursor_to_tempfile(cursor, &self.dir)?);
129
0
        }
130
0
        Ok(())
131
0
    }
132
133
    /// Truncate the file to the specified size.
134
0
    pub fn set_len(&mut self, size: u64) -> Result<(), io::Error> {
135
0
        if size > self.max_size as u64 {
136
0
            self.roll()?; // does nothing if already rolled over
137
0
        }
138
0
        match &mut self.inner {
139
0
            SpooledData::InMemory(cursor) => {
140
0
                cursor.get_mut().resize(size as usize, 0);
141
0
                Ok(())
142
            }
143
0
            SpooledData::OnDisk(file) => file.set_len(size),
144
        }
145
0
    }
146
147
    /// Consumes and returns the inner `SpooledData` type.
148
    #[must_use]
149
0
    pub fn into_inner(self) -> SpooledData {
150
0
        self.inner
151
0
    }
152
153
    /// Convert into a regular unnamed temporary file, writing it to disk if necessary.
154
0
    pub fn into_file(self) -> io::Result<File> {
155
0
        match self.inner {
156
0
            SpooledData::InMemory(cursor) => cursor_to_tempfile(&cursor, &self.dir),
157
0
            SpooledData::OnDisk(file) => Ok(file),
158
        }
159
0
    }
160
}
161
162
impl Read for SpooledTempFile {
163
0
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
164
0
        match &mut self.inner {
165
0
            SpooledData::InMemory(cursor) => cursor.read(buf),
166
0
            SpooledData::OnDisk(file) => file.read(buf),
167
        }
168
0
    }
169
170
0
    fn read_vectored(&mut self, bufs: &mut [io::IoSliceMut<'_>]) -> io::Result<usize> {
171
0
        match &mut self.inner {
172
0
            SpooledData::InMemory(cursor) => cursor.read_vectored(bufs),
173
0
            SpooledData::OnDisk(file) => file.read_vectored(bufs),
174
        }
175
0
    }
176
177
0
    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
178
0
        match &mut self.inner {
179
0
            SpooledData::InMemory(cursor) => cursor.read_to_end(buf),
180
0
            SpooledData::OnDisk(file) => file.read_to_end(buf),
181
        }
182
0
    }
183
184
0
    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
185
0
        match &mut self.inner {
186
0
            SpooledData::InMemory(cursor) => cursor.read_to_string(buf),
187
0
            SpooledData::OnDisk(file) => file.read_to_string(buf),
188
        }
189
0
    }
190
191
0
    fn read_exact(&mut self, buf: &mut [u8]) -> io::Result<()> {
192
0
        match &mut self.inner {
193
0
            SpooledData::InMemory(cursor) => cursor.read_exact(buf),
194
0
            SpooledData::OnDisk(file) => file.read_exact(buf),
195
        }
196
0
    }
197
}
198
199
impl Write for SpooledTempFile {
200
0
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
201
        // roll over to file if necessary
202
0
        if matches! {
203
0
            &self.inner, SpooledData::InMemory(cursor)
204
0
            if cursor.position().saturating_add(buf.len() as u64) > self.max_size as u64
205
        } {
206
0
            self.roll()?;
207
0
        }
208
209
        // write the bytes
210
0
        match &mut self.inner {
211
0
            SpooledData::InMemory(cursor) => cursor.write(buf),
212
0
            SpooledData::OnDisk(file) => file.write(buf),
213
        }
214
0
    }
215
216
0
    fn write_vectored(&mut self, bufs: &[io::IoSlice<'_>]) -> io::Result<usize> {
217
0
        if matches! {
218
0
            &self.inner, SpooledData::InMemory(cursor)
219
0
            // Borrowed from the rust standard library.
220
0
            if bufs
221
0
                .iter()
222
0
                .fold(cursor.position(), |a, b| a.saturating_add(b.len() as u64))
223
0
                > self.max_size as u64
224
        } {
225
0
            self.roll()?;
226
0
        }
227
0
        match &mut self.inner {
228
0
            SpooledData::InMemory(cursor) => cursor.write_vectored(bufs),
229
0
            SpooledData::OnDisk(file) => file.write_vectored(bufs),
230
        }
231
0
    }
232
233
    #[inline]
234
0
    fn flush(&mut self) -> io::Result<()> {
235
0
        match &mut self.inner {
236
0
            SpooledData::InMemory(cursor) => cursor.flush(),
237
0
            SpooledData::OnDisk(file) => file.flush(),
238
        }
239
0
    }
240
}
241
242
impl Seek for SpooledTempFile {
243
0
    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
244
0
        match &mut self.inner {
245
0
            SpooledData::InMemory(cursor) => cursor.seek(pos),
246
0
            SpooledData::OnDisk(file) => file.seek(pos),
247
        }
248
0
    }
249
}