Skip to content

Commit fe97933

Browse files
truncate: eliminate duplicate stat() syscall (#9527)
1 parent 16f7350 commit fe97933

File tree

1 file changed

+108
-180
lines changed

1 file changed

+108
-180
lines changed

src/uu/truncate/src/truncate.rs

Lines changed: 108 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -38,40 +38,55 @@ impl TruncateMode {
3838
/// reduce by is greater than `fsize`, then this function returns
3939
/// 0 (since it cannot return a negative number).
4040
///
41+
/// # Returns
42+
///
43+
/// `None` if rounding by 0, else the target size.
44+
///
4145
/// # Examples
4246
///
4347
/// Extending a file of 10 bytes by 5 bytes:
4448
///
4549
/// ```rust,ignore
4650
/// let mode = TruncateMode::Extend(5);
4751
/// let fsize = 10;
48-
/// assert_eq!(mode.to_size(fsize), 15);
52+
/// assert_eq!(mode.to_size(fsize), Some(15));
4953
/// ```
5054
///
5155
/// Reducing a file by more than its size results in 0:
5256
///
5357
/// ```rust,ignore
5458
/// let mode = TruncateMode::Reduce(5);
5559
/// let fsize = 3;
56-
/// assert_eq!(mode.to_size(fsize), 0);
60+
/// assert_eq!(mode.to_size(fsize), Some(0));
61+
/// ```
62+
///
63+
/// Rounding a file by 0:
64+
///
65+
/// ```rust,ignore
66+
/// let mode = TruncateMode::RoundDown(0);
67+
/// let fsize = 17;
68+
/// assert_eq!(mode.to_size(fsize), None);
5769
/// ```
58-
fn to_size(&self, fsize: u64) -> u64 {
70+
fn to_size(&self, fsize: u64) -> Option<u64> {
5971
match self {
60-
Self::Absolute(size) => *size,
61-
Self::Extend(size) => fsize + size,
62-
Self::Reduce(size) => {
63-
if *size > fsize {
64-
0
65-
} else {
66-
fsize - size
67-
}
68-
}
69-
Self::AtMost(size) => fsize.min(*size),
70-
Self::AtLeast(size) => fsize.max(*size),
71-
Self::RoundDown(size) => fsize - fsize % size,
72-
Self::RoundUp(size) => fsize + fsize % size,
72+
Self::Absolute(size) => Some(*size),
73+
Self::Extend(size) => Some(fsize + size),
74+
Self::Reduce(size) => Some(fsize.saturating_sub(*size)),
75+
Self::AtMost(size) => Some(fsize.min(*size)),
76+
Self::AtLeast(size) => Some(fsize.max(*size)),
77+
Self::RoundDown(size) => fsize.checked_rem(*size).map(|remainder| fsize - remainder),
78+
Self::RoundUp(size) => fsize.checked_next_multiple_of(*size),
7379
}
7480
}
81+
82+
/// Determine if mode is absolute
83+
///
84+
/// # Returns
85+
///
86+
/// `true` is self matches Self::Absolute(_), `false` otherwise.
87+
fn is_absolute(&self) -> bool {
88+
matches!(self, Self::Absolute(_))
89+
}
7590
}
7691

7792
pub mod options {
@@ -170,18 +185,9 @@ pub fn uu_app() -> Command {
170185
///
171186
/// If the file could not be opened, or there was a problem setting the
172187
/// size of the file.
173-
fn file_truncate(filename: &OsString, create: bool, size: u64) -> UResult<()> {
188+
fn do_file_truncate(filename: &Path, create: bool, size: u64) -> UResult<()> {
174189
let path = Path::new(filename);
175190

176-
#[cfg(unix)]
177-
if let Ok(metadata) = metadata(path) {
178-
if metadata.file_type().is_fifo() {
179-
return Err(USimpleError::new(
180-
1,
181-
translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()),
182-
));
183-
}
184-
}
185191
match OpenOptions::new().write(true).create(create).open(path) {
186192
Ok(file) => file.set_len(size),
187193
Err(e) if e.kind() == ErrorKind::NotFound && !create => Ok(()),
@@ -192,181 +198,99 @@ fn file_truncate(filename: &OsString, create: bool, size: u64) -> UResult<()> {
192198
)
193199
}
194200

195-
/// Truncate files to a size relative to a given file.
196-
///
197-
/// `rfilename` is the name of the reference file.
198-
///
199-
/// `size_string` gives the size relative to the reference file to which
200-
/// to set the target files. For example, "+3K" means "set each file to
201-
/// be three kilobytes larger than the size of the reference file".
202-
///
203-
/// If `create` is true, then each file will be created if it does not
204-
/// already exist.
205-
///
206-
/// # Errors
207-
///
208-
/// If any file could not be opened, or there was a problem setting
209-
/// the size of at least one file.
210-
///
211-
/// If at least one file is a named pipe (also known as a fifo).
212-
fn truncate_reference_and_size(
213-
rfilename: &str,
214-
size_string: &str,
215-
filenames: &[OsString],
216-
create: bool,
201+
fn file_truncate(
202+
no_create: bool,
203+
reference_size: Option<u64>,
204+
mode: &TruncateMode,
205+
filename: &OsString,
217206
) -> UResult<()> {
218-
let mode = match parse_mode_and_size(size_string) {
219-
Err(e) => {
220-
return Err(USimpleError::new(
221-
1,
222-
translate!("truncate-error-invalid-number", "error" => e),
223-
));
224-
}
225-
Ok(TruncateMode::Absolute(_)) => {
226-
return Err(USimpleError::new(
227-
1,
228-
translate!("truncate-error-must-specify-relative-size"),
229-
));
207+
let path = Path::new(filename);
208+
209+
// Get the length of the file.
210+
let file_size = match metadata(path) {
211+
Ok(metadata) => {
212+
// A pipe has no length. Do this check here to avoid duplicate `stat()` syscall.
213+
#[cfg(unix)]
214+
if metadata.file_type().is_fifo() {
215+
return Err(USimpleError::new(
216+
1,
217+
translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()),
218+
));
219+
}
220+
metadata.len()
230221
}
231-
Ok(m) => m,
222+
Err(_) => 0,
232223
};
233224

234-
if let TruncateMode::RoundDown(0) | TruncateMode::RoundUp(0) = mode {
225+
// The reference size can be either:
226+
//
227+
// 1. The size of a given file
228+
// 2. The size of the file to be truncated if no reference has been provided.
229+
let actual_reference_size = reference_size.unwrap_or(file_size);
230+
231+
let Some(truncate_size) = mode.to_size(actual_reference_size) else {
235232
return Err(USimpleError::new(
236233
1,
237234
translate!("truncate-error-division-by-zero"),
238235
));
239-
}
240-
241-
let metadata = metadata(rfilename).map_err(|e| match e.kind() {
242-
ErrorKind::NotFound => USimpleError::new(
243-
1,
244-
translate!("truncate-error-cannot-stat-no-such-file", "filename" => rfilename.quote()),
245-
),
246-
_ => e.map_err_context(String::new),
247-
})?;
248-
249-
let fsize = metadata.len();
250-
let tsize = mode.to_size(fsize);
251-
252-
for filename in filenames {
253-
file_truncate(filename, create, tsize)?;
254-
}
236+
};
255237

256-
Ok(())
238+
do_file_truncate(path, !no_create, truncate_size)
257239
}
258240

259-
/// Truncate files to match the size of a given reference file.
260-
///
261-
/// `rfilename` is the name of the reference file.
262-
///
263-
/// If `create` is true, then each file will be created if it does not
264-
/// already exist.
265-
///
266-
/// # Errors
267-
///
268-
/// If any file could not be opened, or there was a problem setting
269-
/// the size of at least one file.
270-
///
271-
/// If at least one file is a named pipe (also known as a fifo).
272-
fn truncate_reference_file_only(
273-
rfilename: &str,
241+
fn truncate(
242+
no_create: bool,
243+
_: bool,
244+
reference: Option<String>,
245+
size: Option<String>,
274246
filenames: &[OsString],
275-
create: bool,
276247
) -> UResult<()> {
277-
let metadata = metadata(rfilename).map_err(|e| match e.kind() {
278-
ErrorKind::NotFound => USimpleError::new(
279-
1,
280-
translate!("truncate-error-cannot-stat-no-such-file", "filename" => rfilename.quote()),
281-
),
282-
_ => e.map_err_context(String::new),
283-
})?;
284-
285-
let tsize = metadata.len();
286-
287-
for filename in filenames {
288-
file_truncate(filename, create, tsize)?;
289-
}
248+
let reference_size = match reference {
249+
Some(reference_path) => {
250+
let reference_metadata = metadata(&reference_path).map_err(|error| match error.kind() {
251+
ErrorKind::NotFound => USimpleError::new(
252+
1,
253+
translate!("truncate-error-cannot-stat-no-such-file", "filename" => reference_path.quote()),
254+
),
255+
_ => error.map_err_context(String::new),
256+
})?;
257+
258+
Some(reference_metadata.len())
259+
}
260+
None => None,
261+
};
290262

291-
Ok(())
292-
}
263+
let size_string = size.as_deref();
293264

294-
/// Truncate files to a specified size.
295-
///
296-
/// `size_string` gives either an absolute size or a relative size. A
297-
/// relative size adjusts the size of each file relative to its current
298-
/// size. For example, "3K" means "set each file to be three kilobytes"
299-
/// whereas "+3K" means "set each file to be three kilobytes larger than
300-
/// its current size".
301-
///
302-
/// If `create` is true, then each file will be created if it does not
303-
/// already exist.
304-
///
305-
/// # Errors
306-
///
307-
/// If any file could not be opened, or there was a problem setting
308-
/// the size of at least one file.
309-
///
310-
/// If at least one file is a named pipe (also known as a fifo).
311-
fn truncate_size_only(size_string: &str, filenames: &[OsString], create: bool) -> UResult<()> {
312-
let mode = parse_mode_and_size(size_string).map_err(|e| {
313-
USimpleError::new(1, translate!("truncate-error-invalid-number", "error" => e))
314-
})?;
265+
// Omitting the mode is equivalent to extending a file by 0 bytes.
266+
let mode = match size_string {
267+
Some(string) => match parse_mode_and_size(string) {
268+
Err(error) => {
269+
return Err(USimpleError::new(
270+
1,
271+
translate!("truncate-error-invalid-number", "error" => error),
272+
));
273+
}
274+
Ok(mode) => mode,
275+
},
276+
None => TruncateMode::Extend(0),
277+
};
315278

316-
if let TruncateMode::RoundDown(0) | TruncateMode::RoundUp(0) = mode {
279+
// If a reference file has been given, the truncate mode cannot be absolute.
280+
if reference_size.is_some() && mode.is_absolute() {
317281
return Err(USimpleError::new(
318282
1,
319-
translate!("truncate-error-division-by-zero"),
283+
translate!("truncate-error-must-specify-relative-size"),
320284
));
321285
}
322286

323287
for filename in filenames {
324-
let path = Path::new(filename);
325-
let fsize = match metadata(path) {
326-
Ok(m) => {
327-
#[cfg(unix)]
328-
if m.file_type().is_fifo() {
329-
return Err(USimpleError::new(
330-
1,
331-
translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()),
332-
));
333-
}
334-
m.len()
335-
}
336-
Err(_) => 0,
337-
};
338-
let tsize = mode.to_size(fsize);
339-
// TODO: Fix duplicate call to stat
340-
file_truncate(filename, create, tsize)?;
288+
file_truncate(no_create, reference_size, &mode, filename)?;
341289
}
342290

343291
Ok(())
344292
}
345293

346-
fn truncate(
347-
no_create: bool,
348-
_: bool,
349-
reference: Option<String>,
350-
size: Option<String>,
351-
filenames: &[OsString],
352-
) -> UResult<()> {
353-
let create = !no_create;
354-
355-
// There are four possibilities
356-
// - reference file given and size given,
357-
// - reference file given but no size given,
358-
// - no reference file given but size given,
359-
// - no reference file given and no size given,
360-
match (reference, size) {
361-
(Some(rfilename), Some(size_string)) => {
362-
truncate_reference_and_size(&rfilename, &size_string, filenames, create)
363-
}
364-
(Some(rfilename), None) => truncate_reference_file_only(&rfilename, filenames, create),
365-
(None, Some(size_string)) => truncate_size_only(&size_string, filenames, create),
366-
(None, None) => unreachable!(), // this case cannot happen anymore because it's handled by clap
367-
}
368-
}
369-
370294
/// Decide whether a character is one of the size modifiers, like '+' or '<'.
371295
fn is_modifier(c: char) -> bool {
372296
c == '+' || c == '-' || c == '<' || c == '>' || c == '/' || c == '%'
@@ -382,13 +306,12 @@ fn is_modifier(c: char) -> bool {
382306
///
383307
/// # Panics
384308
///
385-
/// If `size_string` is empty, or if no number could be parsed from the
386-
/// given string (for example, if the string were `"abc"`).
309+
/// If `size_string` is empty.
387310
///
388311
/// # Examples
389312
///
390313
/// ```rust,ignore
391-
/// assert_eq!(parse_mode_and_size("+123"), (TruncateMode::Extend, 123));
314+
/// assert_eq!(parse_mode_and_size("+123"), Ok(TruncateMode::Extend(123)));
392315
/// ```
393316
fn parse_mode_and_size(size_string: &str) -> Result<TruncateMode, ParseSizeError> {
394317
// Trim any whitespace.
@@ -432,8 +355,13 @@ mod tests {
432355

433356
#[test]
434357
fn test_to_size() {
435-
assert_eq!(TruncateMode::Extend(5).to_size(10), 15);
436-
assert_eq!(TruncateMode::Reduce(5).to_size(10), 5);
437-
assert_eq!(TruncateMode::Reduce(5).to_size(3), 0);
358+
assert_eq!(TruncateMode::Extend(5).to_size(10), Some(15));
359+
assert_eq!(TruncateMode::Reduce(5).to_size(10), Some(5));
360+
assert_eq!(TruncateMode::Reduce(5).to_size(3), Some(0));
361+
assert_eq!(TruncateMode::RoundDown(4).to_size(13), Some(12));
362+
assert_eq!(TruncateMode::RoundDown(4).to_size(16), Some(16));
363+
assert_eq!(TruncateMode::RoundUp(8).to_size(10), Some(16));
364+
assert_eq!(TruncateMode::RoundUp(8).to_size(16), Some(16));
365+
assert_eq!(TruncateMode::RoundDown(0).to_size(123), None);
438366
}
439367
}

0 commit comments

Comments
 (0)