diff --git a/.github/workflows/fuzzing.yml b/.github/workflows/fuzzing.yml index a25ca34d01a..cf8d943c3a8 100644 --- a/.github/workflows/fuzzing.yml +++ b/.github/workflows/fuzzing.yml @@ -62,6 +62,7 @@ jobs: - { name: fuzz_parse_size, should_pass: true } - { name: fuzz_parse_time, should_pass: true } - { name: fuzz_seq_parse_number, should_pass: true } + - { name: fuzz_non_utf8_paths, should_pass: true } steps: - uses: actions/checkout@v5 diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 363c10064d1..14282122461 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -138,3 +138,9 @@ name = "fuzz_cksum" path = "fuzz_targets/fuzz_cksum.rs" test = false doc = false + +[[bin]] +name = "fuzz_non_utf8_paths" +path = "fuzz_targets/fuzz_non_utf8_paths.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs new file mode 100644 index 00000000000..ac7480f3230 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -0,0 +1,442 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore osstring + +#![no_main] +use libfuzzer_sys::fuzz_target; +use rand::Rng; +use rand::prelude::IndexedRandom; +use std::collections::HashSet; +use std::env::temp_dir; +use std::ffi::{OsStr, OsString}; +use std::fs; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; +use std::path::PathBuf; + +use uufuzz::{CommandResult, run_gnu_cmd}; +// Programs that typically take file/path arguments and should be tested +static PATH_PROGRAMS: &[&str] = &[ + // Core file operations + "cat", + "cp", + "mv", + "rm", + "ln", + "link", + "unlink", + "touch", + "truncate", + // Path operations + "ls", + "mkdir", + "rmdir", + "du", + "stat", + "mktemp", + "df", + "basename", + "dirname", + "readlink", + "realpath", + "pathchk", + "chroot", + // File processing + "head", + "tail", + "tee", + "more", + "od", + "wc", + "cksum", + "sum", + "nl", + "tac", + "sort", + "uniq", + "split", + "csplit", + "cut", + "tr", + "shred", + "shuf", + "ptx", + "tsort", + // Text processing with files + "chmod", + "chown", + "chgrp", + "install", + "chcon", + "runcon", + "comm", + "join", + "paste", + "pr", + "fmt", + "fold", + "expand", + "unexpand", + "dir", + "vdir", + "mkfifo", + "mknod", + "hashsum", + // File I/O utilities + "dd", + "sync", + "stdbuf", + "dircolors", + // Encoding/decoding utilities + "base32", + "base64", + "basenc", + "stty", + "tty", + "env", + "nohup", + "nice", + "timeout", +]; + +fn generate_non_utf8_bytes() -> Vec { + let mut rng = rand::rng(); + let mut bytes = Vec::new(); + + // Start with some valid UTF-8 to make it look like a reasonable path + bytes.extend_from_slice(b"test_"); + + // Add some invalid UTF-8 sequences + match rng.random_range(0..4) { + 0 => bytes.extend_from_slice(&[0xFF, 0xFE]), // Invalid UTF-8 + 1 => bytes.extend_from_slice(&[0xC0, 0x80]), // Overlong encoding + 2 => bytes.extend_from_slice(&[0xED, 0xA0, 0x80]), // UTF-16 surrogate + _ => bytes.extend_from_slice(&[0xF4, 0x90, 0x80, 0x80]), // Beyond Unicode range + } + + bytes +} + +fn generate_non_utf8_osstring() -> OsString { + OsString::from_vec(generate_non_utf8_bytes()) +} + +fn setup_test_files() -> Result<(PathBuf, Vec), std::io::Error> { + let mut rng = rand::rng(); + let temp_root = temp_dir().join(format!("utf8_test_{}", rng.random::())); + fs::create_dir_all(&temp_root)?; + + let mut test_files = Vec::new(); + + // Create some files with non-UTF-8 names + for i in 0..3 { + let mut path_bytes = temp_root.as_os_str().as_bytes().to_vec(); + path_bytes.push(b'/'); + + if i == 0 { + // One normal UTF-8 file for comparison + path_bytes.extend_from_slice(b"normal_file.txt"); + } else { + // Files with invalid UTF-8 names + path_bytes.extend_from_slice(&generate_non_utf8_bytes()); + } + + let file_path = PathBuf::from(OsStr::from_bytes(&path_bytes)); + + // Try to create the file - this may fail on some filesystems + if let Ok(mut file) = fs::File::create(&file_path) { + use std::io::Write; + let _ = write!(file, "test content for file {}\n", i); + test_files.push(file_path); + } + } + + Ok((temp_root, test_files)) +} + +fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResult { + let path_os = path.as_os_str(); + + // Use the locally built uutils binary instead of system PATH + let local_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + + // Build appropriate arguments for each program + let local_args = match program { + // Programs that need mode/permissions + "chmod" => vec![ + OsString::from(program), + OsString::from("644"), + path_os.to_owned(), + ], + "chown" => vec![ + OsString::from(program), + OsString::from("root:root"), + path_os.to_owned(), + ], + "chgrp" => vec![ + OsString::from(program), + OsString::from("root"), + path_os.to_owned(), + ], + "chcon" => vec![ + OsString::from(program), + OsString::from("system_u:object_r:admin_home_t:s0"), + path_os.to_owned(), + ], + "runcon" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from("system_u:object_r:admin_home_t:s0"), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + // Programs that need source and destination + "cp" | "mv" | "ln" | "link" => { + let dest_path = path.with_extension("dest"); + vec![ + OsString::from(program), + path_os.to_owned(), + dest_path.as_os_str().to_owned(), + ] + } + "install" => { + let dest_path = path.with_extension("dest"); + vec![ + OsString::from(program), + path_os.to_owned(), + dest_path.as_os_str().to_owned(), + ] + } + // Programs that need size/truncate operations + "truncate" => vec![ + OsString::from(program), + OsString::from("--size=0"), + path_os.to_owned(), + ], + "split" => vec![ + OsString::from(program), + path_os.to_owned(), + OsString::from("split_prefix_"), + ], + "csplit" => vec![ + OsString::from(program), + path_os.to_owned(), + OsString::from("1"), + ], + // File creation programs + "mkfifo" | "mknod" => { + let new_path = path.with_extension("new"); + if program == "mknod" { + vec![ + OsString::from(program), + new_path.as_os_str().to_owned(), + OsString::from("c"), + OsString::from("1"), + OsString::from("3"), + ] + } else { + vec![OsString::from(program), new_path.as_os_str().to_owned()] + } + } + "dd" => vec![ + OsString::from(program), + OsString::from(format!("if={}", path_os.to_string_lossy())), + OsString::from("of=/dev/null"), + OsString::from("bs=1"), + OsString::from("count=1"), + ], + // Hashsum needs algorithm + "hashsum" => vec![ + OsString::from(program), + OsString::from("--md5"), + path_os.to_owned(), + ], + // Encoding/decoding programs + "base32" | "base64" | "basenc" => vec![OsString::from(program), path_os.to_owned()], + "df" => vec![OsString::from(program), path_os.to_owned()], + "chroot" => { + // chroot needs a directory and command + vec![ + OsString::from(program), + path_os.to_owned(), + OsString::from("true"), + ] + } + "sync" => vec![OsString::from(program), path_os.to_owned()], + "stty" => vec![ + OsString::from(program), + OsString::from("-F"), + path_os.to_owned(), + ], + "tty" => vec![OsString::from(program)], // tty doesn't take file args, but test anyway + "env" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + "nohup" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + "nice" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + "timeout" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from("1"), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + "stdbuf" => { + let coreutils_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + vec![ + OsString::from(program), + OsString::from("-o0"), + OsString::from(coreutils_binary), + OsString::from("cat"), + path_os.to_owned(), + ] + } + // Programs that work with multiple files (use just one for testing) + "comm" | "join" => { + // These need two files, use the same file twice for simplicity + vec![ + OsString::from(program), + path_os.to_owned(), + path_os.to_owned(), + ] + } + // Programs that typically take file input + _ => vec![OsString::from(program), path_os.to_owned()], + }; + + // Try to run the local uutils version + match run_gnu_cmd(&local_binary, &local_args, false, None) { + Ok(result) => result, + Err(error_result) => { + // Local command failed, return the error + error_result + } + } +} + +fn cleanup_test_files(temp_root: &PathBuf) { + let _ = fs::remove_dir_all(temp_root); +} + +fn check_for_utf8_error_and_panic(result: &CommandResult, program: &str, path: &PathBuf) { + let stderr_lower = result.stderr.to_lowercase(); + let is_utf8_error = stderr_lower.contains("invalid utf-8") + || stderr_lower.contains("not valid unicode") + || stderr_lower.contains("invalid utf8") + || stderr_lower.contains("utf-8 error"); + + if is_utf8_error { + println!( + "UTF-8 conversion error detected in {}: {}", + program, result.stderr + ); + println!("Path: {:?}", path); + println!("Exit code: {}", result.exit_code); + panic!( + "FUZZER FAILURE: {} failed with UTF-8 error on non-UTF-8 path: {:?}", + program, path + ); + } +} + +fuzz_target!(|_data: &[u8]| { + let mut rng = rand::rng(); + + // Set up test environment + let (temp_root, test_files) = match setup_test_files() { + Ok(files) => files, + Err(_) => return, // Skip if we can't set up test files + }; + + // Pick multiple random programs to test in each iteration + let num_programs_to_test = rng.random_range(1..=3); // Test 1-3 programs per iteration + let mut tested_programs = HashSet::new(); + + let mut programs_tested = Vec::::new(); + + for _ in 0..num_programs_to_test { + // Pick a random program that we haven't tested yet in this iteration + let available_programs: Vec<_> = PATH_PROGRAMS + .iter() + .filter(|p| !tested_programs.contains(*p)) + .collect(); + + if available_programs.is_empty() { + break; + } + + let program = available_programs.choose(&mut rng).unwrap(); + tested_programs.insert(*program); + programs_tested.push(program.to_string()); + + // Test with one random file that has non-UTF-8 names (not all files to speed up) + if let Some(test_file) = test_files.choose(&mut rng) { + let result = test_program_with_non_utf8_path(program, test_file); + + // Check if the program handled the non-UTF-8 path gracefully + check_for_utf8_error_and_panic(&result, program, test_file); + } + + // Special cases for programs that need additional testing + if **program == "mkdir" || **program == "mktemp" { + let non_utf8_dir_name = generate_non_utf8_osstring(); + let non_utf8_dir = temp_root.join(non_utf8_dir_name); + + let local_binary = std::env::var("CARGO_BIN_FILE_COREUTILS") + .unwrap_or_else(|_| "target/release/coreutils".to_string()); + let mkdir_args = vec![OsString::from("mkdir"), non_utf8_dir.as_os_str().to_owned()]; + + let mkdir_result = run_gnu_cmd(&local_binary, &mkdir_args, false, None); + match mkdir_result { + Ok(result) => { + check_for_utf8_error_and_panic(&result, "mkdir", &non_utf8_dir); + } + Err(error) => { + check_for_utf8_error_and_panic(&error, "mkdir", &non_utf8_dir); + } + } + } + } + + println!("Tested programs: {}", programs_tested.join(", ")); + + // Clean up + cleanup_test_files(&temp_root); +}); diff --git a/src/uu/base32/src/base_common.rs b/src/uu/base32/src/base_common.rs index db1e97016f5..5c5dd983d8a 100644 --- a/src/uu/base32/src/base_common.rs +++ b/src/uu/base32/src/base_common.rs @@ -6,6 +6,7 @@ // spell-checker:ignore hexupper lsbf msbf unpadded nopad aGVsbG8sIHdvcmxkIQ use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs::File; use std::io::{self, ErrorKind, Read, Seek, SeekFrom}; use std::path::{Path, PathBuf}; @@ -44,14 +45,14 @@ pub mod options { impl Config { pub fn from(options: &clap::ArgMatches) -> UResult { - let to_read = match options.get_many::(options::FILE) { + let to_read = match options.get_many::(options::FILE) { Some(mut values) => { let name = values.next().unwrap(); if let Some(extra_op) = values.next() { return Err(UUsageError::new( BASE_CMD_PARSE_ERROR, - translate!("base-common-extra-operand", "operand" => extra_op.quote()), + translate!("base-common-extra-operand", "operand" => extra_op.to_string_lossy().quote()), )); } @@ -143,6 +144,7 @@ pub fn base_app(about: &'static str, usage: &str) -> Command { Arg::new(options::FILE) .index(1) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) } diff --git a/src/uu/cat/src/cat.rs b/src/uu/cat/src/cat.rs index f03c7c3b981..89c9f211132 100644 --- a/src/uu/cat/src/cat.rs +++ b/src/uu/cat/src/cat.rs @@ -10,6 +10,7 @@ mod platform; use crate::platform::is_unsafe_overwrite; use clap::{Arg, ArgAction, Command}; use memchr::memchr2; +use std::ffi::OsString; use std::fs::{File, metadata}; use std::io::{self, BufWriter, ErrorKind, IsTerminal, Read, Write}; /// Unix domain socket support @@ -267,9 +268,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .any(|v| matches.get_flag(v)); let squeeze_blank = matches.get_flag(options::SQUEEZE_BLANK); - let files: Vec = match matches.get_many::(options::FILE) { + let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), - None => vec!["-".to_owned()], + None => vec![OsString::from("-")], }; let options = OutputOptions { @@ -294,6 +295,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) .arg( @@ -379,7 +381,7 @@ fn cat_handle( } } -fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> { +fn cat_path(path: &OsString, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> { match get_input_type(path)? { InputType::StdIn => { let stdin = io::stdin(); @@ -417,7 +419,7 @@ fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> Cat } } -fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> { +fn cat_files(files: &[OsString], options: &OutputOptions) -> UResult<()> { let mut state = OutputState { line_number: LineNumber::new(), at_line_start: true, @@ -452,7 +454,7 @@ fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> { /// # Arguments /// /// * `path` - Path on a file system to classify metadata -fn get_input_type(path: &str) -> CatResult { +fn get_input_type(path: &OsString) -> CatResult { if path == "-" { return Ok(InputType::StdIn); } diff --git a/src/uu/chgrp/src/chgrp.rs b/src/uu/chgrp/src/chgrp.rs index aca69d10372..07859d07d05 100644 --- a/src/uu/chgrp/src/chgrp.rs +++ b/src/uu/chgrp/src/chgrp.rs @@ -6,7 +6,7 @@ // spell-checker:ignore (ToDO) COMFOLLOW Chowner RFILE RFILE's derefer dgid nonblank nonprint nonprinting use uucore::display::Quotable; -pub use uucore::entries; +use uucore::entries; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options}; @@ -37,15 +37,16 @@ fn parse_gid_from_str(group: &str) -> Result { fn get_dest_gid(matches: &ArgMatches) -> UResult<(Option, String)> { let mut raw_group = String::new(); - let dest_gid = if let Some(file) = matches.get_one::(options::REFERENCE) { - fs::metadata(file) + let dest_gid = if let Some(file) = matches.get_one::(options::REFERENCE) { + let path = std::path::Path::new(file); + fs::metadata(path) .map(|meta| { let gid = meta.gid(); raw_group = entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string()); Some(gid) }) .map_err_context( - || translate!("chgrp-error-failed-to-get-attributes", "file" => file.quote()), + || translate!("chgrp-error-failed-to-get-attributes", "file" => path.quote()), )? } else { let group = matches @@ -153,6 +154,7 @@ pub fn uu_app() -> Command { .long(options::REFERENCE) .value_name("RFILE") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(std::ffi::OsString)) .help(translate!("chgrp-help-reference")), ) .arg( diff --git a/src/uu/chmod/src/chmod.rs b/src/uu/chmod/src/chmod.rs index 92ea6024e86..d8152d8bdc6 100644 --- a/src/uu/chmod/src/chmod.rs +++ b/src/uu/chmod/src/chmod.rs @@ -119,11 +119,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let quiet = matches.get_flag(options::QUIET); let verbose = matches.get_flag(options::VERBOSE); let preserve_root = matches.get_flag(options::PRESERVE_ROOT); - let fmode = match matches.get_one::(options::REFERENCE) { + let fmode = match matches.get_one::(options::REFERENCE) { Some(fref) => match fs::metadata(fref) { Ok(meta) => Some(meta.mode() & 0o7777), Err(_) => { - return Err(ChmodError::CannotStat(fref.to_string()).into()); + return Err(ChmodError::CannotStat(fref.to_string_lossy().to_string()).into()); } }, None => None, @@ -135,16 +135,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else { modes.unwrap().to_string() // modes is required }; - // FIXME: enable non-utf8 paths - let mut files: Vec = matches - .get_many::(options::FILE) - .map(|v| v.map(ToString::to_string).collect()) + let mut files: Vec = matches + .get_many::(options::FILE) + .map(|v| v.cloned().collect()) .unwrap_or_default(); let cmode = if fmode.is_some() { // "--reference" and MODE are mutually exclusive // if "--reference" was used MODE needs to be interpreted as another FILE // it wasn't possible to implement this behavior directly with clap - files.push(cmode); + files.push(OsString::from(cmode)); None } else { Some(cmode) @@ -236,6 +235,7 @@ pub fn uu_app() -> Command { Arg::new(options::REFERENCE) .long("reference") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .help(translate!("chmod-help-reference")), ) .arg( @@ -248,7 +248,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .required_unless_present(options::MODE) .action(ArgAction::Append) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) // Add common arguments with chgrp, chown & chmod .args(uucore::perms::common_args()) @@ -267,11 +268,10 @@ struct Chmoder { } impl Chmoder { - fn chmod(&self, files: &[String]) -> UResult<()> { + fn chmod(&self, files: &[OsString]) -> UResult<()> { let mut r = Ok(()); for filename in files { - let filename = &filename[..]; let file = Path::new(filename); if !file.exists() { if file.is_symlink() { @@ -285,18 +285,22 @@ impl Chmoder { } if !self.quiet { - show!(ChmodError::DanglingSymlink(filename.to_string())); + show!(ChmodError::DanglingSymlink( + filename.to_string_lossy().to_string() + )); set_exit_code(1); } if self.verbose { println!( "{}", - translate!("chmod-verbose-failed-dangling", "file" => filename.quote()) + translate!("chmod-verbose-failed-dangling", "file" => filename.to_string_lossy().quote()) ); } } else if !self.quiet { - show!(ChmodError::NoSuchFile(filename.to_string())); + show!(ChmodError::NoSuchFile( + filename.to_string_lossy().to_string() + )); } // GNU exits with exit code 1 even if -q or --quiet are passed // So we set the exit code, because it hasn't been set yet if `self.quiet` is true. @@ -308,8 +312,8 @@ impl Chmoder { // should not change the permissions in this case continue; } - if self.recursive && self.preserve_root && filename == "/" { - return Err(ChmodError::PreserveRoot(filename.to_string()).into()); + if self.recursive && self.preserve_root && file == Path::new("/") { + return Err(ChmodError::PreserveRoot("/".to_string()).into()); } if self.recursive { r = self.walk_dir_with_context(file, true); diff --git a/src/uu/comm/src/comm.rs b/src/uu/comm/src/comm.rs index b77c549d75a..e383d4d6fa4 100644 --- a/src/uu/comm/src/comm.rs +++ b/src/uu/comm/src/comm.rs @@ -6,8 +6,10 @@ // spell-checker:ignore (ToDO) delim mkdelim pairable use std::cmp::Ordering; +use std::ffi::OsString; use std::fs::{File, metadata}; use std::io::{self, BufRead, BufReader, Read, Stdin, stdin}; +use std::path::Path; use uucore::LocalizedCommand; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::format_usage; @@ -115,7 +117,7 @@ impl OrderChecker { } // Check if two files are identical by comparing their contents -pub fn are_files_identical(path1: &str, path2: &str) -> io::Result { +pub fn are_files_identical(path1: &Path, path2: &Path) -> io::Result { // First compare file sizes let metadata1 = metadata(path1)?; let metadata2 = metadata(path2)?; @@ -174,11 +176,11 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) let should_check_order = !no_check_order && (check_order || if let (Some(file1), Some(file2)) = ( - opts.get_one::(options::FILE_1), - opts.get_one::(options::FILE_2), + opts.get_one::(options::FILE_1), + opts.get_one::(options::FILE_2), ) { - !(paths_refer_to_same_file(file1, file2, true) - || are_files_identical(file1, file2).unwrap_or(false)) + !(paths_refer_to_same_file(file1.as_os_str(), file2.as_os_str(), true) + || are_files_identical(Path::new(file1), Path::new(file2)).unwrap_or(false)) } else { true }); @@ -264,7 +266,7 @@ fn comm(a: &mut LineReader, b: &mut LineReader, delim: &str, opts: &ArgMatches) } } -fn open_file(name: &str, line_ending: LineEnding) -> io::Result { +fn open_file(name: &OsString, line_ending: LineEnding) -> io::Result { if name == "-" { Ok(LineReader::new(Input::Stdin(stdin()), line_ending)) } else { @@ -283,10 +285,12 @@ fn open_file(name: &str, line_ending: LineEnding) -> io::Result { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO_TERMINATED)); - let filename1 = matches.get_one::(options::FILE_1).unwrap(); - let filename2 = matches.get_one::(options::FILE_2).unwrap(); - let mut f1 = open_file(filename1, line_ending).map_err_context(|| filename1.to_string())?; - let mut f2 = open_file(filename2, line_ending).map_err_context(|| filename2.to_string())?; + let filename1 = matches.get_one::(options::FILE_1).unwrap(); + let filename2 = matches.get_one::(options::FILE_2).unwrap(); + let mut f1 = open_file(filename1, line_ending) + .map_err_context(|| filename1.to_string_lossy().to_string())?; + let mut f2 = open_file(filename2, line_ending) + .map_err_context(|| filename2.to_string_lossy().to_string())?; // Due to default_value(), there must be at least one value here, thus unwrap() must not panic. let all_delimiters = matches @@ -360,12 +364,14 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FILE_1) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::FILE_2) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::TOTAL) diff --git a/src/uu/csplit/src/csplit.rs b/src/uu/csplit/src/csplit.rs index fc18b97da75..a3e10e2b061 100644 --- a/src/uu/csplit/src/csplit.rs +++ b/src/uu/csplit/src/csplit.rs @@ -6,6 +6,7 @@ #![allow(rustdoc::private_intra_doc_links)] use std::cmp::Ordering; +use std::ffi::OsString; use std::io::{self, BufReader, ErrorKind}; use std::{ fs::{File, remove_file}, @@ -608,7 +609,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); // get the file to split - let file_name = matches.get_one::(options::FILE).unwrap(); + let file_name = matches.get_one::(options::FILE).unwrap(); // get the patterns to split on let patterns: Vec = matches @@ -689,7 +690,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .required(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::PATTERN) diff --git a/src/uu/cut/src/cut.rs b/src/uu/cut/src/cut.rs index 57f0e61d18c..aea44c98875 100644 --- a/src/uu/cut/src/cut.rs +++ b/src/uu/cut/src/cut.rs @@ -343,11 +343,11 @@ fn cut_fields( } } -fn cut_files(mut filenames: Vec, mode: &Mode) { +fn cut_files(mut filenames: Vec, mode: &Mode) { let mut stdin_read = false; if filenames.is_empty() { - filenames.push("-".to_owned()); + filenames.push(OsString::from("-")); } let mut out: Box = if stdout().is_terminal() { @@ -370,12 +370,12 @@ fn cut_files(mut filenames: Vec, mode: &Mode) { stdin_read = true; } else { - let path = Path::new(&filename[..]); + let path = Path::new(filename); if path.is_dir() { show_error!( "{}: {}", - filename.maybe_quote(), + filename.to_string_lossy().maybe_quote(), translate!("cut-error-is-directory") ); set_exit_code(1); @@ -384,7 +384,7 @@ fn cut_files(mut filenames: Vec, mode: &Mode) { show_if_err!( File::open(path) - .map_err_context(|| filename.maybe_quote().to_string()) + .map_err_context(|| filename.to_string_lossy().to_string()) .and_then(|file| { match &mode { Mode::Bytes(ranges, opts) | Mode::Characters(ranges, opts) => { @@ -577,8 +577,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }, }; - let files: Vec = matches - .get_many::(options::FILE) + let files: Vec = matches + .get_many::(options::FILE) .unwrap_or_default() .cloned() .collect(); @@ -681,6 +681,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/src/uu/dircolors/src/dircolors.rs b/src/uu/dircolors/src/dircolors.rs index 87a459f2952..c00f5d210f0 100644 --- a/src/uu/dircolors/src/dircolors.rs +++ b/src/uu/dircolors/src/dircolors.rs @@ -7,6 +7,7 @@ use std::borrow::Borrow; use std::env; +use std::ffi::OsString; use std::fmt::Write as _; use std::fs::File; use std::io::{BufRead, BufReader}; @@ -124,7 +125,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); let files = matches - .get_many::(options::FILE) + .get_many::(options::FILE) .map_or(vec![], |file_values| file_values.collect()); // clap provides .conflicts_with / .conflicts_with_all, but we want to @@ -149,7 +150,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if !files.is_empty() { return Err(UUsageError::new( 1, - translate!("dircolors-error-extra-operand-print-database", "operand" => files[0].quote()), + translate!("dircolors-error-extra-operand-print-database", "operand" => files[0].to_string_lossy().quote()), )); } @@ -198,14 +199,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } else if files.len() > 1 { return Err(UUsageError::new( 1, - translate!("dircolors-error-extra-operand", "operand" => files[1].quote()), + translate!("dircolors-error-extra-operand", "operand" => files[1].to_string_lossy().quote()), )); - } else if files[0].eq("-") { + } else if files[0] == "-" { let fin = BufReader::new(std::io::stdin()); // For example, for echo "owt 40;33"|dircolors -b - - result = parse(fin.lines().map_while(Result::ok), &out_format, files[0]); + result = parse( + fin.lines().map_while(Result::ok), + &out_format, + &files[0].to_string_lossy(), + ); } else { - let path = Path::new(files[0]); + let path = Path::new(&files[0]); if path.is_dir() { return Err(USimpleError::new( 2, @@ -280,6 +285,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .action(ArgAction::Append), ) } diff --git a/src/uu/dirname/src/dirname.rs b/src/uu/dirname/src/dirname.rs index aac8e57f3c4..2782c7fb3a9 100644 --- a/src/uu/dirname/src/dirname.rs +++ b/src/uu/dirname/src/dirname.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::path::Path; use uucore::LocalizedCommand; use uucore::display::print_verbatim; @@ -26,8 +27,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO)); - let dirnames: Vec = matches - .get_many::(options::DIR) + let dirnames: Vec = matches + .get_many::(options::DIR) .unwrap_or_default() .cloned() .collect(); @@ -47,7 +48,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } None => { - if p.is_absolute() || path == "/" { + if p.is_absolute() || path.as_os_str() == "/" { print!("/"); } else { print!("."); @@ -79,6 +80,7 @@ pub fn uu_app() -> Command { Arg::new(options::DIR) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 64a7662bb12..d4e20054d47 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -7,6 +7,8 @@ use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; use glob::Pattern; use std::collections::HashSet; use std::env; +use std::ffi::OsStr; +use std::ffi::OsString; use std::fs::Metadata; use std::fs::{self, DirEntry, File}; use std::io::{BufRead, BufReader, stdout}; @@ -530,7 +532,7 @@ impl StatPrinter { } /// Read file paths from the specified file, separated by null characters -fn read_files_from(file_name: &str) -> Result, std::io::Error> { +fn read_files_from(file_name: &OsStr) -> Result, std::io::Error> { let reader: Box = if file_name == "-" { // Read from standard input Box::new(BufReader::new(std::io::stdin())) @@ -539,7 +541,7 @@ fn read_files_from(file_name: &str) -> Result, std::io::Error> { let path = PathBuf::from(file_name); if path.is_dir() { return Err(std::io::Error::other( - translate!("du-error-read-error-is-directory", "file" => file_name), + translate!("du-error-read-error-is-directory", "file" => file_name.to_string_lossy()), )); } @@ -548,7 +550,7 @@ fn read_files_from(file_name: &str) -> Result, std::io::Error> { Ok(file) => Box::new(BufReader::new(file)), Err(e) if e.kind() == std::io::ErrorKind::NotFound => { return Err(std::io::Error::other( - translate!("du-error-cannot-open-for-reading", "file" => file_name), + translate!("du-error-cannot-open-for-reading", "file" => file_name.to_string_lossy()), )); } Err(e) => return Err(e), @@ -564,11 +566,11 @@ fn read_files_from(file_name: &str) -> Result, std::io::Error> { let line_number = i + 1; show_error!( "{}", - translate!("du-error-invalid-zero-length-file-name", "file" => file_name, "line" => line_number) + translate!("du-error-invalid-zero-length-file-name", "file" => file_name.to_string_lossy(), "line" => line_number) ); set_exit_code(1); } else { - let p = PathBuf::from(String::from_utf8_lossy(&path).to_string()); + let p = PathBuf::from(&*uucore::os_str_from_bytes(&path).unwrap()); if !paths.contains(&p) { paths.push(p); } @@ -594,13 +596,14 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { summarize, )?; - let files = if let Some(file_from) = matches.get_one::(options::FILES0_FROM) { - if file_from == "-" && matches.get_one::(options::FILE).is_some() { + let files = if let Some(file_from) = matches.get_one::(options::FILES0_FROM) { + if file_from == "-" && matches.get_one::(options::FILE).is_some() { return Err(std::io::Error::other( translate!("du-error-extra-operand-with-files0-from", "file" => matches - .get_one::(options::FILE) + .get_one::(options::FILE) .unwrap() + .to_string_lossy() .quote() ), ) @@ -608,7 +611,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } read_files_from(file_from)? - } else if let Some(files) = matches.get_many::(options::FILE) { + } else if let Some(files) = matches.get_many::(options::FILE) { let files = files.map(PathBuf::from); if count_links { files.collect() @@ -984,6 +987,7 @@ pub fn uu_app() -> Command { .long("files0-from") .value_name("FILE") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .help(translate!("du-help-files0-from")) .action(ArgAction::Append), ) @@ -1010,6 +1014,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)) .action(ArgAction::Append), ) } diff --git a/src/uu/expand/src/expand.rs b/src/uu/expand/src/expand.rs index cd528316e7f..5b0b8f4e7ea 100644 --- a/src/uu/expand/src/expand.rs +++ b/src/uu/expand/src/expand.rs @@ -170,7 +170,7 @@ fn tabstops_parse(s: &str) -> Result<(RemainingMode, Vec), ParseError> { } struct Options { - files: Vec, + files: Vec, tabstops: Vec, tspaces: String, iflag: bool, @@ -204,9 +204,9 @@ impl Options { .unwrap(); // length of tabstops is guaranteed >= 1 let tspaces = " ".repeat(nspaces); - let files: Vec = match matches.get_many::(options::FILES) { - Some(s) => s.map(|v| v.to_string()).collect(), - None => vec!["-".to_owned()], + let files: Vec = match matches.get_many::(options::FILES) { + Some(s) => s.cloned().collect(), + None => vec![OsString::from("-")], }; Ok(Self { @@ -283,16 +283,18 @@ pub fn uu_app() -> Command { Arg::new(options::FILES) .action(ArgAction::Append) .hide(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } -fn open(path: &str) -> UResult>> { +fn open(path: &OsString) -> UResult>> { let file_buf; if path == "-" { Ok(BufReader::new(Box::new(stdin()) as Box)) } else { - file_buf = File::open(path).map_err_context(|| path.to_string())?; + let path_ref = Path::new(path); + file_buf = File::open(path_ref).map_err_context(|| path.to_string_lossy().to_string())?; Ok(BufReader::new(Box::new(file_buf) as Box)) } } @@ -446,7 +448,7 @@ fn expand(options: &Options) -> UResult<()> { if Path::new(file).is_dir() { show_error!( "{}", - translate!("expand-error-is-directory", "file" => file) + translate!("expand-error-is-directory", "file" => file.to_string_lossy()) ); set_exit_code(1); continue; diff --git a/src/uu/fmt/src/fmt.rs b/src/uu/fmt/src/fmt.rs index 46f9d547f18..4919fb43577 100644 --- a/src/uu/fmt/src/fmt.rs +++ b/src/uu/fmt/src/fmt.rs @@ -6,8 +6,10 @@ // spell-checker:ignore (ToDO) PSKIP linebreak ostream parasplit tabwidth xanti xprefix use clap::{Arg, ArgAction, ArgMatches, Command}; +use std::ffi::OsString; use std::fs::File; use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout}; +use std::path::Path; use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError}; use uucore::translate; @@ -205,27 +207,27 @@ impl FmtOptions { /// /// A `UResult<()>` indicating success or failure. fn process_file( - file_name: &str, + file_name: &OsString, fmt_opts: &FmtOptions, ostream: &mut BufWriter, ) -> UResult<()> { - let mut fp = BufReader::new(match file_name { - "-" => Box::new(stdin()) as Box, - _ => { - let f = File::open(file_name).map_err_context( - || translate!("fmt-error-cannot-open-for-reading", "file" => file_name.quote()), - )?; - if f.metadata() - .map_err_context( - || translate!("fmt-error-cannot-get-metadata", "file" => file_name.quote()), - )? - .is_dir() - { - return Err(FmtError::ReadError.into()); - } - - Box::new(f) as Box + let mut fp = BufReader::new(if file_name == "-" { + Box::new(stdin()) as Box + } else { + let path = Path::new(file_name); + let f = File::open(path).map_err_context( + || translate!("fmt-error-cannot-open-for-reading", "file" => path.quote()), + )?; + if f.metadata() + .map_err_context( + || translate!("fmt-error-cannot-get-metadata", "file" => path.quote()), + )? + .is_dir() + { + return Err(FmtError::ReadError.into()); } + + Box::new(f) as Box }); let p_stream = ParagraphStream::new(fmt_opts, &mut fp); @@ -258,23 +260,24 @@ fn process_file( /// # Returns /// A `UResult<()>` with the file names, or an error if one of the file names could not be parsed /// (e.g., it is given as a negative number not in the first argument and not after a -- -fn extract_files(matches: &ArgMatches) -> UResult> { +fn extract_files(matches: &ArgMatches) -> UResult> { let in_first_pos = matches .index_of(options::FILES_OR_WIDTH) .is_some_and(|x| x == 1); let is_neg = |s: &str| s.parse::().is_ok_and(|w| w < 0); - let files: UResult> = matches - .get_many::(options::FILES_OR_WIDTH) + let files: UResult> = matches + .get_many::(options::FILES_OR_WIDTH) .into_iter() .flatten() .enumerate() .filter_map(|(i, x)| { - if is_neg(x) { + let x_str = x.to_string_lossy(); + if is_neg(&x_str) { if in_first_pos && i == 0 { None } else { - let first_num = x + let first_num = x_str .chars() .nth(1) .expect("a negative number should be at least two characters long"); @@ -287,7 +290,7 @@ fn extract_files(matches: &ArgMatches) -> UResult> { .collect(); if files.as_ref().is_ok_and(|f| f.is_empty()) { - Ok(vec!["-".into()]) + Ok(vec![OsString::from("-")]) } else { files } @@ -304,8 +307,11 @@ fn extract_width(matches: &ArgMatches) -> UResult> { } if let Some(1) = matches.index_of(options::FILES_OR_WIDTH) { - let width_arg = matches.get_one::(options::FILES_OR_WIDTH).unwrap(); - if let Some(num) = width_arg.strip_prefix('-') { + let width_arg = matches + .get_one::(options::FILES_OR_WIDTH) + .unwrap(); + let width_str = width_arg.to_string_lossy(); + if let Some(num) = width_str.strip_prefix('-') { Ok(num.parse::().ok()) } else { // will be treated as a file name @@ -456,6 +462,7 @@ pub fn uu_app() -> Command { .action(ArgAction::Append) .value_name("FILES") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .allow_negative_numbers(true), ) } diff --git a/src/uu/head/src/head.rs b/src/uu/head/src/head.rs index 818062ba939..e4837a630f4 100644 --- a/src/uu/head/src/head.rs +++ b/src/uu/head/src/head.rs @@ -49,6 +49,7 @@ enum HeadError { ParseError(String), #[error("{}", translate!("head-error-bad-encoding"))] + #[allow(dead_code)] BadEncoding, #[error("{}", translate!("head-error-num-too-large"))] @@ -129,6 +130,7 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FILES_NAME) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) } @@ -178,15 +180,22 @@ fn arg_iterate<'a>( let first = args.next().unwrap(); if let Some(second) = args.next() { if let Some(s) = second.to_str() { - match parse::parse_obsolete(s) { - Some(Ok(iter)) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))), - Some(Err(parse::ParseError)) => Err(HeadError::ParseError( - translate!("head-error-bad-argument-format", "arg" => s.quote()), - )), - None => Ok(Box::new(vec![first, second].into_iter().chain(args))), + if let Some(v) = parse::parse_obsolete(s) { + match v { + Ok(iter) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))), + Err(parse::ParseError) => Err(HeadError::ParseError( + translate!("head-error-bad-argument-format", "arg" => s.quote()), + )), + } + } else { + // The second argument contains non-UTF-8 sequences, so it can't be an obsolete option + // like "-5". Treat it as a regular file argument. + Ok(Box::new(vec![first, second].into_iter().chain(args))) } } else { - Err(HeadError::BadEncoding) + // The second argument contains non-UTF-8 sequences, so it can't be an obsolete option + // like "-5". Treat it as a regular file argument. + Ok(Box::new(vec![first, second].into_iter().chain(args))) } } else { Ok(Box::new(vec![first].into_iter())) @@ -200,7 +209,7 @@ struct HeadOptions { pub line_ending: LineEnding, pub presume_input_pipe: bool, pub mode: Mode, - pub files: Vec, + pub files: Vec, } impl HeadOptions { @@ -215,9 +224,9 @@ impl HeadOptions { options.mode = Mode::from(matches)?; - options.files = match matches.get_many::(options::FILES_NAME) { + options.files = match matches.get_many::(options::FILES_NAME) { Some(v) => v.cloned().collect(), - None => vec!["-".to_owned()], + None => vec![OsString::from("-")], }; Ok(options) @@ -463,76 +472,74 @@ fn head_file(input: &mut File, options: &HeadOptions) -> io::Result { fn uu_head(options: &HeadOptions) -> UResult<()> { let mut first = true; for file in &options.files { - let res = match file.as_str() { - "-" => { - if (options.files.len() > 1 && !options.quiet) || options.verbose { - if !first { - println!(); - } - println!("{}", translate!("head-header-stdin")); + let res = if file == "-" { + if (options.files.len() > 1 && !options.quiet) || options.verbose { + if !first { + println!(); } - let stdin = io::stdin(); - - #[cfg(unix)] - { - let stdin_raw_fd = stdin.as_raw_fd(); - let mut stdin_file = unsafe { File::from_raw_fd(stdin_raw_fd) }; - let current_pos = stdin_file.stream_position(); - if let Ok(current_pos) = current_pos { - // We have a seekable file. Ensure we set the input stream to the - // last byte read so that any tools that parse the remainder of - // the stdin stream read from the correct place. - - let bytes_read = head_file(&mut stdin_file, options)?; - stdin_file.seek(SeekFrom::Start(current_pos + bytes_read))?; - } else { - let _bytes_read = head_file(&mut stdin_file, options)?; - } + println!("{}", translate!("head-header-stdin")); + } + let stdin = io::stdin(); + + #[cfg(unix)] + { + let stdin_raw_fd = stdin.as_raw_fd(); + let mut stdin_file = unsafe { File::from_raw_fd(stdin_raw_fd) }; + let current_pos = stdin_file.stream_position(); + if let Ok(current_pos) = current_pos { + // We have a seekable file. Ensure we set the input stream to the + // last byte read so that any tools that parse the remainder of + // the stdin stream read from the correct place. + + let bytes_read = head_file(&mut stdin_file, options)?; + stdin_file.seek(SeekFrom::Start(current_pos + bytes_read))?; + } else { + let _bytes_read = head_file(&mut stdin_file, options)?; } + } - #[cfg(not(unix))] - { - let mut stdin = stdin.lock(); - - match options.mode { - Mode::FirstBytes(n) => read_n_bytes(&mut stdin, n), - Mode::AllButLastBytes(n) => read_but_last_n_bytes(&mut stdin, n), - Mode::FirstLines(n) => { - read_n_lines(&mut stdin, n, options.line_ending.into()) - } - Mode::AllButLastLines(n) => { - read_but_last_n_lines(&mut stdin, n, options.line_ending.into()) - } - }?; - } + #[cfg(not(unix))] + { + let mut stdin = stdin.lock(); - Ok(()) - } - name => { - let mut file = match File::open(name) { - Ok(f) => f, - Err(err) => { - show!(err.map_err_context( - || translate!("head-error-cannot-open", "name" => name.quote()) - )); - continue; + match options.mode { + Mode::FirstBytes(n) => read_n_bytes(&mut stdin, n), + Mode::AllButLastBytes(n) => read_but_last_n_bytes(&mut stdin, n), + Mode::FirstLines(n) => read_n_lines(&mut stdin, n, options.line_ending.into()), + Mode::AllButLastLines(n) => { + read_but_last_n_lines(&mut stdin, n, options.line_ending.into()) } - }; - if (options.files.len() > 1 && !options.quiet) || options.verbose { - if !first { - println!(); - } - println!("==> {name} <=="); + }?; + } + + Ok(()) + } else { + let mut file_handle = match File::open(file) { + Ok(f) => f, + Err(err) => { + show!(err.map_err_context( + || translate!("head-error-cannot-open", "name" => file.to_string_lossy().quote()) + )); + continue; + } + }; + if (options.files.len() > 1 && !options.quiet) || options.verbose { + if !first { + println!(); + } + match file.to_str() { + Some(name) => println!("==> {name} <=="), + None => println!("==> {} <==", file.to_string_lossy()), } - head_file(&mut file, options)?; - Ok(()) } + head_file(&mut file_handle, options)?; + Ok(()) }; if let Err(e) = res { - let name = if file.as_str() == "-" { - "standard input" + let name = if file == "-" { + "standard input".to_string() } else { - file + file.to_string_lossy().into_owned() }; return Err(HeadError::Io { name: name.to_string(), @@ -675,7 +682,7 @@ mod tests { use std::os::unix::ffi::OsStringExt; let invalid = OsString::from_vec(vec![b'\x80', b'\x81']); // this arises from a conversion from OsString to &str - assert!(arg_iterate(vec![OsString::from("head"), invalid].into_iter()).is_err()); + assert!(arg_iterate(vec![OsString::from("head"), invalid].into_iter()).is_ok()); } #[test] diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 66e80b90d62..903f3928d58 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -10,6 +10,7 @@ mod mode; use clap::{Arg, ArgAction, ArgMatches, Command}; use file_diff::diff; use filetime::{FileTime, set_file_times}; +use std::ffi::OsString; use std::fmt::Debug; use std::fs::File; use std::fs::{self, metadata}; @@ -168,9 +169,9 @@ static ARG_FILES: &str = "files"; pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); - let paths: Vec = matches - .get_many::(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) + let paths: Vec = matches + .get_many::(ARG_FILES) + .map(|v| v.cloned().collect()) .unwrap_or_default(); let behavior = behavior(&matches)?; @@ -303,7 +304,8 @@ pub fn uu_app() -> Command { Arg::new(ARG_FILES) .action(ArgAction::Append) .num_args(1..) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) } @@ -435,7 +437,7 @@ fn behavior(matches: &ArgMatches) -> UResult { /// /// Returns a Result type with the Err variant containing the error message. /// -fn directory(paths: &[String], b: &Behavior) -> UResult<()> { +fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> { if paths.is_empty() { Err(InstallError::DirNeedsArg.into()) } else { @@ -518,7 +520,7 @@ fn is_potential_directory_path(path: &Path) -> bool { /// Returns a Result type with the Err variant containing the error message. /// #[allow(clippy::cognitive_complexity)] -fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { +fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { // first check that paths contains at least one element if paths.is_empty() { return Err(UUsageError::new( @@ -528,7 +530,7 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { } if b.no_target_dir && paths.len() > 2 { return Err(InstallError::ExtraOperand( - paths[2].clone(), + paths[2].to_string_lossy().into_owned(), format_usage(&translate!("install-usage")), ) .into()); @@ -544,7 +546,7 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { if paths.is_empty() { return Err(UUsageError::new( 1, - translate!("install-error-missing-destination-operand", "path" => last_path.to_str().unwrap()), + translate!("install-error-missing-destination-operand", "path" => last_path.to_string_lossy()), )); } @@ -566,10 +568,18 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { if let Some(to_create) = to_create { // if the path ends in /, remove it - let to_create = if to_create.to_string_lossy().ends_with('/') { - Path::new(to_create.to_str().unwrap().trim_end_matches('/')) - } else { - to_create + let to_create_owned; + let to_create = match uucore::os_str_as_bytes(to_create.as_os_str()) { + Ok(path_bytes) if path_bytes.ends_with(b"/") => { + let mut trimmed_bytes = path_bytes; + while trimmed_bytes.ends_with(b"/") { + trimmed_bytes = &trimmed_bytes[..trimmed_bytes.len() - 1]; + } + let trimmed_os_str = std::ffi::OsStr::from_bytes(trimmed_bytes); + to_create_owned = PathBuf::from(trimmed_os_str); + to_create_owned.as_path() + } + _ => to_create, }; if !to_create.exists() { @@ -835,7 +845,7 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { /// fn strip_file(to: &Path, b: &Behavior) -> UResult<()> { // Check if the filename starts with a hyphen and adjust the path - let to_str = to.as_os_str().to_str().unwrap_or_default(); + let to_str = to.to_string_lossy(); let to = if to_str.starts_with('-') { let mut new_path = PathBuf::from("."); new_path.push(to); @@ -1085,7 +1095,7 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool { } // Check if the contents of the source and destination files differ. - if !diff(from.to_str().unwrap(), to.to_str().unwrap()) { + if !diff(&from.to_string_lossy(), &to.to_string_lossy()) { return true; } diff --git a/src/uu/join/src/join.rs b/src/uu/join/src/join.rs index dd132350661..4dc1dd146ec 100644 --- a/src/uu/join/src/join.rs +++ b/src/uu/join/src/join.rs @@ -413,7 +413,7 @@ impl Line { struct State<'a> { key: usize, - file_name: &'a str, + file_name: &'a OsString, file_num: FileNum, print_unpaired: bool, lines: Split>, @@ -427,7 +427,7 @@ struct State<'a> { impl<'a> State<'a> { fn new( file_num: FileNum, - name: &'a str, + name: &'a OsString, stdin: &'a Stdin, key: usize, line_ending: LineEnding, @@ -436,7 +436,8 @@ impl<'a> State<'a> { let file_buf = if name == "-" { Box::new(stdin.lock()) as Box } else { - let file = File::open(name).map_err_context(|| format!("{}", name.maybe_quote()))?; + let file = File::open(name) + .map_err_context(|| format!("{}", name.to_string_lossy().maybe_quote()))?; Box::new(BufReader::new(file)) as Box }; @@ -639,7 +640,7 @@ impl<'a> State<'a> { && (input.check_order == CheckOrder::Enabled || (self.has_unpaired && !self.has_failed)) { - let err_msg = translate!("join-error-not-sorted", "file" => self.file_name.maybe_quote(), "line_num" => self.line_num, "content" => String::from_utf8_lossy(&line.string)); + let err_msg = translate!("join-error-not-sorted", "file" => self.file_name.to_string_lossy().maybe_quote(), "line_num" => self.line_num, "content" => String::from_utf8_lossy(&line.string)); // This is fatal if the check is enabled. if input.check_order == CheckOrder::Enabled { return Err(JoinError::UnorderedInput(err_msg)); @@ -826,8 +827,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let settings = parse_settings(&matches)?; - let file1 = matches.get_one::("file1").unwrap(); - let file2 = matches.get_one::("file2").unwrap(); + let file1 = matches.get_one::("file1").unwrap(); + let file2 = matches.get_one::("file2").unwrap(); if file1 == "-" && file2 == "-" { return Err(USimpleError::new( @@ -951,6 +952,7 @@ pub fn uu_app() -> Command { .required(true) .value_name("FILE1") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .hide(true), ) .arg( @@ -958,11 +960,17 @@ pub fn uu_app() -> Command { .required(true) .value_name("FILE2") .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)) .hide(true), ) } -fn exec(file1: &str, file2: &str, settings: Settings, sep: Sep) -> UResult<()> { +fn exec( + file1: &OsString, + file2: &OsString, + settings: Settings, + sep: Sep, +) -> UResult<()> { let stdin = stdin(); let mut state1 = State::new( diff --git a/src/uu/ln/src/ln.rs b/src/uu/ln/src/ln.rs index 8b21bd09372..0557dd4f319 100644 --- a/src/uu/ln/src/ln.rs +++ b/src/uu/ln/src/ln.rs @@ -30,11 +30,11 @@ use uucore::fs::{MissingHandling, ResolveMode, canonicalize}; pub struct Settings { overwrite: OverwriteMode, backup: BackupMode, - suffix: String, + suffix: OsString, symbolic: bool, relative: bool, logical: bool, - target_dir: Option, + target_dir: Option, no_target_dir: bool, no_dereference: bool, verbose: bool, @@ -61,7 +61,7 @@ enum LnError { #[error("{}", translate!("ln-error-missing-destination", "operand" => _0.quote()))] MissingDestination(PathBuf), - #[error("{}", translate!("ln-error-extra-operand", "operand" => format!("{_0:?}").trim_matches('"'), "program" => _1.clone()))] + #[error("{}", translate!("ln-error-extra-operand", "operand" => _0.to_string_lossy(), "program" => _1.clone()))] ExtraOperand(OsString, String), } @@ -102,7 +102,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { /* the list of files */ let paths: Vec = matches - .get_many::(ARG_FILES) + .get_many::(ARG_FILES) .unwrap() .map(PathBuf::from) .collect(); @@ -126,13 +126,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let settings = Settings { overwrite: overwrite_mode, backup: backup_mode, - suffix: backup_suffix, + suffix: OsString::from(backup_suffix), symbolic, logical, relative: matches.get_flag(options::RELATIVE), target_dir: matches - .get_one::(options::TARGET_DIRECTORY) - .map(String::from), + .get_one::(options::TARGET_DIRECTORY) + .map(PathBuf::from), no_target_dir: matches.get_flag(options::NO_TARGET_DIRECTORY), no_dereference: matches.get_flag(options::NO_DEREFERENCE), verbose: matches.get_flag(options::VERBOSE), @@ -210,6 +210,7 @@ pub fn uu_app() -> Command { .help(translate!("ln-help-target-directory")) .value_name("DIRECTORY") .value_hint(clap::ValueHint::DirPath) + .value_parser(clap::value_parser!(OsString)) .conflicts_with(options::NO_TARGET_DIRECTORY), ) .arg( @@ -238,6 +239,7 @@ pub fn uu_app() -> Command { Arg::new(ARG_FILES) .action(ArgAction::Append) .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)) .required(true) .num_args(1..), ) @@ -245,9 +247,9 @@ pub fn uu_app() -> Command { fn exec(files: &[PathBuf], settings: &Settings) -> UResult<()> { // Handle cases where we create links in a directory first. - if let Some(ref name) = settings.target_dir { + if let Some(ref target_path) = settings.target_dir { // 4th form: a directory is specified by -t. - return link_files_in_dir(files, &PathBuf::from(name), settings); + return link_files_in_dir(files, target_path, settings); } if !settings.no_target_dir { if files.len() == 1 { @@ -445,16 +447,16 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> { Ok(()) } -fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf { - let mut p = path.as_os_str().to_str().unwrap().to_owned(); - p.push_str(suffix); - PathBuf::from(p) +fn simple_backup_path(path: &Path, suffix: &OsString) -> PathBuf { + let mut file_name = path.file_name().unwrap_or_default().to_os_string(); + file_name.push(suffix); + path.with_file_name(file_name) } fn numbered_backup_path(path: &Path) -> PathBuf { let mut i: u64 = 1; loop { - let new_path = simple_backup_path(path, &format!(".~{i}~")); + let new_path = simple_backup_path(path, &OsString::from(format!(".~{i}~"))); if !new_path.exists() { return new_path; } @@ -462,8 +464,8 @@ fn numbered_backup_path(path: &Path) -> PathBuf { } } -fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf { - let test_path = simple_backup_path(path, ".~1~"); +fn existing_backup_path(path: &Path, suffix: &OsString) -> PathBuf { + let test_path = simple_backup_path(path, &OsString::from(".~1~")); if test_path.exists() { return numbered_backup_path(path); } diff --git a/src/uu/mktemp/src/mktemp.rs b/src/uu/mktemp/src/mktemp.rs index 23d22d361e0..ceb8605bfb7 100644 --- a/src/uu/mktemp/src/mktemp.rs +++ b/src/uu/mktemp/src/mktemp.rs @@ -13,7 +13,7 @@ use uucore::format_usage; use uucore::translate; use std::env; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; use std::io::ErrorKind; use std::iter; use std::path::{MAIN_SEPARATOR, Path, PathBuf}; @@ -105,7 +105,7 @@ pub struct Options { pub treat_as_template: bool, /// The template to use for the name of the temporary file. - pub template: String, + pub template: OsString, } impl Options { @@ -123,12 +123,12 @@ impl Options { .ok() .map_or_else(env::temp_dir, PathBuf::from), }); - let (tmpdir, template) = match matches.get_one::(ARG_TEMPLATE) { + let (tmpdir, template) = match matches.get_one::(ARG_TEMPLATE) { // If no template argument is given, `--tmpdir` is implied. None => { let tmpdir = Some(tmpdir.unwrap_or_else(env::temp_dir)); let template = DEFAULT_TEMPLATE; - (tmpdir, template.to_string()) + (tmpdir, OsString::from(template)) } Some(template) => { let tmpdir = if env::var(TMPDIR_ENV_VAR).is_ok() && matches.get_flag(OPT_T) { @@ -142,7 +142,7 @@ impl Options { } else { None }; - (tmpdir, template.to_string()) + (tmpdir, template.clone()) } }; Self { @@ -200,23 +200,30 @@ fn find_last_contiguous_block_of_xs(s: &str) -> Option<(usize, usize)> { impl Params { fn from(options: Options) -> Result { + // Convert OsString template to string for processing + let Some(template_str) = options.template.to_str() else { + // For non-UTF-8 templates, return an error + return Err(MkTempError::InvalidTemplate( + options.template.to_string_lossy().into_owned(), + )); + }; + // The template argument must end in 'X' if a suffix option is given. - if options.suffix.is_some() && !options.template.ends_with('X') { - return Err(MkTempError::MustEndInX(options.template)); + if options.suffix.is_some() && !template_str.ends_with('X') { + return Err(MkTempError::MustEndInX(template_str.to_string())); } // Get the start and end indices of the randomized part of the template. // // For example, if the template is "abcXXXXyz", then `i` is 3 and `j` is 7. - let Some((i, j)) = find_last_contiguous_block_of_xs(&options.template) else { + let Some((i, j)) = find_last_contiguous_block_of_xs(template_str) else { let s = match options.suffix { // If a suffix is specified, the error message includes the template without the suffix. - Some(_) => options - .template + Some(_) => template_str .chars() - .take(options.template.len()) + .take(template_str.len()) .collect::(), - None => options.template, + None => template_str.to_string(), }; return Err(MkTempError::TooFewXs(s)); }; @@ -227,35 +234,36 @@ impl Params { // then `prefix` is "a/b/c/d". let tmpdir = options.tmpdir; let prefix_from_option = tmpdir.clone().unwrap_or_default(); - let prefix_from_template = &options.template[..i]; - let prefix = Path::new(&prefix_from_option) - .join(prefix_from_template) - .display() - .to_string(); + let prefix_from_template = &template_str[..i]; + let prefix_path = Path::new(&prefix_from_option).join(prefix_from_template); if options.treat_as_template && prefix_from_template.contains(MAIN_SEPARATOR) { - return Err(MkTempError::PrefixContainsDirSeparator(options.template)); + return Err(MkTempError::PrefixContainsDirSeparator( + template_str.to_string(), + )); } if tmpdir.is_some() && Path::new(prefix_from_template).is_absolute() { - return Err(MkTempError::InvalidTemplate(options.template)); + return Err(MkTempError::InvalidTemplate(template_str.to_string())); } // Split the parent directory from the file part of the prefix. // - // For example, if `prefix` is "a/b/c/d", then `directory` is - // "a/b/c" is `prefix` gets reassigned to "d". - let (directory, prefix) = if prefix.ends_with(MAIN_SEPARATOR) { - (prefix, String::new()) - } else { - let path = Path::new(&prefix); - let directory = match path.parent() { - None => String::new(), - Some(d) => d.display().to_string(), - }; - let prefix = match path.file_name() { - None => String::new(), - Some(f) => f.to_str().unwrap().to_string(), - }; - (directory, prefix) + // For example, if `prefix_path` is "a/b/c/d", then `directory` is + // "a/b/c" and `prefix` gets reassigned to "d". + let (directory, prefix) = { + let prefix_str = prefix_path.to_string_lossy(); + if prefix_str.ends_with(MAIN_SEPARATOR) { + (prefix_path, String::new()) + } else { + let directory = match prefix_path.parent() { + None => PathBuf::new(), + Some(d) => d.to_path_buf(), + }; + let prefix = match prefix_path.file_name() { + None => String::new(), + Some(f) => f.to_str().unwrap().to_string(), + }; + (directory, prefix) + } }; // Combine the suffix from the template with the suffix given as an option. @@ -263,7 +271,7 @@ impl Params { // For example, if the suffix command-line argument is ".txt" and // the template is "XXXabc", then `suffix` is "abc.txt". let suffix_from_option = options.suffix.unwrap_or_default(); - let suffix_from_template = &options.template[j..]; + let suffix_from_template = &template_str[j..]; let suffix = format!("{suffix_from_template}{suffix_from_option}"); if suffix.contains(MAIN_SEPARATOR) { return Err(MkTempError::SuffixContainsDirSeparator(suffix)); @@ -276,7 +284,7 @@ impl Params { let num_rand_chars = j - i; Ok(Self { - directory: directory.into(), + directory, prefix, num_rand_chars, suffix, @@ -360,7 +368,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // If POSIXLY_CORRECT was set, template MUST be the last argument. if matches.contains_id(ARG_TEMPLATE) { // Template argument was provided, check if was the last one. - if args.last().unwrap() != OsStr::new(&options.template) { + if args.last().unwrap() != &options.template { return Err(Box::new(MkTempError::TooManyTemplates)); } } @@ -457,7 +465,11 @@ pub fn uu_app() -> Command { .help(translate!("mktemp-help-t")) .action(ArgAction::SetTrue), ) - .arg(Arg::new(ARG_TEMPLATE).num_args(..=1)) + .arg( + Arg::new(ARG_TEMPLATE) + .num_args(..=1) + .value_parser(clap::value_parser!(OsString)), + ) } fn dry_exec(tmpdir: &Path, prefix: &str, rand: usize, suffix: &str) -> UResult { diff --git a/src/uu/more/src/more.rs b/src/uu/more/src/more.rs index 8aa6b772920..262dc9940f4 100644 --- a/src/uu/more/src/more.rs +++ b/src/uu/more/src/more.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. use std::{ + ffi::OsString, fs::File, io::{BufRead, BufReader, Stdin, Stdout, Write, stdin, stdout}, panic::set_hook, @@ -154,12 +155,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { })); let matches = uu_app().get_matches_from_localized(args); let mut options = Options::from(&matches); - if let Some(files) = matches.get_many::(options::FILES) { + if let Some(files) = matches.get_many::(options::FILES) { let length = files.len(); - let mut files_iter = files.map(|s| s.as_str()).peekable(); - while let (Some(file), next_file) = (files_iter.next(), files_iter.peek()) { - let file = Path::new(file); + let mut files_iter = files.peekable(); + while let (Some(file_os), next_file) = (files_iter.next(), files_iter.peek()) { + let file = Path::new(file_os); if file.is_dir() { show!(UUsageError::new( 0, @@ -188,11 +189,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } Ok(opened_file) => opened_file, }; + let next_file_str = next_file.map(|f| f.to_string_lossy().into_owned()); more( InputType::File(BufReader::new(opened_file)), length > 1, - file.to_str(), - next_file.copied(), + Some(&file.to_string_lossy()), + next_file_str.as_deref(), &mut options, )?; } @@ -311,7 +313,8 @@ pub fn uu_app() -> Command { .required(false) .action(ArgAction::Append) .help(translate!("more-help-files")) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/src/uu/nl/src/nl.rs b/src/uu/nl/src/nl.rs index 890de1e868f..f63be4022d0 100644 --- a/src/uu/nl/src/nl.rs +++ b/src/uu/nl/src/nl.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs::File; use std::io::{BufRead, BufReader, Read, stdin}; use std::path::Path; @@ -195,9 +196,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { )); } - let files: Vec = match matches.get_many::(options::FILE) { + let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), - None => vec!["-".to_owned()], + None => vec![OsString::from("-")], }; let mut stats = Stats::new(settings.starting_line_number); @@ -216,7 +217,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { ); set_exit_code(1); } else { - let reader = File::open(path).map_err_context(|| file.to_string())?; + let reader = + File::open(path).map_err_context(|| file.to_string_lossy().to_string())?; let mut buffer = BufReader::new(reader); nl(&mut buffer, &mut stats, &settings)?; } @@ -245,7 +247,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::BODY_NUMBERING) diff --git a/src/uu/paste/src/paste.rs b/src/uu/paste/src/paste.rs index 82a03f93b8d..540938ccda2 100644 --- a/src/uu/paste/src/paste.rs +++ b/src/uu/paste/src/paste.rs @@ -5,9 +5,11 @@ use clap::{Arg, ArgAction, Command}; use std::cell::{OnceCell, RefCell}; +use std::ffi::OsString; use std::fs::File; use std::io::{BufRead, BufReader, Stdin, Write, stdin, stdout}; use std::iter::Cycle; +use std::path::Path; use std::rc::Rc; use std::slice::Iter; use uucore::LocalizedCommand; @@ -30,7 +32,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let serial = matches.get_flag(options::SERIAL); let delimiters = matches.get_one::(options::DELIMITER).unwrap(); let files = matches - .get_many::(options::FILE) + .get_many::(options::FILE) .unwrap() .cloned() .collect(); @@ -67,7 +69,8 @@ pub fn uu_app() -> Command { .value_name("FILE") .action(ArgAction::Append) .default_value("-") - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::ZERO_TERMINATED) @@ -80,7 +83,7 @@ pub fn uu_app() -> Command { #[allow(clippy::cognitive_complexity)] fn paste( - filenames: Vec, + filenames: Vec, serial: bool, delimiters: &str, line_ending: LineEnding, @@ -92,17 +95,16 @@ fn paste( let mut input_source_vec = Vec::with_capacity(filenames.len()); for filename in filenames { - let input_source = match filename.as_str() { - "-" => InputSource::StandardInput( + let input_source = if filename == "-" { + InputSource::StandardInput( stdin_once_cell .get_or_init(|| Rc::new(RefCell::new(stdin()))) .clone(), - ), - st => { - let file = File::open(st)?; - - InputSource::File(BufReader::new(file)) - } + ) + } else { + let path = Path::new(&filename); + let file = File::open(path)?; + InputSource::File(BufReader::new(file)) }; input_source_vec.push(input_source); diff --git a/src/uu/pathchk/src/pathchk.rs b/src/uu/pathchk/src/pathchk.rs index c46ed39ae79..e73ac2d7c12 100644 --- a/src/uu/pathchk/src/pathchk.rs +++ b/src/uu/pathchk/src/pathchk.rs @@ -6,6 +6,7 @@ // spell-checker:ignore (ToDO) lstat use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs; use std::io::{ErrorKind, Write}; use uucore::LocalizedCommand; @@ -53,7 +54,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; // take necessary actions - let paths = matches.get_many::(options::PATH); + let paths = matches.get_many::(options::PATH); if paths.is_none() { return Err(UUsageError::new( 1, @@ -65,8 +66,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // FIXME: TCS, seems inefficient and overly verbose (?) let mut res = true; for p in paths.unwrap() { + let path_str = p.to_string_lossy(); let mut path = Vec::new(); - for path_segment in p.split('/') { + for path_segment in path_str.split('/') { path.push(path_segment.to_string()); } res &= check_path(&mode, &path); @@ -108,7 +110,8 @@ pub fn uu_app() -> Command { Arg::new(options::PATH) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::AnyPath), + .value_hint(clap::ValueHint::AnyPath) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/src/uu/ptx/src/ptx.rs b/src/uu/ptx/src/ptx.rs index b8352e06fa0..a5696b96b6b 100644 --- a/src/uu/ptx/src/ptx.rs +++ b/src/uu/ptx/src/ptx.rs @@ -7,10 +7,12 @@ use std::cmp; use std::collections::{BTreeSet, HashMap, HashSet}; +use std::ffi::{OsStr, OsString}; use std::fmt::Write as FmtWrite; use std::fs::File; use std::io::{BufRead, BufReader, BufWriter, Read, Write, stdin, stdout}; use std::num::ParseIntError; +use std::path::Path; use clap::{Arg, ArgAction, Command}; use regex::Regex; @@ -66,13 +68,12 @@ fn read_word_filter_file( option: &str, ) -> std::io::Result> { let filename = matches - .get_one::(option) - .expect("parsing options failed!") - .to_string(); + .get_one::(option) + .expect("parsing options failed!"); let reader: BufReader> = BufReader::new(if filename == "-" { Box::new(stdin()) } else { - let file = File::open(filename)?; + let file = File::open(Path::new(filename))?; Box::new(file) }); let mut words: HashSet = HashSet::new(); @@ -88,12 +89,12 @@ fn read_char_filter_file( option: &str, ) -> std::io::Result> { let filename = matches - .get_one::(option) + .get_one::(option) .expect("parsing options failed!"); let mut reader: Box = if filename == "-" { Box::new(stdin()) } else { - let file = File::open(filename)?; + let file = File::open(Path::new(filename))?; Box::new(file) }; let mut buffer = String::new(); @@ -191,7 +192,7 @@ struct WordRef { local_line_nr: usize, position: usize, position_end: usize, - filename: String, + filename: OsString, } #[derive(Debug, Error)] @@ -273,16 +274,16 @@ struct FileContent { offset: usize, } -type FileMap = HashMap; +type FileMap = HashMap; -fn read_input(input_files: &[String]) -> std::io::Result { +fn read_input(input_files: &[OsString]) -> std::io::Result { let mut file_map: FileMap = HashMap::new(); let mut offset: usize = 0; for filename in input_files { let reader: BufReader> = BufReader::new(if filename == "-" { Box::new(stdin()) } else { - let file = File::open(filename)?; + let file = File::open(Path::new(filename))?; Box::new(file) }); let lines: Vec = reader.lines().collect::>>()?; @@ -292,7 +293,7 @@ fn read_input(input_files: &[String]) -> std::io::Result { let chars_lines: Vec> = lines.iter().map(|x| x.chars().collect()).collect(); let size = lines.len(); file_map.insert( - filename.to_owned(), + filename.clone(), FileContent { lines, chars_lines, @@ -646,21 +647,22 @@ fn write_traditional_output( config: &Config, file_map: &FileMap, words: &BTreeSet, - output_filename: &str, + output_filename: &OsStr, ) -> UResult<()> { - let mut writer: BufWriter> = BufWriter::new(if output_filename == "-" { - Box::new(stdout()) - } else { - let file = File::create(output_filename) - .map_err_context(|| output_filename.maybe_quote().to_string())?; - Box::new(file) - }); + let mut writer: BufWriter> = + BufWriter::new(if output_filename == OsStr::new("-") { + Box::new(stdout()) + } else { + let file = File::create(output_filename) + .map_err_context(|| output_filename.to_string_lossy().quote().to_string())?; + Box::new(file) + }); let context_reg = Regex::new(&config.context_regex).unwrap(); for word_ref in words { let file_map_value: &FileContent = file_map - .get(&(word_ref.filename)) + .get(&word_ref.filename) .expect("Missing file in file map"); let FileContent { ref lines, @@ -733,10 +735,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let config = get_config(&matches)?; let input_files; - let output_file; + let output_file: OsString; let mut files = matches - .get_many::(options::FILE) + .get_many::(options::FILE) .into_iter() .flatten() .cloned(); @@ -745,18 +747,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { input_files = { let mut files = files.collect::>(); if files.is_empty() { - files.push("-".to_string()); + files.push(OsString::from("-")); } files }; - output_file = "-".to_string(); + output_file = OsString::from("-"); } else { - input_files = vec![files.next().unwrap_or("-".to_string())]; - output_file = files.next().unwrap_or("-".to_string()); + input_files = vec![files.next().unwrap_or(OsString::from("-"))]; + output_file = files.next().unwrap_or(OsString::from("-")); if let Some(file) = files.next() { return Err(UUsageError::new( 1, - translate!("ptx-error-extra-operand", "operand" => file.quote()), + translate!("ptx-error-extra-operand", "operand" => file.to_string_lossy().quote()), )); } } @@ -778,7 +780,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::AUTO_REFERENCE) @@ -856,7 +859,8 @@ pub fn uu_app() -> Command { .long(options::BREAK_FILE) .help(translate!("ptx-help-break-file")) .value_name("FILE") - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::IGNORE_CASE) @@ -878,7 +882,8 @@ pub fn uu_app() -> Command { .long(options::IGNORE_FILE) .help(translate!("ptx-help-ignore-file")) .value_name("FILE") - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::ONLY_FILE) @@ -886,7 +891,8 @@ pub fn uu_app() -> Command { .long(options::ONLY_FILE) .help(translate!("ptx-help-only-file")) .value_name("FILE") - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::REFERENCES) diff --git a/src/uu/readlink/src/readlink.rs b/src/uu/readlink/src/readlink.rs index f634c970a03..dd2c785b41b 100644 --- a/src/uu/readlink/src/readlink.rs +++ b/src/uu/readlink/src/readlink.rs @@ -6,11 +6,11 @@ // spell-checker:ignore (ToDO) errno use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs; use std::io::{Write, stdout}; use std::path::{Path, PathBuf}; use uucore::LocalizedCommand; -use uucore::display::Quotable; use uucore::error::{FromIo, UResult, USimpleError, UUsageError}; use uucore::fs::{MissingHandling, ResolveMode, canonicalize}; use uucore::line_ending::LineEnding; @@ -54,9 +54,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { MissingHandling::Normal }; - let files: Vec = matches - .get_many::(ARG_FILES) - .map(|v| v.map(ToString::to_string).collect()) + let files: Vec = matches + .get_many::(ARG_FILES) + .map(|v| v.map(PathBuf::from).collect()) .unwrap_or_default(); if files.is_empty() { @@ -77,12 +77,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Some(LineEnding::from_zero_flag(use_zero)) }; - for f in &files { - let p = PathBuf::from(f); + for p in &files { let path_result = if res_mode == ResolveMode::None { - fs::read_link(&p) + fs::read_link(p) } else { - canonicalize(&p, can_mode, res_mode) + canonicalize(p, can_mode, res_mode) }; match path_result { @@ -93,7 +92,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { return if verbose { Err(USimpleError::new( 1, - err.map_err_context(move || f.maybe_quote().to_string()) + err.map_err_context(move || p.to_string_lossy().to_string()) .to_string(), )) } else { @@ -171,13 +170,13 @@ pub fn uu_app() -> Command { .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::AnyPath), ) } fn show(path: &Path, line_ending: Option) -> std::io::Result<()> { - let path = path.to_str().unwrap(); - print!("{path}"); + uucore::display::print_verbatim(path)?; if let Some(line_ending) = line_ending { print!("{line_ending}"); } diff --git a/src/uu/realpath/locales/en-US.ftl b/src/uu/realpath/locales/en-US.ftl index d0fdf586b80..9da567cdc4e 100644 --- a/src/uu/realpath/locales/en-US.ftl +++ b/src/uu/realpath/locales/en-US.ftl @@ -11,3 +11,6 @@ realpath-help-canonicalize-existing = canonicalize by following every symlink in realpath-help-canonicalize-missing = canonicalize by following every symlink in every component of the given name recursively, without requirements on components existence realpath-help-relative-to = print the resolved path relative to DIR realpath-help-relative-base = print absolute paths unless paths below DIR + +# Error messages +realpath-invalid-empty-operand = invalid operand: empty string diff --git a/src/uu/realpath/locales/fr-FR.ftl b/src/uu/realpath/locales/fr-FR.ftl index f1f94d14bdc..16b522ee9d5 100644 --- a/src/uu/realpath/locales/fr-FR.ftl +++ b/src/uu/realpath/locales/fr-FR.ftl @@ -11,3 +11,6 @@ realpath-help-canonicalize-existing = canonicaliser en suivant récursivement ch realpath-help-canonicalize-missing = canonicaliser en suivant récursivement chaque lien symbolique dans chaque composant du nom donné, sans exigences sur l'existence des composants realpath-help-relative-to = afficher le chemin résolu relativement à RÉP realpath-help-relative-base = afficher les chemins absolus sauf pour les chemins sous RÉP + +# Messages d'erreur +realpath-invalid-empty-operand = opérande invalide : chaîne vide diff --git a/src/uu/realpath/src/realpath.rs b/src/uu/realpath/src/realpath.rs index 1b4ea602842..2f1efacc612 100644 --- a/src/uu/realpath/src/realpath.rs +++ b/src/uu/realpath/src/realpath.rs @@ -5,8 +5,12 @@ // spell-checker:ignore (ToDO) retcode -use clap::{Arg, ArgAction, ArgMatches, Command, builder::NonEmptyStringValueParser}; +use clap::{ + Arg, ArgAction, ArgMatches, Command, + builder::{TypedValueParser, ValueParserFactory}, +}; use std::{ + ffi::{OsStr, OsString}, io::{Write, stdout}, path::{Path, PathBuf}, }; @@ -33,6 +37,39 @@ const OPT_RELATIVE_BASE: &str = "relative-base"; const ARG_FILES: &str = "files"; +/// Custom parser that validates `OsString` is not empty +#[derive(Clone, Debug)] +struct NonEmptyOsStringParser; + +impl TypedValueParser for NonEmptyOsStringParser { + type Value = OsString; + + fn parse_ref( + &self, + _cmd: &Command, + _arg: Option<&Arg>, + value: &OsStr, + ) -> Result { + if value.is_empty() { + let mut err = clap::Error::new(clap::error::ErrorKind::ValueValidation); + err.insert( + clap::error::ContextKind::Custom, + clap::error::ContextValue::String(translate!("realpath-invalid-empty-operand")), + ); + return Err(err); + } + Ok(value.to_os_string()) + } +} + +impl ValueParserFactory for NonEmptyOsStringParser { + type Parser = Self; + + fn value_parser() -> Self::Parser { + Self + } +} + #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().try_get_matches_from(args).with_exit_code(1)?; @@ -40,7 +77,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { /* the list of files */ let paths: Vec = matches - .get_many::(ARG_FILES) + .get_many::(ARG_FILES) .unwrap() .map(PathBuf::from) .collect(); @@ -145,21 +182,21 @@ pub fn uu_app() -> Command { Arg::new(OPT_RELATIVE_TO) .long(OPT_RELATIVE_TO) .value_name("DIR") - .value_parser(NonEmptyStringValueParser::new()) + .value_parser(NonEmptyOsStringParser) .help(translate!("realpath-help-relative-to")), ) .arg( Arg::new(OPT_RELATIVE_BASE) .long(OPT_RELATIVE_BASE) .value_name("DIR") - .value_parser(NonEmptyStringValueParser::new()) + .value_parser(NonEmptyOsStringParser) .help(translate!("realpath-help-relative-base")), ) .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) .required(true) - .value_parser(NonEmptyStringValueParser::new()) + .value_parser(NonEmptyOsStringParser) .value_hint(clap::ValueHint::AnyPath), ) } @@ -174,10 +211,10 @@ fn prepare_relative_options( resolve_mode: ResolveMode, ) -> UResult<(Option, Option)> { let relative_to = matches - .get_one::(OPT_RELATIVE_TO) + .get_one::(OPT_RELATIVE_TO) .map(PathBuf::from); let relative_base = matches - .get_one::(OPT_RELATIVE_BASE) + .get_one::(OPT_RELATIVE_BASE) .map(PathBuf::from); let relative_to = canonicalize_relative_option(relative_to, can_mode, resolve_mode)?; let relative_base = canonicalize_relative_option(relative_base, can_mode, resolve_mode)?; diff --git a/src/uu/rm/src/rm.rs b/src/uu/rm/src/rm.rs index 9e3fa9b3991..c00a9463963 100644 --- a/src/uu/rm/src/rm.rs +++ b/src/uu/rm/src/rm.rs @@ -31,17 +31,17 @@ enum RmError { #[error("{}", translate!("rm-error-missing-operand", "util_name" => uucore::execution_phrase()))] MissingOperand, #[error("{}", translate!("rm-error-cannot-remove-no-such-file", "file" => _0.quote()))] - CannotRemoveNoSuchFile(String), + CannotRemoveNoSuchFile(OsString), #[error("{}", translate!("rm-error-cannot-remove-permission-denied", "file" => _0.quote()))] - CannotRemovePermissionDenied(String), + CannotRemovePermissionDenied(OsString), #[error("{}", translate!("rm-error-cannot-remove-is-directory", "file" => _0.quote()))] - CannotRemoveIsDirectory(String), + CannotRemoveIsDirectory(OsString), #[error("{}", translate!("rm-error-dangerous-recursive-operation"))] DangerousRecursiveOperation, #[error("{}", translate!("rm-error-use-no-preserve-root"))] UseNoPreserveRoot, - #[error("{}", translate!("rm-error-refusing-to-remove-directory", "path" => _0))] - RefusingToRemoveDirectory(String), + #[error("{}", translate!("rm-error-refusing-to-remove-directory", "path" => _0.to_string_lossy()))] + RefusingToRemoveDirectory(OsString), } impl UError for RmError {} @@ -366,7 +366,7 @@ pub fn remove(files: &[&OsStr], options: &Options) -> bool { } else { show_error!( "{}", - RmError::CannotRemoveNoSuchFile(filename.to_string_lossy().to_string()) + RmError::CannotRemoveNoSuchFile(filename.to_os_string()) ); true } @@ -542,7 +542,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool { if path_is_current_or_parent_directory(path) { show_error!( "{}", - RmError::RefusingToRemoveDirectory(path.display().to_string()) + RmError::RefusingToRemoveDirectory(path.as_os_str().to_os_string()) ); return true; } @@ -559,7 +559,7 @@ fn handle_dir(path: &Path, options: &Options) -> bool { } else { show_error!( "{}", - RmError::CannotRemoveIsDirectory(path.to_string_lossy().to_string()) + RmError::CannotRemoveIsDirectory(path.as_os_str().to_os_string()) ); had_err = true; } @@ -580,7 +580,7 @@ fn remove_dir(path: &Path, options: &Options) -> bool { if !options.dir && !options.recursive { show_error!( "{}", - RmError::CannotRemoveIsDirectory(path.to_string_lossy().to_string()) + RmError::CannotRemoveIsDirectory(path.as_os_str().to_os_string()) ); return true; } @@ -621,7 +621,7 @@ fn remove_file(path: &Path, options: &Options) -> bool { // GNU compatibility (rm/fail-eacces.sh) show_error!( "{}", - RmError::CannotRemovePermissionDenied(path.to_string_lossy().to_string()) + RmError::CannotRemovePermissionDenied(path.as_os_str().to_os_string()) ); } else { show_error!("cannot remove {}: {e}", path.quote()); diff --git a/src/uu/shred/src/shred.rs b/src/uu/shred/src/shred.rs index c158678a414..b7b882c2694 100644 --- a/src/uu/shred/src/shred.rs +++ b/src/uu/shred/src/shred.rs @@ -9,6 +9,7 @@ use clap::{Arg, ArgAction, Command}; #[cfg(unix)] use libc::S_IWUSR; use rand::{Rng, SeedableRng, rngs::StdRng, seq::SliceRandom}; +use std::ffi::OsString; use std::fs::{self, File, OpenOptions}; use std::io::{self, Read, Seek, Write}; #[cfg(unix)] @@ -297,7 +298,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let zero = matches.get_flag(options::ZERO); let verbose = matches.get_flag(options::VERBOSE); - for path_str in matches.get_many::(options::FILE).unwrap() { + for path_str in matches.get_many::(options::FILE).unwrap() { show_if_err!(wipe_file( path_str, iterations, @@ -396,7 +397,8 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FILE) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) } @@ -428,7 +430,7 @@ fn pass_name(pass_type: &PassType) -> String { #[allow(clippy::too_many_arguments)] #[allow(clippy::cognitive_complexity)] fn wipe_file( - path_str: &str, + path_str: &OsString, n_passes: usize, remove_method: RemoveMethod, size: Option, @@ -605,7 +607,7 @@ fn do_pass( /// Repeatedly renames the file with strings of decreasing length (most likely all 0s) /// Return the path of the file after its last renaming or None in case of an error fn wipe_name(orig_path: &Path, verbose: bool, remove_method: RemoveMethod) -> Option { - let file_name_len = orig_path.file_name().unwrap().to_str().unwrap().len(); + let file_name_len = orig_path.file_name().unwrap().len(); let mut last_path = PathBuf::from(orig_path); @@ -657,7 +659,7 @@ fn wipe_name(orig_path: &Path, verbose: bool, remove_method: RemoveMethod) -> Op fn do_remove( path: &Path, - orig_filename: &str, + orig_filename: &OsString, verbose: bool, remove_method: RemoveMethod, ) -> Result<(), io::Error> { diff --git a/src/uu/split/src/filenames.rs b/src/uu/split/src/filenames.rs index 5d8796dd97d..5c09bf6f2aa 100644 --- a/src/uu/split/src/filenames.rs +++ b/src/uu/split/src/filenames.rs @@ -39,6 +39,7 @@ use crate::{ OPT_NUMERIC_SUFFIXES_SHORT, OPT_SUFFIX_LENGTH, }; use clap::ArgMatches; +use std::ffi::{OsStr, OsString}; use std::path::is_separator; use thiserror::Error; use uucore::display::Quotable; @@ -76,7 +77,7 @@ pub struct Suffix { length: usize, start: usize, auto_widening: bool, - additional: String, + additional: OsString, } /// An error when parsing suffix parameters from command-line arguments. @@ -219,11 +220,13 @@ impl Suffix { } let additional = matches - .get_one::(OPT_ADDITIONAL_SUFFIX) + .get_one::(OPT_ADDITIONAL_SUFFIX) .unwrap() - .to_string(); - if additional.chars().any(is_separator) { - return Err(SuffixError::ContainsSeparator(additional)); + .clone(); + if additional.to_string_lossy().chars().any(is_separator) { + return Err(SuffixError::ContainsSeparator( + additional.to_string_lossy().to_string(), + )); } let result = Self { @@ -300,14 +303,14 @@ impl Suffix { /// assert_eq!(it.next().unwrap(), "chunk_02.txt"); /// ``` pub struct FilenameIterator<'a> { - prefix: &'a str, - additional_suffix: &'a str, + prefix: &'a OsStr, + additional_suffix: &'a OsStr, number: Number, first_iteration: bool, } impl<'a> FilenameIterator<'a> { - pub fn new(prefix: &'a str, suffix: &'a Suffix) -> UResult { + pub fn new(prefix: &'a OsStr, suffix: &'a Suffix) -> UResult { let radix = suffix.stype.radix(); let number = if suffix.auto_widening { Number::DynamicWidth(DynamicWidthNumber::new(radix, suffix.start)) @@ -321,7 +324,7 @@ impl<'a> FilenameIterator<'a> { })?, ) }; - let additional_suffix = suffix.additional.as_str(); + let additional_suffix = &suffix.additional; Ok(FilenameIterator { prefix, @@ -345,7 +348,9 @@ impl Iterator for FilenameIterator<'_> { // struct parameters unchanged. Some(format!( "{}{}{}", - self.prefix, self.number, self.additional_suffix + self.prefix.to_string_lossy(), + self.number, + self.additional_suffix.to_string_lossy() )) } } @@ -364,14 +369,14 @@ mod tests { length: 2, start: 0, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_aa.txt"); assert_eq!(it.next().unwrap(), "chunk_ab.txt"); assert_eq!(it.next().unwrap(), "chunk_ac.txt"); - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.nth(26 * 26 - 1).unwrap(), "chunk_zz.txt"); assert_eq!(it.next(), None); } @@ -383,14 +388,14 @@ mod tests { length: 2, start: 0, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_00.txt"); assert_eq!(it.next().unwrap(), "chunk_01.txt"); assert_eq!(it.next().unwrap(), "chunk_02.txt"); - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.nth(10 * 10 - 1).unwrap(), "chunk_99.txt"); assert_eq!(it.next(), None); } @@ -402,14 +407,14 @@ mod tests { length: 2, start: 0, auto_widening: true, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_aa.txt"); assert_eq!(it.next().unwrap(), "chunk_ab.txt"); assert_eq!(it.next().unwrap(), "chunk_ac.txt"); - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.nth(26 * 25 - 1).unwrap(), "chunk_yz.txt"); assert_eq!(it.next().unwrap(), "chunk_zaaa.txt"); assert_eq!(it.next().unwrap(), "chunk_zaab.txt"); @@ -422,14 +427,14 @@ mod tests { length: 2, start: 0, auto_widening: true, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_00.txt"); assert_eq!(it.next().unwrap(), "chunk_01.txt"); assert_eq!(it.next().unwrap(), "chunk_02.txt"); - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.nth(10 * 9 - 1).unwrap(), "chunk_89.txt"); assert_eq!(it.next().unwrap(), "chunk_9000.txt"); assert_eq!(it.next().unwrap(), "chunk_9001.txt"); @@ -442,9 +447,9 @@ mod tests { length: 2, start: 5, auto_widening: true, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_05.txt"); assert_eq!(it.next().unwrap(), "chunk_06.txt"); assert_eq!(it.next().unwrap(), "chunk_07.txt"); @@ -457,9 +462,9 @@ mod tests { length: 2, start: 9, auto_widening: true, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_09.txt"); assert_eq!(it.next().unwrap(), "chunk_0a.txt"); assert_eq!(it.next().unwrap(), "chunk_0b.txt"); @@ -472,9 +477,9 @@ mod tests { length: 3, start: 999, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_999.txt"); assert!(it.next().is_none()); @@ -483,9 +488,9 @@ mod tests { length: 3, start: 1000, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let it = FilenameIterator::new("chunk_", &suffix); + let it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix); assert!(it.is_err()); let suffix = Suffix { @@ -493,9 +498,9 @@ mod tests { length: 3, start: 0xfff, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let mut it = FilenameIterator::new("chunk_", &suffix).unwrap(); + let mut it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix).unwrap(); assert_eq!(it.next().unwrap(), "chunk_fff.txt"); assert!(it.next().is_none()); @@ -504,9 +509,9 @@ mod tests { length: 3, start: 0x1000, auto_widening: false, - additional: ".txt".to_string(), + additional: ".txt".into(), }; - let it = FilenameIterator::new("chunk_", &suffix); + let it = FilenameIterator::new(std::ffi::OsStr::new("chunk_"), &suffix); assert!(it.is_err()); } } diff --git a/src/uu/split/src/platform/unix.rs b/src/uu/split/src/platform/unix.rs index 97cb782ba3e..d1257954d3e 100644 --- a/src/uu/split/src/platform/unix.rs +++ b/src/uu/split/src/platform/unix.rs @@ -3,6 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. use std::env; +use std::ffi::OsStr; use std::io::Write; use std::io::{BufWriter, Error, Result}; use std::path::Path; @@ -163,12 +164,12 @@ pub fn instantiate_current_writer( } } -pub fn paths_refer_to_same_file(p1: &str, p2: &str) -> bool { +pub fn paths_refer_to_same_file(p1: &OsStr, p2: &OsStr) -> bool { // We have to take symlinks and relative paths into account. let p1 = if p1 == "-" { FileInformation::from_file(&std::io::stdin()) } else { - FileInformation::from_path(Path::new(&p1), true) + FileInformation::from_path(Path::new(p1), true) }; fs::infos_refer_to_same_file(p1, FileInformation::from_path(Path::new(p2), true)) } diff --git a/src/uu/split/src/platform/windows.rs b/src/uu/split/src/platform/windows.rs index f6659ebce1e..e443a9cfb3b 100644 --- a/src/uu/split/src/platform/windows.rs +++ b/src/uu/split/src/platform/windows.rs @@ -2,6 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +use std::ffi::OsStr; use std::io::Write; use std::io::{BufWriter, Error, Result}; use std::path::Path; @@ -39,7 +40,7 @@ pub fn instantiate_current_writer( Ok(BufWriter::new(Box::new(file) as Box)) } -pub fn paths_refer_to_same_file(p1: &str, p2: &str) -> bool { +pub fn paths_refer_to_same_file(p1: &OsStr, p2: &OsStr) -> bool { // Windows doesn't support many of the unix ways of paths being equals fs::paths_refer_to_same_file(Path::new(p1), Path::new(p2), true) } diff --git a/src/uu/split/src/split.rs b/src/uu/split/src/split.rs index 70c47578289..7d6cdbd94c5 100644 --- a/src/uu/split/src/split.rs +++ b/src/uu/split/src/split.rs @@ -274,6 +274,7 @@ pub fn uu_app() -> Command { .allow_hyphen_values(true) .value_name("SUFFIX") .default_value("") + .value_parser(clap::value_parser!(OsString)) .help(translate!("split-help-additional-suffix")), ) .arg( @@ -375,9 +376,14 @@ pub fn uu_app() -> Command { .arg( Arg::new(ARG_INPUT) .default_value("-") - .value_hint(ValueHint::FilePath), + .value_hint(ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), + ) + .arg( + Arg::new(ARG_PREFIX) + .default_value("x") + .value_parser(clap::value_parser!(OsString)), ) - .arg(Arg::new(ARG_PREFIX).default_value("x")) } /// Parameters that control how a file gets split. @@ -385,9 +391,9 @@ pub fn uu_app() -> Command { /// You can convert an [`ArgMatches`] instance into a [`Settings`] /// instance by calling [`Settings::from`]. struct Settings { - prefix: String, + prefix: OsString, suffix: Suffix, - input: String, + input: OsString, /// When supplied, a shell command to output to instead of xaa, xab … filter: Option, strategy: Strategy, @@ -489,9 +495,9 @@ impl Settings { }; let result = Self { - prefix: matches.get_one::(ARG_PREFIX).unwrap().clone(), + prefix: matches.get_one::(ARG_PREFIX).unwrap().clone(), suffix, - input: matches.get_one::(ARG_INPUT).unwrap().clone(), + input: matches.get_one::(ARG_INPUT).unwrap().clone(), filter: matches.get_one::(OPT_FILTER).cloned(), strategy, verbose: matches.value_source(OPT_VERBOSE) == Some(ValueSource::CommandLine), @@ -529,7 +535,7 @@ impl Settings { filename: &str, is_new: bool, ) -> io::Result>> { - if platform::paths_refer_to_same_file(&self.input, filename) { + if platform::paths_refer_to_same_file(&self.input, filename.as_ref()) { return Err(io::Error::other( translate!("split-error-would-overwrite-input", "file" => filename.quote()), )); @@ -598,7 +604,7 @@ fn custom_write_all( /// /// Note: The `buf` might end up with either partial or entire input content. fn get_input_size( - input: &String, + input: &OsString, reader: &mut R, buf: &mut Vec, io_blksize: Option, @@ -633,12 +639,12 @@ where // STDIN stream that did not fit all content into a buffer // Most likely continuous/infinite input stream Err(io::Error::other( - translate!("split-error-cannot-determine-input-size", "input" => input), + translate!("split-error-cannot-determine-input-size", "input" => input.to_string_lossy()), )) } else { // Could be that file size is larger than set read limit // Get the file size from filesystem metadata - let metadata = metadata(input)?; + let metadata = metadata(Path::new(input))?; let metadata_size = metadata.len(); if num_bytes <= metadata_size { Ok(metadata_size) @@ -658,7 +664,7 @@ where // TODO It might be possible to do more here // to address all possible file types and edge cases Err(io::Error::other( - translate!("split-error-cannot-determine-file-size", "input" => input), + translate!("split-error-cannot-determine-file-size", "input" => input.to_string_lossy()), )) } } @@ -1167,7 +1173,7 @@ where Err(error) => { return Err(USimpleError::new( 1, - translate!("split-error-cannot-read-from-input", "input" => settings.input.clone(), "error" => error), + translate!("split-error-cannot-read-from-input", "input" => settings.input.to_string_lossy(), "error" => error), )); } } @@ -1529,7 +1535,7 @@ fn split(settings: &Settings) -> UResult<()> { Box::new(stdin()) as Box } else { let r = File::open(Path::new(&settings.input)).map_err_context( - || translate!("split-error-cannot-open-for-reading", "file" => settings.input.quote()), + || translate!("split-error-cannot-open-for-reading", "file" => settings.input.to_string_lossy().quote()), )?; Box::new(r) as Box }; diff --git a/src/uu/stdbuf/src/stdbuf.rs b/src/uu/stdbuf/src/stdbuf.rs index c2c6beab888..e8721d9814f 100644 --- a/src/uu/stdbuf/src/stdbuf.rs +++ b/src/uu/stdbuf/src/stdbuf.rs @@ -6,6 +6,7 @@ // spell-checker:ignore (ToDO) tempdir dyld dylib optgrps libstdbuf use clap::{Arg, ArgAction, ArgMatches, Command}; +use std::ffi::OsString; use std::os::unix::process::ExitStatusExt; use std::path::PathBuf; use std::process; @@ -183,9 +184,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let options = ProgramOptions::try_from(&matches).map_err(|e| UUsageError::new(125, e.to_string()))?; - let mut command_values = matches.get_many::(options::COMMAND).unwrap(); + let mut command_values = matches.get_many::(options::COMMAND).unwrap(); let mut command = process::Command::new(command_values.next().unwrap()); - let command_params: Vec<&str> = command_values.map(|s| s.as_ref()).collect(); + let command_params: Vec<&OsString> = command_values.collect(); let tmp_dir = tempdir().unwrap(); let (preload_env, libstdbuf) = get_preload_env(&tmp_dir)?; @@ -269,6 +270,7 @@ pub fn uu_app() -> Command { .action(ArgAction::Append) .hide(true) .required(true) - .value_hint(clap::ValueHint::CommandName), + .value_hint(clap::ValueHint::CommandName) + .value_parser(clap::value_parser!(OsString)), ) } diff --git a/src/uu/sum/src/sum.rs b/src/uu/sum/src/sum.rs index 2366a59d376..5832df77873 100644 --- a/src/uu/sum/src/sum.rs +++ b/src/uu/sum/src/sum.rs @@ -6,6 +6,7 @@ // spell-checker:ignore (ToDO) sysv use clap::{Arg, ArgAction, Command}; +use std::ffi::OsString; use std::fs::File; use std::io::{ErrorKind, Read, Write, stdin, stdout}; use std::path::Path; @@ -67,27 +68,26 @@ fn sysv_sum(mut reader: impl Read) -> std::io::Result<(usize, u16)> { Ok((blocks_read, ret as u16)) } -fn open(name: &str) -> UResult> { - match name { - "-" => Ok(Box::new(stdin()) as Box), - _ => { - let path = &Path::new(name); - if path.is_dir() { - return Err(USimpleError::new( - 2, - translate!("sum-error-is-directory", "name" => name.maybe_quote()), - )); - } - // Silent the warning as we want to the error message - if path.metadata().is_err() { - return Err(USimpleError::new( - 2, - translate!("sum-error-no-such-file-or-directory", "name" => name.maybe_quote()), - )); - } - let f = File::open(path).map_err_context(String::new)?; - Ok(Box::new(f) as Box) +fn open(name: &OsString) -> UResult> { + if name == "-" { + Ok(Box::new(stdin()) as Box) + } else { + let path = Path::new(name); + if path.is_dir() { + return Err(USimpleError::new( + 2, + translate!("sum-error-is-directory", "name" => name.to_string_lossy().maybe_quote()), + )); + } + // Silent the warning as we want to the error message + if path.metadata().is_err() { + return Err(USimpleError::new( + 2, + translate!("sum-error-no-such-file-or-directory", "name" => name.to_string_lossy().maybe_quote()), + )); } + let f = File::open(path).map_err_context(String::new)?; + Ok(Box::new(f) as Box) } } @@ -101,9 +101,9 @@ mod options { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); - let files: Vec = match matches.get_many::(options::FILE) { + let files: Vec = match matches.get_many::(options::FILE) { Some(v) => v.cloned().collect(), - None => vec!["-".to_owned()], + None => vec![OsString::from("-")], }; let sysv = matches.get_flag(options::SYSTEM_V_COMPATIBLE); @@ -127,7 +127,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let mut stdout = stdout().lock(); if print_names { - writeln!(stdout, "{sum:0width$} {blocks:width$} {file}")?; + writeln!( + stdout, + "{sum:0width$} {blocks:width$} {}", + file.to_string_lossy() + )?; } else { writeln!(stdout, "{sum:0width$} {blocks:width$}")?; } @@ -146,7 +150,8 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .action(ArgAction::Append) .hide(true) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::BSD_COMPATIBLE) diff --git a/src/uu/tac/src/error.rs b/src/uu/tac/src/error.rs index a69a34a0d6f..133a46266a0 100644 --- a/src/uu/tac/src/error.rs +++ b/src/uu/tac/src/error.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. //! Errors returned by tac during processing of a file. +use std::ffi::OsString; use thiserror::Error; use uucore::display::Quotable; use uucore::error::UError; @@ -16,16 +17,16 @@ pub enum TacError { InvalidRegex(regex::Error), /// An argument to tac is invalid. #[error("{}", translate!("tac-error-invalid-argument", "argument" => .0.maybe_quote()))] - InvalidArgument(String), + InvalidArgument(OsString), /// The specified file is not found on the filesystem. #[error("{}", translate!("tac-error-file-not-found", "filename" => .0.quote()))] - FileNotFound(String), + FileNotFound(OsString), /// An error reading the contents of a file or stdin. /// /// The parameters are the name of the file and the underlying /// [`std::io::Error`] that caused this error. - #[error("{}", translate!("tac-error-read-error", "filename" => .0.clone(), "error" => .1))] - ReadError(String, std::io::Error), + #[error("{}", translate!("tac-error-read-error", "filename" => .0.quote(), "error" => .1))] + ReadError(OsString, std::io::Error), /// An error writing the (reversed) contents of a file or stdin. /// /// The parameter is the underlying [`std::io::Error`] that caused diff --git a/src/uu/tac/src/tac.rs b/src/uu/tac/src/tac.rs index 799e6557131..521e2c9dbca 100644 --- a/src/uu/tac/src/tac.rs +++ b/src/uu/tac/src/tac.rs @@ -9,12 +9,12 @@ mod error; use clap::{Arg, ArgAction, Command}; use memchr::memmem; use memmap2::Mmap; +use std::ffi::OsString; use std::io::{BufWriter, Read, Write, stdin, stdout}; use std::{ fs::{File, read}, path::Path, }; -use uucore::display::Quotable; use uucore::error::UError; use uucore::error::UResult; use uucore::{format_usage, show}; @@ -46,9 +46,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { raw_separator }; - let files: Vec<&str> = match matches.get_many::(options::FILE) { - Some(v) => v.map(|s| s.as_str()).collect(), - None => vec!["-"], + let files: Vec = match matches.get_many::(options::FILE) { + Some(v) => v.cloned().collect(), + None => vec![OsString::from("-")], }; tac(&files, before, regex, separator) @@ -86,6 +86,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .hide(true) .action(ArgAction::Append) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) } @@ -221,7 +222,7 @@ fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> } #[allow(clippy::cognitive_complexity)] -fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResult<()> { +fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UResult<()> { // Compile the regular expression pattern if it is provided. let maybe_pattern = if regex { match regex::bytes::Regex::new(separator) { @@ -232,7 +233,7 @@ fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResul None }; - for &filename in filenames { + for filename in filenames { let mmap; let buf; @@ -243,7 +244,7 @@ fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResul } else { let mut buf1 = Vec::new(); if let Err(e) = stdin().read_to_end(&mut buf1) { - let e: Box = TacError::ReadError("stdin".to_string(), e).into(); + let e: Box = TacError::ReadError(OsString::from("stdin"), e).into(); show!(e); continue; } @@ -253,13 +254,13 @@ fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResul } else { let path = Path::new(filename); if path.is_dir() { - let e: Box = TacError::InvalidArgument(String::from(filename)).into(); + let e: Box = TacError::InvalidArgument(filename.clone()).into(); show!(e); continue; } if path.metadata().is_err() { - let e: Box = TacError::FileNotFound(String::from(filename)).into(); + let e: Box = TacError::FileNotFound(filename.clone()).into(); show!(e); continue; } @@ -274,8 +275,7 @@ fn tac(filenames: &[&str], before: bool, regex: bool, separator: &str) -> UResul &buf } Err(e) => { - let s = format!("{}", filename.quote()); - let e: Box = TacError::ReadError(s.to_string(), e).into(); + let e: Box = TacError::ReadError(filename.clone(), e).into(); show!(e); continue; } diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 5c6a120e7af..faf848ebda6 100644 --- a/src/uu/tee/src/tee.rs +++ b/src/uu/tee/src/tee.rs @@ -6,6 +6,7 @@ // cSpell:ignore POLLERR POLLRDBAND pfds revents use clap::{Arg, ArgAction, Command, builder::PossibleValue}; +use std::ffi::OsString; use std::fs::OpenOptions; use std::io::{Error, ErrorKind, Read, Result, Write, stdin, stdout}; use std::path::PathBuf; @@ -34,7 +35,7 @@ struct Options { append: bool, ignore_interrupts: bool, ignore_pipe_errors: bool, - files: Vec, + files: Vec, output_error: Option, } @@ -77,8 +78,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let files = matches - .get_many::(options::FILE) - .map(|v| v.map(ToString::to_string).collect()) + .get_many::(options::FILE) + .map(|v| v.cloned().collect()) .unwrap_or_default(); let options = Options { @@ -127,7 +128,8 @@ pub fn uu_app() -> Command { .arg( Arg::new(options::FILE) .action(ArgAction::Append) - .value_hint(clap::ValueHint::FilePath), + .value_hint(clap::ValueHint::FilePath) + .value_parser(clap::value_parser!(OsString)), ) .arg( Arg::new(options::IGNORE_PIPE_ERRORS) @@ -252,7 +254,7 @@ fn copy(mut input: impl Read, mut output: impl Write) -> Result { /// If that error should lead to program termination, this function returns Some(Err()), /// otherwise it returns None. fn open( - name: &str, + name: &OsString, append: bool, output_error: Option<&OutputErrorMode>, ) -> Option> { @@ -266,10 +268,10 @@ fn open( match mode.write(true).create(true).open(path.as_path()) { Ok(file) => Some(Ok(NamedWriter { inner: Box::new(file), - name: name.to_owned(), + name: name.to_string_lossy().to_string(), })), Err(f) => { - show_error!("{}: {f}", name.maybe_quote()); + show_error!("{}: {f}", name.to_string_lossy().maybe_quote()); match output_error { Some(OutputErrorMode::Exit | OutputErrorMode::ExitNoPipe) => Some(Err(f)), _ => None, diff --git a/src/uu/touch/src/touch.rs b/src/uu/touch/src/touch.rs index 451b5b5604c..6994a5f70ff 100644 --- a/src/uu/touch/src/touch.rs +++ b/src/uu/touch/src/touch.rs @@ -157,20 +157,20 @@ fn is_first_filename_timestamp( reference: Option<&OsString>, date: Option<&str>, timestamp: Option<&str>, - files: &[&String], + files: &[&OsString], ) -> bool { - if timestamp.is_none() + timestamp.is_none() && reference.is_none() && date.is_none() && files.len() >= 2 // env check is last as the slowest op && matches!(std::env::var("_POSIX2_VERSION").as_deref(), Ok("199209")) - { - let s = files[0]; - all_digits(s) && (s.len() == 8 || (s.len() == 10 && (69..=99).contains(&get_year(s)))) - } else { - false - } + && files[0].to_str().is_some_and(is_timestamp) +} + +// Check if string is a valid POSIX timestamp (8 digits or 10 digits with valid year range) +fn is_timestamp(s: &str) -> bool { + all_digits(s) && (s.len() == 8 || (s.len() == 10 && (69..=99).contains(&get_year(s)))) } /// Cycle the last two characters to the beginning of the string. @@ -189,8 +189,8 @@ fn shr2(s: &str) -> String { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); - let mut filenames: Vec<&String> = matches - .get_many::(ARG_FILES) + let mut filenames: Vec<&OsString> = matches + .get_many::(ARG_FILES) .ok_or_else(|| { USimpleError::new( 1, @@ -211,10 +211,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|t| t.to_owned()); if is_first_filename_timestamp(reference, date.as_deref(), timestamp.as_deref(), &filenames) { - timestamp = if filenames[0].len() == 10 { - Some(shr2(filenames[0])) + let first_file = filenames[0].to_str().unwrap(); + timestamp = if first_file.len() == 10 { + Some(shr2(first_file)) } else { - Some(filenames[0].to_string()) + Some(first_file.to_string()) }; filenames = filenames[1..].to_vec(); } @@ -338,6 +339,7 @@ pub fn uu_app() -> Command { Arg::new(ARG_FILES) .action(ArgAction::Append) .num_args(1..) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::AnyPath), ) .group( diff --git a/src/uu/truncate/src/truncate.rs b/src/uu/truncate/src/truncate.rs index 0ad207b7ad8..a2750b72a5f 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/src/uu/tsort/src/tsort.rs b/src/uu/tsort/src/tsort.rs index 33822a47410..41a3cb79d6b 100644 --- a/src/uu/tsort/src/tsort.rs +++ b/src/uu/tsort/src/tsort.rs @@ -5,6 +5,7 @@ //spell-checker:ignore TAOCP indegree use clap::{Arg, Command}; use std::collections::{HashMap, HashSet, VecDeque}; +use std::ffi::OsString; use std::path::Path; use thiserror::Error; use uucore::display::Quotable; @@ -47,26 +48,26 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uu_app().get_matches_from_localized(args); let input = matches - .get_one::(options::FILE) + .get_one::(options::FILE) .expect("Value is required by clap"); let data = if input == "-" { let stdin = std::io::stdin(); std::io::read_to_string(stdin)? } else { - let path = Path::new(&input); + let path = Path::new(input); if path.is_dir() { - return Err(TsortError::IsDir(input.to_string()).into()); + return Err(TsortError::IsDir(input.to_string_lossy().to_string()).into()); } std::fs::read_to_string(path)? }; // Create the directed graph from pairs of tokens in the input data. - let mut g = Graph::new(input.clone()); + let mut g = Graph::new(input.to_string_lossy().to_string()); for ab in data.split_whitespace().collect::>().chunks(2) { match ab { [a, b] => g.add_edge(a, b), - _ => return Err(TsortError::NumTokensOdd(input.to_string()).into()), + _ => return Err(TsortError::NumTokensOdd(input.to_string_lossy().to_string()).into()), } } @@ -85,6 +86,7 @@ pub fn uu_app() -> Command { Arg::new(options::FILE) .default_value("-") .hide(true) + .value_parser(clap::value_parser!(OsString)) .value_hint(clap::ValueHint::FilePath), ) } diff --git a/src/uucore/src/lib/features/perms.rs b/src/uucore/src/lib/features/perms.rs index bddce9cf285..c03d0032ac9 100644 --- a/src/uucore/src/lib/features/perms.rs +++ b/src/uucore/src/lib/features/perms.rs @@ -16,6 +16,7 @@ use clap::{Arg, ArgMatches, Command}; use libc::{gid_t, uid_t}; use options::traverse; +use std::ffi::OsString; use walkdir::WalkDir; use std::ffi::CString; @@ -193,7 +194,7 @@ pub struct ChownExecutor { pub traverse_symlinks: TraverseSymlinks, pub verbosity: Verbosity, pub filter: IfFrom, - pub files: Vec, + pub files: Vec, pub recursive: bool, pub preserve_root: bool, pub dereference: bool, @@ -597,13 +598,14 @@ pub fn chown_base( .value_hint(clap::ValueHint::FilePath) .action(clap::ArgAction::Append) .required(true) - .num_args(1..), + .num_args(1..) + .value_parser(clap::value_parser!(std::ffi::OsString)), ); let matches = command.try_get_matches_from(args)?; - 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(); let preserve_root = matches.get_flag(options::preserve_root::PRESERVE); diff --git a/tests/by-util/test_base64.rs b/tests/by-util/test_base64.rs index ad0d1c2b1ed..17b46ab29e0 100644 --- a/tests/by-util/test_base64.rs +++ b/tests/by-util/test_base64.rs @@ -2,9 +2,25 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(target_os = "linux")] +use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; +#[test] +#[cfg(target_os = "linux")] +fn test_base64_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"hello world").unwrap(); + + ucmd.arg(&filename) + .succeeds() + .stdout_is("aGVsbG8gd29ybGQ=\n"); +} + #[test] fn test_encode() { let input = "hello, world!"; diff --git a/tests/by-util/test_cat.rs b/tests/by-util/test_cat.rs index c3e25c6d010..c809231c7b0 100644 --- a/tests/by-util/test_cat.rs +++ b/tests/by-util/test_cat.rs @@ -747,6 +747,30 @@ fn test_write_fast_read_error() { ucmd.arg("foo").fails().stderr_contains("Permission denied"); } +#[test] +#[cfg(target_os = "linux")] +fn test_cat_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create the actual file with some content + std::fs::write(at.plus(non_utf8_name), "Hello, non-UTF-8 world!\n").unwrap(); + + // Test that cat handles non-UTF-8 file names without crashing + let result = scene.ucmd().arg(non_utf8_name).succeeds(); + + // The result should contain the file content + let output = result.stdout_str_lossy(); + assert_eq!(output, "Hello, non-UTF-8 world!\n"); +} + #[test] #[cfg(target_os = "linux")] fn test_appending_same_input_output() { diff --git a/tests/by-util/test_chgrp.rs b/tests/by-util/test_chgrp.rs index e0085b3683b..38081f5ac00 100644 --- a/tests/by-util/test_chgrp.rs +++ b/tests/by-util/test_chgrp.rs @@ -4,6 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore (words) nosuchgroup groupname +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uucore::process::getegid; use uutests::{at_and_ucmd, new_ucmd}; #[cfg(not(target_vendor = "apple"))] @@ -599,3 +601,17 @@ fn test_numeric_group_formats() { let final_gid = at.plus("test_file").metadata().unwrap().gid(); assert_eq!(final_gid, first_group.as_raw()); } + +#[test] +#[cfg(target_os = "linux")] +fn test_chgrp_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"test content").unwrap(); + + // Get current user's primary group + let current_gid = getegid(); + + ucmd.arg(current_gid.to_string()).arg(&filename).succeeds(); +} diff --git a/tests/by-util/test_chmod.rs b/tests/by-util/test_chmod.rs index ea5d3f898a6..a93815dba68 100644 --- a/tests/by-util/test_chmod.rs +++ b/tests/by-util/test_chmod.rs @@ -1183,3 +1183,88 @@ fn test_chmod_recursive_symlink_combinations() { 0o100_600 ); } + +#[test] +#[cfg(target_os = "linux")] +fn test_chmod_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a file with non-UTF-8 name + // Using bytes that form an invalid UTF-8 sequence + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create the file using OpenOptions with the non-UTF-8 name + OpenOptions::new() + .mode(0o644) + .create(true) + .write(true) + .truncate(true) + .open(at.plus(non_utf8_name)) + .unwrap(); + + // Verify initial permissions + let initial_perms = metadata(at.plus(non_utf8_name)) + .unwrap() + .permissions() + .mode(); + assert_eq!(initial_perms & 0o777, 0o644); + + // Test chmod with the non-UTF-8 filename + scene + .ucmd() + .arg("755") + .arg(non_utf8_name) + .succeeds() + .no_stderr(); + + // Verify permissions were changed + let new_perms = metadata(at.plus(non_utf8_name)) + .unwrap() + .permissions() + .mode(); + assert_eq!(new_perms & 0o777, 0o755); + + // Test with multiple non-UTF-8 files + let non_utf8_bytes2 = b"file_\xC0\x80.dat"; + let non_utf8_name2 = OsStr::from_bytes(non_utf8_bytes2); + + OpenOptions::new() + .mode(0o666) + .create(true) + .write(true) + .truncate(true) + .open(at.plus(non_utf8_name2)) + .unwrap(); + + // Change permissions on both files at once + scene + .ucmd() + .arg("644") + .arg(non_utf8_name) + .arg(non_utf8_name2) + .succeeds() + .no_stderr(); + + // Verify both files have the new permissions + assert_eq!( + metadata(at.plus(non_utf8_name)) + .unwrap() + .permissions() + .mode() + & 0o777, + 0o644 + ); + assert_eq!( + metadata(at.plus(non_utf8_name2)) + .unwrap() + .permissions() + .mode() + & 0o777, + 0o644 + ); +} diff --git a/tests/by-util/test_csplit.rs b/tests/by-util/test_csplit.rs index ea1d9ebf991..b13d6c35d4d 100644 --- a/tests/by-util/test_csplit.rs +++ b/tests/by-util/test_csplit.rs @@ -1501,3 +1501,15 @@ fn test_stdin_no_trailing_newline() { .succeeds() .stdout_only("2\n5\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_csplit_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"line1\nline2\nline3\nline4\nline5\n").unwrap(); + + ucmd.arg(&filename).arg("3").succeeds(); +} diff --git a/tests/by-util/test_cut.rs b/tests/by-util/test_cut.rs index a2406679c42..b94a158343b 100644 --- a/tests/by-util/test_cut.rs +++ b/tests/by-util/test_cut.rs @@ -385,3 +385,28 @@ fn test_failed_write_is_reported() { .fails() .stderr_is("cut: write error: No space left on device\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_cut_non_utf8_paths() { + use std::fs::File; + use std::io::Write; + use std::os::unix::ffi::OsStrExt; + use uutests::util::TestScenario; + use uutests::util_name; + + let ts = TestScenario::new(util_name!()); + let test_dir = ts.fixtures.subdir.as_path(); + + // Create file directly with non-UTF-8 name + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + let mut file = File::create(test_dir.join(file_name)).unwrap(); + file.write_all(b"a\tb\tc\n1\t2\t3\n").unwrap(); + + // Test that cut can handle non-UTF-8 filenames + ts.ucmd() + .arg("-f1,3") + .arg(file_name) + .succeeds() + .stdout_only("a\tc\n1\t3\n"); +} diff --git a/tests/by-util/test_dircolors.rs b/tests/by-util/test_dircolors.rs index dde00a494de..dd98e5d43ce 100644 --- a/tests/by-util/test_dircolors.rs +++ b/tests/by-util/test_dircolors.rs @@ -3,10 +3,28 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore overridable colorterm +#[cfg(target_os = "linux")] +use uutests::at_and_ucmd; use uutests::new_ucmd; use dircolors::{OutputFmt, StrUtils, guess_syntax}; +#[test] +#[cfg(target_os = "linux")] +fn test_dircolors_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"NORMAL 00\n*.txt 32\n").unwrap(); + + ucmd.env("SHELL", "bash") + .arg(&filename) + .succeeds() + .stdout_contains("LS_COLORS=") + .stdout_contains("*.txt=32"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); diff --git a/tests/by-util/test_dirname.rs b/tests/by-util/test_dirname.rs index e73ff2b095e..933e882d7e7 100644 --- a/tests/by-util/test_dirname.rs +++ b/tests/by-util/test_dirname.rs @@ -64,3 +64,23 @@ fn test_pwd() { fn test_empty() { new_ucmd!().arg("").succeeds().stdout_is(".\n"); } + +#[test] +#[cfg(unix)] +fn test_dirname_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE/file.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Test that dirname handles non-UTF-8 paths without crashing + let result = new_ucmd!().arg(non_utf8_name).succeeds(); + + // Just verify it didn't crash and produced some output + // The exact output format may vary due to lossy conversion + let output = result.stdout_str_lossy(); + assert!(!output.is_empty()); + assert!(output.contains("test_")); +} diff --git a/tests/by-util/test_expand.rs b/tests/by-util/test_expand.rs index 799feb4a317..741aad36640 100644 --- a/tests/by-util/test_expand.rs +++ b/tests/by-util/test_expand.rs @@ -426,3 +426,19 @@ fn test_nonexisting_file() { .stderr_contains("expand: nonexistent: No such file or directory") .stdout_contains_line("// !note: file contains significant whitespace"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_expand_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + use uutests::at_and_ucmd; + + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"hello\tworld\ntest\tline\n").unwrap(); + + ucmd.arg(&filename) + .succeeds() + .stdout_is("hello world\ntest line\n"); +} diff --git a/tests/by-util/test_fmt.rs b/tests/by-util/test_fmt.rs index 54d827f83a8..abf2e132ce8 100644 --- a/tests/by-util/test_fmt.rs +++ b/tests/by-util/test_fmt.rs @@ -4,7 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore plass samp - +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uutests::new_ucmd; #[test] @@ -374,3 +375,16 @@ fn test_fmt_knuth_plass_line_breaking() { .succeeds() .stdout_is(expected); } + +#[test] +#[cfg(target_os = "linux")] +fn test_fmt_non_utf8_paths() { + use uutests::at_and_ucmd; + + let (at, mut ucmd) = at_and_ucmd!(); + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + + std::fs::write(at.plus(&filename), b"hello world this is a test").unwrap(); + + ucmd.arg(&filename).succeeds(); +} diff --git a/tests/by-util/test_head.rs b/tests/by-util/test_head.rs index 30f8378b93f..2acc783eaff 100644 --- a/tests/by-util/test_head.rs +++ b/tests/by-util/test_head.rs @@ -858,3 +858,27 @@ fn test_write_to_dev_full() { } } } + +#[test] +#[cfg(target_os = "linux")] +fn test_head_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + std::fs::write(at.plus(non_utf8_name), "line1\nline2\nline3\n").unwrap(); + + let result = scene.ucmd().arg(non_utf8_name).succeeds(); + + let output = result.stdout_str_lossy(); + assert!(output.contains("line1")); + assert!(output.contains("line2")); + assert!(output.contains("line3")); +} +// Test that head handles non-UTF-8 file names without crashing diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index ea3448bbb63..4d1eed3e236 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -7,6 +7,8 @@ #[cfg(not(target_os = "openbsd"))] use filetime::FileTime; use std::fs; +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use std::os::unix::fs::{MetadataExt, PermissionsExt}; #[cfg(not(windows))] use std::process::Command; @@ -2366,3 +2368,26 @@ fn test_install_compare_with_mode_bits() { ); } } + +#[test] +#[cfg(target_os = "linux")] +fn test_install_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + let source_filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + let dest_dir = "target_dir"; + + std::fs::write(at.plus(&source_filename), b"test content").unwrap(); + at.mkdir(dest_dir); + + ucmd.arg(&source_filename).arg(dest_dir).succeeds(); + + // Test with trailing slash and directory creation (-D flag) + let (at, mut ucmd) = at_and_ucmd!(); + let source_file = "source.txt"; + let mut target_path = std::ffi::OsString::from_vec(vec![0xFF, 0xFE, b'd', b'i', b'r']); + target_path.push("/target.txt"); + + at.touch(source_file); + + ucmd.arg("-D").arg(source_file).arg(&target_path).succeeds(); +} diff --git a/tests/by-util/test_join.rs b/tests/by-util/test_join.rs index e9924eea9ae..8a239b965f9 100644 --- a/tests/by-util/test_join.rs +++ b/tests/by-util/test_join.rs @@ -533,3 +533,36 @@ fn test_full() { .fails() .stderr_contains("No space left on device"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_join_non_utf8_paths() { + use std::fs::File; + use std::io::Write; + + let ts = TestScenario::new(util_name!()); + let test_dir = ts.fixtures.subdir.as_path(); + + // Create files directly with non-UTF-8 names + let file1_bytes = b"test_\xFF\xFE_1.txt"; + let file2_bytes = b"test_\xFF\xFE_2.txt"; + + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + let file1_name = std::ffi::OsStr::from_bytes(file1_bytes); + let file2_name = std::ffi::OsStr::from_bytes(file2_bytes); + + let mut file1 = File::create(test_dir.join(file1_name)).unwrap(); + file1.write_all(b"a 1\n").unwrap(); + + let mut file2 = File::create(test_dir.join(file2_name)).unwrap(); + file2.write_all(b"a 2\n").unwrap(); + + ts.ucmd() + .arg(file1_name) + .arg(file2_name) + .succeeds() + .stdout_only("a 1 2\n"); + } +} diff --git a/tests/by-util/test_ln.rs b/tests/by-util/test_ln.rs index 99859503539..71f9b571662 100644 --- a/tests/by-util/test_ln.rs +++ b/tests/by-util/test_ln.rs @@ -843,3 +843,48 @@ fn test_ln_seen_file() { ); } } + +#[test] +#[cfg(target_os = "linux")] +fn test_ln_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + let non_utf8_link_bytes = b"link_\xFF\xFE.txt"; + let non_utf8_link_name = OsStr::from_bytes(non_utf8_link_bytes); + + // Create the actual file + at.touch(non_utf8_name); + + // Test creating a hard link with non-UTF-8 file names + scene + .ucmd() + .arg(non_utf8_name) + .arg(non_utf8_link_name) + .succeeds(); + + // Both files should exist + assert!(at.file_exists(non_utf8_name)); + assert!(at.file_exists(non_utf8_link_name)); + + // Test creating a symbolic link with non-UTF-8 file names + let symlink_bytes = b"symlink_\xFF\xFE.txt"; + let symlink_name = OsStr::from_bytes(symlink_bytes); + + scene + .ucmd() + .args(&["-s"]) + .arg(non_utf8_name) + .arg(symlink_name) + .succeeds(); + + // Check if symlink was created successfully + let symlink_path = at.plus(symlink_name); + assert!(symlink_path.is_symlink()); +} diff --git a/tests/by-util/test_mktemp.rs b/tests/by-util/test_mktemp.rs index 60714ad7f0f..405c7bfeeca 100644 --- a/tests/by-util/test_mktemp.rs +++ b/tests/by-util/test_mktemp.rs @@ -997,3 +997,66 @@ fn test_missing_short_tmpdir_flag() { .no_stdout() .stderr_contains("a value is required for '-p ' but none was supplied"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_non_utf8_template() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let ts = TestScenario::new(util_name!()); + + // Test that mktemp gracefully handles non-UTF-8 templates with an error instead of panicking + let template = OsStr::from_bytes(b"test_\xFF\xFE_XXXXXX"); + + ts.ucmd().arg(template).fails().stderr_contains("invalid"); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_non_utf8_tmpdir_path() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + // Create a directory with non-UTF8 bytes + let dir_name = std::ffi::OsStr::from_bytes(b"test_dir_\xFF\xFE"); + std::fs::create_dir(at.plus(dir_name)).unwrap(); + + // Test that mktemp can handle non-UTF8 directory paths with -p option + ucmd.arg("-p").arg(at.plus(dir_name)).succeeds(); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_non_utf8_tmpdir_long_option() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + // Create a directory with non-UTF8 bytes + let dir_name = std::ffi::OsStr::from_bytes(b"test_dir_\xFF\xFE"); + std::fs::create_dir(at.plus(dir_name)).unwrap(); + + // Test that mktemp can handle non-UTF8 directory paths with --tmpdir option + // Note: Due to test framework limitations with non-UTF8 arguments and --tmpdir= syntax, + // we'll test a more limited scenario that still validates non-UTF8 path handling + ucmd.arg("-p") + .arg(at.plus(dir_name)) + .arg("tmpXXXXXX") + .succeeds(); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_non_utf8_tmpdir_directory_creation() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + // Create a directory with non-UTF8 bytes + let dir_name = std::ffi::OsStr::from_bytes(b"test_dir_\xFF\xFE"); + std::fs::create_dir(at.plus(dir_name)).unwrap(); + + // Test directory creation (-d flag) with non-UTF8 directory paths + // We can't easily verify the exact output path because of UTF8 conversion issues, + // but we can verify the command succeeds + ucmd.arg("-d").arg("-p").arg(at.plus(dir_name)).succeeds(); +} diff --git a/tests/by-util/test_more.rs b/tests/by-util/test_more.rs index 4cd984d7cef..a46648a8b1a 100644 --- a/tests/by-util/test_more.rs +++ b/tests/by-util/test_more.rs @@ -126,3 +126,21 @@ fn test_invalid_file_perms() { .stderr_contains("permission denied"); } } + +#[test] +#[cfg(target_os = "linux")] +fn test_more_non_utf8_paths() { + use std::os::unix::ffi::OsStrExt; + if std::io::stdout().is_terminal() { + let (at, mut ucmd) = at_and_ucmd!(); + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + // Create test file with normal name first + at.write( + &file_name.to_string_lossy(), + "test content for non-UTF-8 file", + ); + + // Test that more can handle non-UTF-8 filenames without crashing + ucmd.arg(file_name).succeeds(); + } +} diff --git a/tests/by-util/test_nl.rs b/tests/by-util/test_nl.rs index 7e9fb7c14a2..34a869dbbaa 100644 --- a/tests/by-util/test_nl.rs +++ b/tests/by-util/test_nl.rs @@ -9,6 +9,22 @@ use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; +#[test] +#[cfg(target_os = "linux")] +fn test_nl_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"line 1\nline 2\nline 3\n").unwrap(); + + ucmd.arg(&filename) + .succeeds() + .stdout_contains("1\t") + .stdout_contains("2\t") + .stdout_contains("3\t"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); diff --git a/tests/by-util/test_paste.rs b/tests/by-util/test_paste.rs index 8da77e3ff9c..a87f2159883 100644 --- a/tests/by-util/test_paste.rs +++ b/tests/by-util/test_paste.rs @@ -4,7 +4,8 @@ // file that was distributed with this source code. // spell-checker:ignore bsdutils toybox - +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -252,6 +253,7 @@ FIRST!SECOND@THIRD#FOURTH!ABCDEFG } #[test] +#[cfg(unix)] fn test_non_utf8_input() { // 0xC0 is not valid UTF-8 const INPUT: &[u8] = b"Non-UTF-8 test: \xC0\x00\xC0.\n"; @@ -375,3 +377,20 @@ fn test_data() { .stdout_is(example.out); } } + +#[test] +#[cfg(target_os = "linux")] +fn test_paste_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename1 = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + let filename2 = std::ffi::OsString::from_vec(vec![0xF0, 0x90]); + + std::fs::write(at.plus(&filename1), b"line1\nline2\n").unwrap(); + std::fs::write(at.plus(&filename2), b"col1\ncol2\n").unwrap(); + + ucmd.arg(&filename1) + .arg(&filename2) + .succeeds() + .stdout_is("line1\tcol1\nline2\tcol2\n"); +} diff --git a/tests/by-util/test_pathchk.rs b/tests/by-util/test_pathchk.rs index 064f0aa273b..85f5c09ea0b 100644 --- a/tests/by-util/test_pathchk.rs +++ b/tests/by-util/test_pathchk.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uutests::new_ucmd; #[test] @@ -164,3 +166,10 @@ fn test_posix_all() { // fail on empty path new_ucmd!().args(&["-p", "-P", ""]).fails().no_stdout(); } + +#[test] +#[cfg(target_os = "linux")] +fn test_pathchk_non_utf8_paths() { + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + new_ucmd!().arg(&filename).succeeds(); +} diff --git a/tests/by-util/test_readlink.rs b/tests/by-util/test_readlink.rs index f837f2b4aef..ebc85f543fe 100644 --- a/tests/by-util/test_readlink.rs +++ b/tests/by-util/test_readlink.rs @@ -373,3 +373,25 @@ fn test_delimiters() { .stderr_contains("ignoring --no-newline with multiple arguments") .stdout_is("/a\n/a\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_readlink_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let file_name = "target_file"; + at.touch(file_name); + let non_utf8_bytes = b"symlink_\xFF\xFE"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + std::os::unix::fs::symlink(at.plus_as_string(file_name), at.plus(non_utf8_name)).unwrap(); + + // Test that readlink handles non-UTF-8 symlink names without crashing + let result = scene.ucmd().arg(non_utf8_name).succeeds(); + + let output = result.stdout_str_lossy(); + assert!(output.contains(file_name)); +} diff --git a/tests/by-util/test_realpath.rs b/tests/by-util/test_realpath.rs index ee156f5d031..249614bf4ac 100644 --- a/tests/by-util/test_realpath.rs +++ b/tests/by-util/test_realpath.rs @@ -464,3 +464,44 @@ fn test_realpath_trailing_slash() { fn test_realpath_empty() { new_ucmd!().fails_with_code(1); } + +#[test] +#[cfg(target_os = "linux")] +fn test_realpath_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + at.touch(non_utf8_name); + let result = scene.ucmd().arg(non_utf8_name).succeeds(); + + let output = result.stdout_str_lossy(); + assert!(output.contains("test_")); + assert!(output.contains(".txt")); +} + +#[test] +fn test_realpath_empty_string() { + // Test that empty string arguments are rejected with exit code 1 + new_ucmd!().arg("").fails().code_is(1); + + // Test that empty --relative-base is rejected + new_ucmd!() + .arg("--relative-base=") + .arg("--relative-to=.") + .arg(".") + .fails() + .code_is(1); + + new_ucmd!() + .arg("--relative-to=") + .arg(".") + .fails() + .code_is(1); +} diff --git a/tests/by-util/test_rm.rs b/tests/by-util/test_rm.rs index ec7de913698..e14268a20ea 100644 --- a/tests/by-util/test_rm.rs +++ b/tests/by-util/test_rm.rs @@ -1037,3 +1037,38 @@ fn test_inaccessible_dir_recursive() { assert!(!at.dir_exists("a/unreadable")); assert!(!at.dir_exists("a")); } + +#[test] +#[cfg(target_os = "linux")] +fn test_rm_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + // Create the actual file + at.touch(non_utf8_name); + assert!(at.file_exists(non_utf8_name)); + + // Test that rm handles non-UTF-8 file names without crashing + scene.ucmd().arg(non_utf8_name).succeeds(); + + // The file should be removed + assert!(!at.file_exists(non_utf8_name)); + + // Test with directory + let non_utf8_dir_bytes = b"test_dir_\xFF\xFE"; + let non_utf8_dir_name = OsStr::from_bytes(non_utf8_dir_bytes); + + at.mkdir(non_utf8_dir_name); + assert!(at.dir_exists(non_utf8_dir_name)); + + scene.ucmd().args(&["-r"]).arg(non_utf8_dir_name).succeeds(); + + assert!(!at.dir_exists(non_utf8_dir_name)); +} diff --git a/tests/by-util/test_shred.rs b/tests/by-util/test_shred.rs index a31ef4bf4a2..aa95a769ae1 100644 --- a/tests/by-util/test_shred.rs +++ b/tests/by-util/test_shred.rs @@ -316,3 +316,17 @@ fn test_shred_rename_exhaustion() { assert!(!at.file_exists("test")); } + +#[test] +#[cfg(target_os = "linux")] +fn test_shred_non_utf8_paths() { + use std::os::unix::ffi::OsStrExt; + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + std::fs::write(at.plus(file_name), "test content").unwrap(); + + // Test that shred can handle non-UTF-8 filenames + ts.ucmd().arg(file_name).succeeds(); +} diff --git a/tests/by-util/test_split.rs b/tests/by-util/test_split.rs index 34b24d84dbd..f710e14425b 100644 --- a/tests/by-util/test_split.rs +++ b/tests/by-util/test_split.rs @@ -10,6 +10,8 @@ use regex::Regex; use rlimit::Resource; #[cfg(not(windows))] use std::env; +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use std::path::Path; use std::{ fs::{File, read_dir}, @@ -1709,7 +1711,7 @@ fn test_split_invalid_input() { /// Test if there are invalid (non UTF-8) in the arguments - unix /// clap is expected to fail/panic #[test] -#[cfg(unix)] +#[cfg(target_os = "linux")] fn test_split_non_utf8_argument_unix() { use std::ffi::OsStr; use std::os::unix::ffi::OsStrExt; @@ -1724,9 +1726,7 @@ fn test_split_non_utf8_argument_unix() { let opt_value = [0x66, 0x6f, 0x80, 0x6f]; let opt_value = OsStr::from_bytes(&opt_value[..]); let name = OsStr::from_bytes(name.as_bytes()); - ucmd.args(&[opt, opt_value, name]) - .fails() - .stderr_contains("error: invalid UTF-8 was detected in one or more arguments"); + ucmd.args(&[opt, opt_value, name]).succeeds(); } /// Test if there are invalid (non UTF-8) in the arguments - windows @@ -1747,9 +1747,7 @@ fn test_split_non_utf8_argument_windows() { let opt_value = [0x0066, 0x006f, 0xD800, 0x006f]; let opt_value = OsString::from_wide(&opt_value[..]); let name = OsString::from(name); - ucmd.args(&[opt, opt_value, name]) - .fails() - .stderr_contains("error: invalid UTF-8 was detected in one or more arguments"); + ucmd.args(&[opt, opt_value, name]).succeeds(); } // Test '--separator' / '-t' option following GNU tests example @@ -2006,3 +2004,77 @@ fn test_long_lines() { assert_eq!(at.read("xac").len(), 131_072); assert!(!at.plus("xad").exists()); } + +#[test] +#[cfg(target_os = "linux")] +fn test_split_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"line1\nline2\nline3\nline4\nline5\n").unwrap(); + + ucmd.arg(&filename).succeeds(); + + // Check that at least one split file was created + assert!(at.plus("xaa").exists()); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_split_non_utf8_prefix() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("input.txt", "line1\nline2\nline3\nline4\n"); + + let prefix = std::ffi::OsStr::from_bytes(b"\xFF\xFE"); + ucmd.arg("input.txt").arg(prefix).succeeds(); + + // Check that split files were created (functionality works) + // The actual filename may be converted due to lossy conversion, but the command should succeed + let entries: Vec<_> = std::fs::read_dir(at.as_string()).unwrap().collect(); + let split_files = entries + .iter() + .filter_map(|e| e.as_ref().ok()) + .filter(|entry| { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + name_str.starts_with("�") || name_str.len() > 2 // split files should exist + }) + .count(); + assert!( + split_files > 0, + "Expected at least one split file to be created" + ); +} + +#[test] +#[cfg(target_os = "linux")] +fn test_split_non_utf8_additional_suffix() { + use std::os::unix::ffi::OsStrExt; + let (at, mut ucmd) = at_and_ucmd!(); + + at.write("input.txt", "line1\nline2\nline3\nline4\n"); + + let suffix = std::ffi::OsStr::from_bytes(b"\xFF\xFE"); + ucmd.args(&["input.txt", "--additional-suffix"]) + .arg(suffix) + .succeeds(); + + // Check that split files were created (functionality works) + // The actual filename may be converted due to lossy conversion, but the command should succeed + let entries: Vec<_> = std::fs::read_dir(at.as_string()).unwrap().collect(); + let split_files = entries + .iter() + .filter_map(|e| e.as_ref().ok()) + .filter(|entry| { + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + name_str.ends_with("�") || name_str.starts_with('x') // split files should exist + }) + .count(); + assert!( + split_files > 0, + "Expected at least one split file to be created" + ); +} diff --git a/tests/by-util/test_stdbuf.rs b/tests/by-util/test_stdbuf.rs index 810e5df5de4..8c3fef5870d 100644 --- a/tests/by-util/test_stdbuf.rs +++ b/tests/by-util/test_stdbuf.rs @@ -3,6 +3,8 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore dyld dylib setvbuf +#[cfg(target_os = "linux")] +use uutests::at_and_ucmd; use uutests::new_ucmd; #[cfg(not(target_os = "windows"))] use uutests::util::TestScenario; @@ -216,3 +218,21 @@ fn test_libstdbuf_preload() { "uutils echo should not show architecture mismatch" ); } + +#[cfg(target_os = "linux")] +#[cfg(not(target_env = "musl"))] +#[test] +fn test_stdbuf_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"test content for stdbuf\n").unwrap(); + + ucmd.arg("-o0") + .arg("cat") + .arg(&filename) + .succeeds() + .stdout_is("test content for stdbuf\n"); +} diff --git a/tests/by-util/test_sum.rs b/tests/by-util/test_sum.rs index 89c454aff24..d4e4e8c5548 100644 --- a/tests/by-util/test_sum.rs +++ b/tests/by-util/test_sum.rs @@ -2,6 +2,8 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(target_os = "linux")] +use std::os::unix::ffi::OsStringExt; use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -80,3 +82,14 @@ fn test_invalid_metadata() { .fails() .stderr_is("sum: b: No such file or directory\n"); } + +#[test] +#[cfg(target_os = "linux")] +fn test_sum_non_utf8_paths() { + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"test content").unwrap(); + + ucmd.arg(&filename).succeeds(); +} diff --git a/tests/by-util/test_tac.rs b/tests/by-util/test_tac.rs index 42e7b76d6c7..0f5aad48808 100644 --- a/tests/by-util/test_tac.rs +++ b/tests/by-util/test_tac.rs @@ -3,10 +3,26 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. // spell-checker:ignore axxbxx bxxaxx axxx axxxx xxaxx xxax xxxxa axyz zyax zyxa +#[cfg(target_os = "linux")] +use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; +#[test] +#[cfg(target_os = "linux")] +fn test_tac_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"line1\nline2\nline3\n").unwrap(); + + ucmd.arg(&filename) + .succeeds() + .stdout_is("line3\nline2\nline1\n"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); diff --git a/tests/by-util/test_touch.rs b/tests/by-util/test_touch.rs index 6c2d3ff1b67..f5c66bc2468 100644 --- a/tests/by-util/test_touch.rs +++ b/tests/by-util/test_touch.rs @@ -1013,3 +1013,19 @@ fn test_touch_f_option() { assert!(at.file_exists(file)); at.remove(file); } + +#[test] +#[cfg(target_os = "linux")] +fn test_touch_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + scene.ucmd().arg(non_utf8_name).succeeds().no_output(); + assert!(std::fs::metadata(at.plus(non_utf8_name)).is_ok()); +} diff --git a/tests/by-util/test_truncate.rs b/tests/by-util/test_truncate.rs index 789bd1d66e1..36ce92dc971 100644 --- a/tests/by-util/test_truncate.rs +++ b/tests/by-util/test_truncate.rs @@ -420,3 +420,16 @@ 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::os::unix::ffi::OsStrExt; + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + let file_name = std::ffi::OsStr::from_bytes(b"test_\xFF\xFE.txt"); + at.write(&file_name.to_string_lossy(), "test content"); + + // Test that truncate can handle non-UTF-8 filenames + ts.ucmd().arg("-s").arg("10").arg(file_name).succeeds(); +} diff --git a/tests/by-util/test_tsort.rs b/tests/by-util/test_tsort.rs index b680aa928ff..eb1a8630d31 100644 --- a/tests/by-util/test_tsort.rs +++ b/tests/by-util/test_tsort.rs @@ -7,6 +7,18 @@ use uutests::at_and_ucmd; use uutests::new_ucmd; +#[test] +#[cfg(target_os = "linux")] +fn test_tsort_non_utf8_paths() { + use std::os::unix::ffi::OsStringExt; + let (at, mut ucmd) = at_and_ucmd!(); + + let filename = std::ffi::OsString::from_vec(vec![0xFF, 0xFE]); + std::fs::write(at.plus(&filename), b"a b\nb c\n").unwrap(); + + ucmd.arg(&filename).succeeds().stdout_is("a\nb\nc\n"); +} + #[test] fn test_invalid_arg() { new_ucmd!().arg("--definitely-invalid").fails_with_code(1); diff --git a/tests/by-util/test_unlink.rs b/tests/by-util/test_unlink.rs index 905f579a6d4..7a2b50ac3c4 100644 --- a/tests/by-util/test_unlink.rs +++ b/tests/by-util/test_unlink.rs @@ -75,3 +75,23 @@ fn test_unlink_symlink() { assert!(at.file_exists("foo")); assert!(!at.file_exists("bar")); } + +#[test] +#[cfg(target_os = "linux")] +fn test_unlink_non_utf8_paths() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let (at, mut ucmd) = at_and_ucmd!(); + // Create a test file with non-UTF-8 bytes in the name + let non_utf8_bytes = b"test_\xFF\xFE.txt"; + let non_utf8_name = OsStr::from_bytes(non_utf8_bytes); + + at.touch(non_utf8_name); + assert!(at.file_exists(non_utf8_name)); + + // Test that unlink handles non-UTF-8 file names without crashing + ucmd.arg(non_utf8_name).succeeds(); + + assert!(!at.file_exists(non_utf8_name)); +}