Merge pull request #8457 from sylvestre/no-utf8

Allow non-utf8 paths name as input for most of the programs
This commit is contained in:
Daniel Hofstetter
2025-08-14 14:46:31 +02:00
committed by GitHub
75 changed files with 1898 additions and 484 deletions
+1
View File
@@ -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
+6
View File
@@ -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
+442
View File
@@ -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<u8> {
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<PathBuf>), std::io::Error> {
let mut rng = rand::rng();
let temp_root = temp_dir().join(format!("utf8_test_{}", rng.random::<u64>()));
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::<String>::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);
});
+4 -2
View File
@@ -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<Self> {
let to_read = match options.get_many::<String>(options::FILE) {
let to_read = match options.get_many::<OsString>(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),
)
}
+7 -5
View File
@@ -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<String> = match matches.get_many::<String>(options::FILE) {
let files: Vec<OsString> = match matches.get_many::<OsString>(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<R: FdReadable>(
}
}
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<InputType> {
fn get_input_type(path: &OsString) -> CatResult<InputType> {
if path == "-" {
return Ok(InputType::StdIn);
}
+6 -4
View File
@@ -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<u32, String> {
fn get_dest_gid(matches: &ArgMatches) -> UResult<(Option<u32>, String)> {
let mut raw_group = String::new();
let dest_gid = if let Some(file) = matches.get_one::<String>(options::REFERENCE) {
fs::metadata(file)
let dest_gid = if let Some(file) = matches.get_one::<std::ffi::OsString>(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(
+19 -15
View File
@@ -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::<String>(options::REFERENCE) {
let fmode = match matches.get_one::<OsString>(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<String> = matches
.get_many::<String>(options::FILE)
.map(|v| v.map(ToString::to_string).collect())
let mut files: Vec<OsString> = matches
.get_many::<OsString>(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);
+18 -12
View File
@@ -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<bool> {
pub fn are_files_identical(path1: &Path, path2: &Path) -> io::Result<bool> {
// 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::<String>(options::FILE_1),
opts.get_one::<String>(options::FILE_2),
opts.get_one::<OsString>(options::FILE_1),
opts.get_one::<OsString>(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<LineReader> {
fn open_file(name: &OsString, line_ending: LineEnding) -> io::Result<LineReader> {
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<LineReader> {
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::<String>(options::FILE_1).unwrap();
let filename2 = matches.get_one::<String>(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::<OsString>(options::FILE_1).unwrap();
let filename2 = matches.get_one::<OsString>(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)
+4 -2
View File
@@ -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::<String>(options::FILE).unwrap();
let file_name = matches.get_one::<OsString>(options::FILE).unwrap();
// get the patterns to split on
let patterns: Vec<String> = 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)
+9 -8
View File
@@ -343,11 +343,11 @@ fn cut_fields<R: Read, W: Write>(
}
}
fn cut_files(mut filenames: Vec<String>, mode: &Mode) {
fn cut_files(mut filenames: Vec<OsString>, mode: &Mode) {
let mut stdin_read = false;
if filenames.is_empty() {
filenames.push("-".to_owned());
filenames.push(OsString::from("-"));
}
let mut out: Box<dyn Write> = if stdout().is_terminal() {
@@ -370,12 +370,12 @@ fn cut_files(mut filenames: Vec<String>, 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<String>, 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<String> = matches
.get_many::<String>(options::FILE)
let files: Vec<OsString> = matches
.get_many::<OsString>(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)),
)
}
+12 -6
View File
@@ -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::<String>(options::FILE)
.get_many::<OsString>(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),
)
}
+6 -4
View File
@@ -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<String> = matches
.get_many::<String>(options::DIR)
let dirnames: Vec<OsString> = matches
.get_many::<OsString>(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)),
)
}
+14 -9
View File
@@ -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<Vec<PathBuf>, std::io::Error> {
fn read_files_from(file_name: &OsStr) -> Result<Vec<PathBuf>, std::io::Error> {
let reader: Box<dyn BufRead> = 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<Vec<PathBuf>, 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<Vec<PathBuf>, 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<Vec<PathBuf>, 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::<String>(options::FILES0_FROM) {
if file_from == "-" && matches.get_one::<String>(options::FILE).is_some() {
let files = if let Some(file_from) = matches.get_one::<OsString>(options::FILES0_FROM) {
if file_from == "-" && matches.get_one::<OsString>(options::FILE).is_some() {
return Err(std::io::Error::other(
translate!("du-error-extra-operand-with-files0-from",
"file" => matches
.get_one::<String>(options::FILE)
.get_one::<OsString>(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::<String>(options::FILE) {
} else if let Some(files) = matches.get_many::<OsString>(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),
)
}
+10 -8
View File
@@ -170,7 +170,7 @@ fn tabstops_parse(s: &str) -> Result<(RemainingMode, Vec<usize>), ParseError> {
}
struct Options {
files: Vec<String>,
files: Vec<OsString>,
tabstops: Vec<usize>,
tspaces: String,
iflag: bool,
@@ -204,9 +204,9 @@ impl Options {
.unwrap(); // length of tabstops is guaranteed >= 1
let tspaces = " ".repeat(nspaces);
let files: Vec<String> = match matches.get_many::<String>(options::FILES) {
Some(s) => s.map(|v| v.to_string()).collect(),
None => vec!["-".to_owned()],
let files: Vec<OsString> = match matches.get_many::<OsString>(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<BufReader<Box<dyn Read + 'static>>> {
fn open(path: &OsString) -> UResult<BufReader<Box<dyn Read + 'static>>> {
let file_buf;
if path == "-" {
Ok(BufReader::new(Box::new(stdin()) as Box<dyn Read>))
} 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<dyn Read>))
}
}
@@ -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;
+32 -25
View File
@@ -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<Stdout>,
) -> UResult<()> {
let mut fp = BufReader::new(match file_name {
"-" => Box::new(stdin()) as Box<dyn Read + 'static>,
_ => {
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<dyn Read + 'static>
let mut fp = BufReader::new(if file_name == "-" {
Box::new(stdin()) as Box<dyn Read + 'static>
} 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<dyn Read + 'static>
});
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<Vec<String>> {
fn extract_files(matches: &ArgMatches) -> UResult<Vec<OsString>> {
let in_first_pos = matches
.index_of(options::FILES_OR_WIDTH)
.is_some_and(|x| x == 1);
let is_neg = |s: &str| s.parse::<isize>().is_ok_and(|w| w < 0);
let files: UResult<Vec<String>> = matches
.get_many::<String>(options::FILES_OR_WIDTH)
let files: UResult<Vec<OsString>> = matches
.get_many::<OsString>(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<Vec<String>> {
.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<Option<usize>> {
}
if let Some(1) = matches.index_of(options::FILES_OR_WIDTH) {
let width_arg = matches.get_one::<String>(options::FILES_OR_WIDTH).unwrap();
if let Some(num) = width_arg.strip_prefix('-') {
let width_arg = matches
.get_one::<OsString>(options::FILES_OR_WIDTH)
.unwrap();
let width_str = width_arg.to_string_lossy();
if let Some(num) = width_str.strip_prefix('-') {
Ok(num.parse::<usize>().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),
)
}
+81 -74
View File
@@ -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<String>,
pub files: Vec<OsString>,
}
impl HeadOptions {
@@ -215,9 +224,9 @@ impl HeadOptions {
options.mode = Mode::from(matches)?;
options.files = match matches.get_many::<String>(options::FILES_NAME) {
options.files = match matches.get_many::<OsString>(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<u64> {
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)?;
}
}
#[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())
}
}?;
}
Ok(())
println!("{}", translate!("head-header-stdin"));
}
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;
}
};
if (options.files.len() > 1 && !options.quiet) || options.verbose {
if !first {
println!();
}
println!("==> {name} <==");
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)?;
}
head_file(&mut file, options)?;
Ok(())
}
#[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())
}
}?;
}
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_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]
+24 -14
View File
@@ -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<String> = matches
.get_many::<String>(ARG_FILES)
.map(|v| v.map(ToString::to_string).collect())
let paths: Vec<OsString> = matches
.get_many::<OsString>(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<Behavior> {
///
/// 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<String>, b: &Behavior) -> UResult<()> {
fn standard(mut paths: Vec<OsString>, 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<String>, 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<String>, 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<String>, 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;
}
+15 -7
View File
@@ -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<Box<dyn BufRead + 'a>>,
@@ -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<dyn BufRead>
} 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<dyn BufRead>
};
@@ -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::<String>("file1").unwrap();
let file2 = matches.get_one::<String>("file2").unwrap();
let file1 = matches.get_one::<OsString>("file1").unwrap();
let file2 = matches.get_one::<OsString>("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<Sep: Separator>(file1: &str, file2: &str, settings: Settings, sep: Sep) -> UResult<()> {
fn exec<Sep: Separator>(
file1: &OsString,
file2: &OsString,
settings: Settings,
sep: Sep,
) -> UResult<()> {
let stdin = stdin();
let mut state1 = State::new(
+18 -16
View File
@@ -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<String>,
target_dir: Option<PathBuf>,
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<PathBuf> = matches
.get_many::<String>(ARG_FILES)
.get_many::<OsString>(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::<String>(options::TARGET_DIRECTORY)
.map(String::from),
.get_one::<OsString>(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);
}
+50 -38
View File
@@ -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::<String>(ARG_TEMPLATE) {
let (tmpdir, template) = match matches.get_one::<OsString>(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<Self, MkTempError> {
// 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::<String>(),
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<PathBuf> {

Some files were not shown because too many files have changed in this diff Show More