Skip to content

Commit d09d05d

Browse files
truncate: Avoid duplicate stat() syscall
Signed-off-by: Jean-Christian CÎRSTEA <[email protected]>
1 parent 821c674 commit d09d05d

File tree

1 file changed

+97
-177
lines changed

1 file changed

+97
-177
lines changed

src/uu/truncate/src/truncate.rs

Lines changed: 97 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -38,40 +38,50 @@ 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_rem(*size).map(|remainder| fsize + remainder),
7379
}
7480
}
81+
82+
fn is_absolute(&self) -> bool {
83+
matches!(self, Self::Absolute(_))
84+
}
7585
}
7686

7787
pub mod options {
@@ -170,18 +180,9 @@ pub fn uu_app() -> Command {
170180
///
171181
/// If the file could not be opened, or there was a problem setting the
172182
/// size of the file.
173-
fn file_truncate(filename: &OsString, create: bool, size: u64) -> UResult<()> {
183+
fn do_file_truncate(filename: &Path, create: bool, size: u64) -> UResult<()> {
174184
let path = Path::new(filename);
175185

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-
}
185186
match OpenOptions::new().write(true).create(create).open(path) {
186187
Ok(file) => file.set_len(size),
187188
Err(e) if e.kind() == ErrorKind::NotFound && !create => Ok(()),
@@ -192,181 +193,97 @@ fn file_truncate(filename: &OsString, create: bool, size: u64) -> UResult<()> {
192193
)
193194
}
194195

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,
196+
fn file_truncate(
197+
no_create: bool,
198+
reference_size: Option<u64>,
199+
mode: &TruncateMode,
200+
filename: &OsString,
217201
) -> 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-
));
202+
let path = Path::new(filename);
203+
204+
let file_size = match metadata(path) {
205+
Ok(metadata) => {
206+
#[cfg(unix)]
207+
if metadata.file_type().is_fifo() {
208+
return Err(USimpleError::new(
209+
1,
210+
translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()),
211+
));
212+
}
213+
metadata.len()
230214
}
231-
Ok(m) => m,
215+
Err(_) => 0,
232216
};
233217

234-
if let TruncateMode::RoundDown(0) | TruncateMode::RoundUp(0) = mode {
218+
// The reference size can be either:
219+
//
220+
// 1. The size of a given file
221+
// 2. The size of the file to be truncated if no reference has been provided.
222+
let actual_reference_size = reference_size.unwrap_or(file_size);
223+
224+
let Some(truncate_size) = mode.to_size(actual_reference_size) else {
235225
return Err(USimpleError::new(
236226
1,
237227
translate!("truncate-error-division-by-zero"),
238228
));
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-
}
229+
};
255230

256-
Ok(())
231+
do_file_truncate(path, !no_create, truncate_size)
257232
}
258233

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,
234+
fn truncate(
235+
no_create: bool,
236+
_: bool,
237+
reference: Option<String>,
238+
size: Option<String>,
274239
filenames: &[OsString],
275-
create: bool,
276240
) -> 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-
}
241+
let reference_size = match reference {
242+
Some(reference_path) => {
243+
let reference_metadata = metadata(&reference_path).map_err(|error| match error.kind() {
244+
ErrorKind::NotFound => USimpleError::new(
245+
1,
246+
translate!("truncate-error-cannot-stat-no-such-file", "filename" => reference_path.quote()),
247+
),
248+
_ => error.map_err_context(String::new),
249+
})?;
250+
251+
Some(reference_metadata.len())
252+
}
253+
None => None,
254+
};
290255

291-
Ok(())
292-
}
256+
let size_string = size.as_deref();
293257

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-
})?;
258+
// Omitting the mode is equivalent to extending a file by 0 bytes.
259+
let mode = match size_string {
260+
Some(string) => match parse_mode_and_size(string) {
261+
Err(error) => {
262+
return Err(USimpleError::new(
263+
1,
264+
translate!("truncate-error-invalid-number", "error" => error),
265+
));
266+
}
267+
Ok(mode) => mode,
268+
},
269+
None => TruncateMode::Extend(0),
270+
};
315271

316-
if let TruncateMode::RoundDown(0) | TruncateMode::RoundUp(0) = mode {
272+
// If a reference file has been given, the truncate mode cannot be absolute.
273+
if reference_size.is_some() && mode.is_absolute() {
317274
return Err(USimpleError::new(
318275
1,
319-
translate!("truncate-error-division-by-zero"),
276+
translate!("truncate-error-must-specify-relative-size"),
320277
));
321278
}
322279

323280
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)?;
281+
file_truncate(no_create, reference_size, &mode, filename)?;
341282
}
342283

343284
Ok(())
344285
}
345286

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-
370287
/// Decide whether a character is one of the size modifiers, like '+' or '<'.
371288
fn is_modifier(c: char) -> bool {
372289
c == '+' || c == '-' || c == '<' || c == '>' || c == '/' || c == '%'
@@ -432,8 +349,11 @@ mod tests {
432349

433350
#[test]
434351
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);
352+
assert_eq!(TruncateMode::Extend(5).to_size(10), Some(15));
353+
assert_eq!(TruncateMode::Reduce(5).to_size(10), Some(5));
354+
assert_eq!(TruncateMode::Reduce(5).to_size(3), Some(0));
355+
assert_eq!(TruncateMode::RoundDown(4).to_size(13), Some(12));
356+
assert_eq!(TruncateMode::RoundUp(8).to_size(16), Some(16));
357+
assert_eq!(TruncateMode::RoundDown(0).to_size(123), None);
438358
}
439359
}

0 commit comments

Comments
 (0)