Merge pull request #8457 from sylvestre/no-utf8
Some checks are pending
CICD / MinRustV (push) Waiting to run
CICD / Style/cargo-deny (push) Waiting to run
CICD / Build (push) Blocked by required conditions
CICD / Style/deps (push) Waiting to run
CICD / Documentation/warnings (push) Waiting to run
CICD / Test all features separately (push) Blocked by required conditions
CICD / Dependencies (push) Waiting to run
CICD / Build/Makefile (push) Blocked by required conditions
CICD / Build/stable (push) Blocked by required conditions
CICD / Build/nightly (push) Blocked by required conditions
CICD / Binary sizes (push) Blocked by required conditions
CICD / Tests/BusyBox test suite (push) Blocked by required conditions
CICD / Tests/Toybox test suite (push) Blocked by required conditions
CICD / Code Coverage (push) Waiting to run
CICD / Separate Builds (push) Waiting to run
CICD / Build/SELinux (push) Blocked by required conditions
GnuTests / Run GNU tests (native) (push) Waiting to run
GnuTests / Run GNU tests (SELinux) (push) Waiting to run
GnuTests / Aggregate GNU test results (push) Blocked by required conditions
Android / Test builds (push) Waiting to run
Code Quality / Style/format (push) Waiting to run
Code Quality / Style/lint (push) Waiting to run
Code Quality / Style/spelling (push) Waiting to run
Code Quality / Style/toml (push) Waiting to run
Code Quality / Style/Python (push) Waiting to run
Code Quality / Pre-commit hooks (push) Waiting to run
FreeBSD / Style and Lint (push) Waiting to run
FreeBSD / Tests (push) Waiting to run
L10n (Localization) / L10n/Build and Test (push) Waiting to run
L10n (Localization) / L10n/Fluent Syntax Check (push) Waiting to run
L10n (Localization) / L10n/Clap Error Localization Test (push) Waiting to run
L10n (Localization) / L10n/French Integration Test (push) Waiting to run
L10n (Localization) / L10n/Multi-call Binary Install Test (push) Waiting to run
L10n (Localization) / L10n/Installation Test (Make & Cargo) (push) Waiting to run
L10n (Localization) / L10n/Locale Support Verification (push) Waiting to run
L10n (Localization) / L10n/Locale Embedding Regression Test (push) Waiting to run
WSL2 / Test (push) Waiting to run

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
commit 5ba99ae564
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 1898 additions and 484 deletions

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

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

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);
});

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),
)
}

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);
}

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(

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);

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)

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)

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)),
)
}

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),
)
}

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)),
)
}

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),
)
}

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;

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),
)
}

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]

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;
}

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(

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);
}

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> {

View file

@ -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::<String>(options::FILES) {
if let Some(files) = matches.get_many::<OsString>(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)),
)
}

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::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<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 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)

View file

@ -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::<String>(options::DELIMITER).unwrap();
let files = matches
.get_many::<String>(options::FILE)
.get_many::<OsString>(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<String>,
filenames: Vec<OsString>,
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);

View file

@ -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::<String>(options::PATH);
let paths = matches.get_many::<OsString>(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)),
)
}

View file

@ -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<HashSet<String>> {
let filename = matches
.get_one::<String>(option)
.expect("parsing options failed!")
.to_string();
.get_one::<OsString>(option)
.expect("parsing options failed!");
let reader: BufReader<Box<dyn Read>> = 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<String> = HashSet::new();
@ -88,12 +89,12 @@ fn read_char_filter_file(
option: &str,
) -> std::io::Result<HashSet<char>> {
let filename = matches
.get_one::<String>(option)
.get_one::<OsString>(option)
.expect("parsing options failed!");
let mut reader: Box<dyn Read> = 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<String, FileContent>;
type FileMap = HashMap<OsString, FileContent>;
fn read_input(input_files: &[String]) -> std::io::Result<FileMap> {
fn read_input(input_files: &[OsString]) -> std::io::Result<FileMap> {
let mut file_map: FileMap = HashMap::new();
let mut offset: usize = 0;
for filename in input_files {
let reader: BufReader<Box<dyn Read>> = 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<String> = reader.lines().collect::<std::io::Result<Vec<String>>>()?;
@ -292,7 +293,7 @@ fn read_input(input_files: &[String]) -> std::io::Result<FileMap> {
let chars_lines: Vec<Vec<char>> = 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<WordRef>,
output_filename: &str,
output_filename: &OsStr,
) -> UResult<()> {
let mut writer: BufWriter<Box<dyn Write>> = 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<Box<dyn Write>> =
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::<String>(options::FILE)
.get_many::<OsString>(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::<Vec<_>>();
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)

View file

@ -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<String> = matches
.get_many::<String>(ARG_FILES)
.map(|v| v.map(ToString::to_string).collect())
let files: Vec<PathBuf> = matches
.get_many::<OsString>(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<LineEnding>) -> 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}");
}

View file

@ -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

View file

@ -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

View file

@ -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<Self::Value, clap::Error> {
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<PathBuf> = matches
.get_many::<String>(ARG_FILES)
.get_many::<OsString>(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<PathBuf>, Option<PathBuf>)> {
let relative_to = matches
.get_one::<String>(OPT_RELATIVE_TO)
.get_one::<OsString>(OPT_RELATIVE_TO)
.map(PathBuf::from);
let relative_base = matches
.get_one::<String>(OPT_RELATIVE_BASE)
.get_one::<OsString>(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)?;

View file

@ -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());

View file

@ -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::<String>(options::FILE).unwrap() {
for path_str in matches.get_many::<OsString>(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<u64>,
@ -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<PathBuf> {
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> {

View file

@ -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::<String>(OPT_ADDITIONAL_SUFFIX)
.get_one::<OsString>(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<Self> {
pub fn new(prefix: &'a OsStr, suffix: &'a Suffix) -> UResult<Self> {
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());
}
}

View file

@ -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))
}

View file

@ -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<dyn Write>))
}
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)
}

View file

@ -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<String>,
strategy: Strategy,
@ -489,9 +495,9 @@ impl Settings {
};
let result = Self {
prefix: matches.get_one::<String>(ARG_PREFIX).unwrap().clone(),
prefix: matches.get_one::<OsString>(ARG_PREFIX).unwrap().clone(),
suffix,
input: matches.get_one::<String>(ARG_INPUT).unwrap().clone(),
input: matches.get_one::<OsString>(ARG_INPUT).unwrap().clone(),
filter: matches.get_one::<String>(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<BufWriter<Box<dyn Write>>> {
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<T: Write>(
///
/// Note: The `buf` might end up with either partial or entire input content.
fn get_input_size<R>(
input: &String,
input: &OsString,
reader: &mut R,
buf: &mut Vec<u8>,
io_blksize: Option<u64>,
@ -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<dyn Read>
} 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<dyn Read>
};

View file

@ -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::<String>(options::COMMAND).unwrap();
let mut command_values = matches.get_many::<OsString>(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)),
)
}

View file

@ -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<Box<dyn Read>> {
match name {
"-" => Ok(Box::new(stdin()) as Box<dyn Read>),
_ => {
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<dyn Read>)
fn open(name: &OsString) -> UResult<Box<dyn Read>> {
if name == "-" {
Ok(Box::new(stdin()) as Box<dyn Read>)
} 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<dyn Read>)
}
}
@ -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<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 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)

