From 693bdd77483c4d839493dafdcb9da6d0ef35124c Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 15:02:25 +0200 Subject: [PATCH] Fix truncate to handle non-UTF-8 filenames --- src/uu/truncate/src/truncate.rs | 33 ++++++++++++++++++--------------- tests/by-util/test_truncate.rs | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 15 deletions(-) diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 0ad207b7a..a2750b72a 100644 --- a/src/uu/truncate/src/truncate.rs +++ b/src/uu/truncate/src/truncate.rs @@ -5,6 +5,7 @@ // spell-checker:ignore (ToDO) RFILE refsize rfilename fsize tsize use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs::{OpenOptions, metadata}; use std::io::ErrorKind; #[cfg(unix)] @@ -94,9 +95,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } })?; - let files: Vec = matches - .get_many::(options::ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) + let files: Vec = matches + .get_many::(options::ARG_FILES) + .map(|v| v.cloned().collect()) .unwrap_or_default(); if files.is_empty() { @@ -158,7 +159,8 @@ pub fn uu_app() -> Command { .value_name("FILE") .action(ArgAction::Append) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } @@ -174,18 +176,18 @@ pub fn uu_app() -> Command { /// /// If the file could not be opened, or there was a problem setting the /// size of the file. -fn file_truncate(filename: &str, create: bool, size: u64) -> UResult<()> { +fn file_truncate(filename: &OsString, create: bool, size: u64) -> UResult<()> { + let path = Path::new(filename); + #[cfg(unix)] - if let Ok(metadata) = metadata(filename) { + if let Ok(metadata) = metadata(path) { if metadata.file_type().is_fifo() { return Err(USimpleError::new( 1, - translate!("truncate-error-cannot-open-no-device", "filename" => filename.quote()), + translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()), )); } } - - let path = Path::new(filename); match OpenOptions::new().write(true).create(create).open(path) { Ok(file) => file.set_len(size), Err(e) if e.kind() == ErrorKind::NotFound && !create => Ok(()), @@ -216,7 +218,7 @@ fn file_truncate(filename: &str, create: bool, size: u64) -> UResult<()> { fn truncate_reference_and_size( rfilename: &str, size_string: &str, - filenames: &[String], + filenames: &[OsString], create: bool, ) -> UResult<()> { let mode = match parse_mode_and_size(size_string) { @@ -275,7 +277,7 @@ fn truncate_reference_and_size( /// If at least one file is a named pipe (also known as a fifo). fn truncate_reference_file_only( rfilename: &str, - filenames: &[String], + filenames: &[OsString], create: bool, ) -> UResult<()> { let metadata = metadata(rfilename).map_err(|e| match e.kind() { @@ -312,7 +314,7 @@ fn truncate_reference_file_only( /// the size of at least one file. /// /// If at least one file is a named pipe (also known as a fifo). -fn truncate_size_only(size_string: &str, filenames: &[String], create: bool) -> UResult<()> { +fn truncate_size_only(size_string: &str, filenames: &[OsString], create: bool) -> UResult<()> { let mode = parse_mode_and_size(size_string).map_err(|e| { USimpleError::new(1, translate!("truncate-error-invalid-number", "error" => e)) })?; @@ -325,13 +327,14 @@ fn truncate_size_only(size_string: &str, filenames: &[String], create: bool) -> } for filename in filenames { - let fsize = match metadata(filename) { + let path = Path::new(filename); + let fsize = match metadata(path) { Ok(m) => { #[cfg(unix)] if m.file_type().is_fifo() { return Err(USimpleError::new( 1, - translate!("truncate-error-cannot-open-no-device", "filename" => filename.quote()), + translate!("truncate-error-cannot-open-no-device", "filename" => filename.to_string_lossy().quote()), )); } m.len() @@ -351,7 +354,7 @@ fn truncate( _: bool, reference: Option, size: Option, - filenames: &[String], + filenames: &[OsString], ) -> UResult<()> { let create = !no_create; diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index 789bd1d66..ef1ea872c 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -420,3 +420,27 @@ fn test_fifo_error_reference_and_size() { .no_stdout() .stderr_contains("cannot open 'fifo' for writing: No such device or address"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_truncate_non_utf8_paths() { + use std::fs; + + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + // Create test file with normal name first + at.write("temp.txt", "test content"); + + // Rename to non-UTF-8 name + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + + fs::rename(at.subdir.join("temp.txt"), at.subdir.join(file_name)).unwrap(); + + // Test that truncate can handle non-UTF-8 filenames + ts.ucmd().arg("-s").arg("10").arg(file_name).succeeds(); + } +}