@@ -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
7792pub 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 '<'.
371295fn 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/// ```
393316fn 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