View file

@ -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

View file

@ -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::<String>(options::FILE) {
Some(v) => v.map(|s| s.as_str()).collect(),
None => vec!["-"],
let files: Vec<OsString> = match matches.get_many::<OsString>(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<dyn UError> = TacError::ReadError("stdin".to_string(), e).into();
let e: Box<dyn UError> = 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<dyn UError> = TacError::InvalidArgument(String::from(filename)).into();
let e: Box<dyn UError> = TacError::InvalidArgument(filename.clone()).into();
show!(e);
continue;
}
if path.metadata().is_err() {
let e: Box<dyn UError> = TacError::FileNotFound(String::from(filename)).into();
let e: Box<dyn UError> = 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<dyn UError> = TacError::ReadError(s.to_string(), e).into();
let e: Box<dyn UError> = TacError::ReadError(filename.clone(), e).into();
show!(e);
continue;
}

View file

@ -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<String>,
files: Vec<OsString>,
output_error: Option<OutputErrorMode>,
}
@ -77,8 +78,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
};
let files = matches
.get_many::<String>(options::FILE)
.map(|v| v.map(ToString::to_string).collect())
.get_many::<OsString>(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<usize> {
/// 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<Result<NamedWriter>> {
@ -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,

View file

@ -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::<String>(ARG_FILES)
let mut filenames: Vec<&OsString> = matches
.get_many::<OsString>(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(

View file

@ -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<String> = matches
.get_many::<String>(options::ARG_FILES)
.map(|v| v.map(ToString::to_string).collect())
let files: Vec<OsString> = matches
.get_many::<OsString>(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<String>,
size: Option<String>,
filenames: &[String],
filenames: &[OsString],
) -> UResult<()> {
let create = !no_create;

View file

@ -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::<String>(options::FILE)
.get_one::<OsString>(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::<Vec<&str>>().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),
)
}

View file

@ -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<String>,
pub files: Vec<OsString>,
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<String> = matches
.get_many::<String>(options::ARG_FILES)
.map(|v| v.map(ToString::to_string).collect())
let files: Vec<OsString> = matches
.get_many::<OsString>(options::ARG_FILES)
.map(|v| v.cloned().collect())
.unwrap_or_default();
let preserve_root = matches.get_flag(options::preserve_root::PRESERVE);

View file

@ -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!";

View file

@ -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() {

View file

@ -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();
}

View file

@ -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
);
}

View file

@ -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();
}

View file

@ -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");
}

View file

@ -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);

View file

@ -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_"));
}

View file

@ -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");
}

View file

@ -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();
}

View file

@ -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

View file

@ -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();
}

View file

@ -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");
}
}

View file

@ -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());
}

View file

@ -997,3 +997,66 @@ fn test_missing_short_tmpdir_flag() {
.no_stdout()
.stderr_contains("a value is required for '-p <DIR>' 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();
}

View file

@ -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();
}
}

View file

@ -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);

View file

@ -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");
}

View file

@ -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();
}

View file

@ -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));
}

View file

@ -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);
}

View file

@ -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));
}

View file

@ -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();
}

View file

@ -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("<EFBFBD>") || 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("<EFBFBD>") || name_str.starts_with('x') // split files should exist
})
.count();
assert!(
split_files > 0,
"Expected at least one split file to be created"
);
}

View file

@ -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");
}

View file

@ -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();
}

View file

@ -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);

View file

@ -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());
}

View file

@ -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();
}

View file

@ -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);

View file

@ -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));
}