From 9f0447cbe9aa2886ceab3bea63c95051aeac4293 Mon Sep 17 00:00:00 2001 From: Sylvestre Ledru Date: Fri, 8 Aug 2025 12:39:54 +0200 Subject: [PATCH] Add non-UTF-8 filename support to tee and improve fuzzer coverage --- fuzz/fuzz_targets/fuzz_non_utf8_paths.rs | 71 ++++++++++++++++++++++-- src/uu/tee/src/tee.rs | 16 +++--- 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs index b4a0f9b2b..78a6c2d0a 100644 --- a/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs +++ b/fuzz/fuzz_targets/fuzz_non_utf8_paths.rs @@ -17,8 +17,22 @@ use std::path::PathBuf; use uufuzz::{run_gnu_cmd, CommandResult}; // Programs that typically take file/path arguments and should be tested static PATH_PROGRAMS: &[&str] = &[ - "basename", "cat", "chmod", "cp", "dirname", "du", "head", "ln", "ls", "mkdir", "mv", - "readlink", "realpath", "rm", "rmdir", "tail", "touch", "unlink", + // Core file operations + "cat", "cp", "mv", "rm", "ln", "link", "unlink", "touch", "truncate", + // Directory operations + "ls", "mkdir", "rmdir", "du", "stat", "mktemp", + // Path operations + "basename", "dirname", "readlink", "realpath", "pathchk", + // File content operations + "head", "tail", "tee", "more", "od", "wc", "cksum", "sum", + // File processing + "sort", "uniq", "split", "csplit", "cut", "tr", "shred", + // File permissions/ownership + "chmod", "chown", "chgrp", "install", + // Text processing with files + "comm", "join", "paste", "pr", "fmt", "fold", "expand", "unexpand", + // Directory listing variants + "dir", "vdir", ]; fn generate_non_utf8_bytes() -> Vec { @@ -84,20 +98,65 @@ fn test_program_with_non_utf8_path(program: &str, path: &PathBuf) -> CommandResu // 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(), ], - "cp" | "mv" | "ln" => { - // These need a destination - create a temp destination + "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(), + ], + // 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"), + ], + // 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()], }; @@ -175,7 +234,7 @@ fuzz_target!(|_data: &[u8]| { } // Special cases for programs that need additional testing - if **program == "mkdir" { + 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); diff --git a/src/uu/tee/src/tee.rs b/src/uu/tee/src/tee.rs index 5c6a120e7..faf848ebd 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,