mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
Merge pull request #8457 from sylvestre/no-utf8
Allow non-utf8 paths name as input for most of the programs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ mod platform;
|
||||
use crate::platform::is_unsafe_overwrite;
|
||||
use clap::{Arg, ArgAction, Command};
|
||||
use memchr::memchr2;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::{File, metadata};
|
||||
use std::io::{self, BufWriter, ErrorKind, IsTerminal, Read, Write};
|
||||
/// Unix domain socket support
|
||||
@@ -267,9 +268,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
.any(|v| matches.get_flag(v));
|
||||
|
||||
let squeeze_blank = matches.get_flag(options::SQUEEZE_BLANK);
|
||||
let files: Vec<String> = match matches.get_many::<String>(options::FILE) {
|
||||
let files: Vec<OsString> = match matches.get_many::<OsString>(options::FILE) {
|
||||
Some(v) => v.cloned().collect(),
|
||||
None => vec!["-".to_owned()],
|
||||
None => vec![OsString::from("-")],
|
||||
};
|
||||
|
||||
let options = OutputOptions {
|
||||
@@ -294,6 +295,7 @@ pub fn uu_app() -> Command {
|
||||
Arg::new(options::FILE)
|
||||
.hide(true)
|
||||
.action(ArgAction::Append)
|
||||
.value_parser(clap::value_parser!(OsString))
|
||||
.value_hint(clap::ValueHint::FilePath),
|
||||
)
|
||||
.arg(
|
||||
@@ -379,7 +381,7 @@ fn cat_handle<R: FdReadable>(
|
||||
}
|
||||
}
|
||||
|
||||
fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> {
|
||||
fn cat_path(path: &OsString, options: &OutputOptions, state: &mut OutputState) -> CatResult<()> {
|
||||
match get_input_type(path)? {
|
||||
InputType::StdIn => {
|
||||
let stdin = io::stdin();
|
||||
@@ -417,7 +419,7 @@ fn cat_path(path: &str, options: &OutputOptions, state: &mut OutputState) -> Cat
|
||||
}
|
||||
}
|
||||
|
||||
fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> {
|
||||
fn cat_files(files: &[OsString], options: &OutputOptions) -> UResult<()> {
|
||||
let mut state = OutputState {
|
||||
line_number: LineNumber::new(),
|
||||
at_line_start: true,
|
||||
@@ -452,7 +454,7 @@ fn cat_files(files: &[String], options: &OutputOptions) -> UResult<()> {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Path on a file system to classify metadata
|
||||
fn get_input_type(path: &str) -> CatResult<InputType> {
|
||||
fn get_input_type(path: &OsString) -> CatResult<InputType> {
|
||||
if path == "-" {
|
||||
return Ok(InputType::StdIn);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// spell-checker:ignore (ToDO) COMFOLLOW Chowner RFILE RFILE's derefer dgid nonblank nonprint nonprinting
|
||||
|
||||
use uucore::display::Quotable;
|
||||
pub use uucore::entries;
|
||||
use uucore::entries;
|
||||
use uucore::error::{FromIo, UResult, USimpleError};
|
||||
use uucore::format_usage;
|
||||
use uucore::perms::{GidUidOwnerFilter, IfFrom, chown_base, options};
|
||||
@@ -37,15 +37,16 @@ fn parse_gid_from_str(group: &str) -> Result<u32, String> {
|
||||
|
||||
fn get_dest_gid(matches: &ArgMatches) -> UResult<(Option<u32>, String)> {
|
||||
let mut raw_group = String::new();
|
||||
let dest_gid = if let Some(file) = matches.get_one::<String>(options::REFERENCE) {
|
||||
fs::metadata(file)
|
||||
let dest_gid = if let Some(file) = matches.get_one::<std::ffi::OsString>(options::REFERENCE) {
|
||||
let path = std::path::Path::new(file);
|
||||
fs::metadata(path)
|
||||
.map(|meta| {
|
||||
let gid = meta.gid();
|
||||
raw_group = entries::gid2grp(gid).unwrap_or_else(|_| gid.to_string());
|
||||
Some(gid)
|
||||
})
|
||||
.map_err_context(
|
||||
|| translate!("chgrp-error-failed-to-get-attributes", "file" => file.quote()),
|
||||
|| translate!("chgrp-error-failed-to-get-attributes", "file" => path.quote()),
|
||||
)?
|
||||
} else {
|
||||
let group = matches
|
||||
@@ -153,6 +154,7 @@ pub fn uu_app() -> Command {
|
||||
.long(options::REFERENCE)
|
||||
.value_name("RFILE")
|
||||
.value_hint(clap::ValueHint::FilePath)
|
||||
.value_parser(clap::value_parser!(std::ffi::OsString))
|
||||
.help(translate!("chgrp-help-reference")),
|
||||
)
|
||||
.arg(
|
||||
|
||||
+19
-15
@@ -119,11 +119,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let quiet = matches.get_flag(options::QUIET);
|
||||
let verbose = matches.get_flag(options::VERBOSE);
|
||||
let preserve_root = matches.get_flag(options::PRESERVE_ROOT);
|
||||
let fmode = match matches.get_one::<String>(options::REFERENCE) {
|
||||
let fmode = match matches.get_one::<OsString>(options::REFERENCE) {
|
||||
Some(fref) => match fs::metadata(fref) {
|
||||
Ok(meta) => Some(meta.mode() & 0o7777),
|
||||
Err(_) => {
|
||||
return Err(ChmodError::CannotStat(fref.to_string()).into());
|
||||
return Err(ChmodError::CannotStat(fref.to_string_lossy().to_string()).into());
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
@@ -135,16 +135,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
} else {
|
||||
modes.unwrap().to_string() // modes is required
|
||||
};
|
||||
// FIXME: enable non-utf8 paths
|
||||
let mut files: Vec<String> = matches
|
||||
.get_many::<String>(options::FILE)
|
||||
.map(|v| v.map(ToString::to_string).collect())
|
||||
let mut files: Vec<OsString> = matches
|
||||
.get_many::<OsString>(options::FILE)
|
||||
.map(|v| v.cloned().collect())
|
||||
.unwrap_or_default();
|
||||
let cmode = if fmode.is_some() {
|
||||
// "--reference" and MODE are mutually exclusive
|
||||
// if "--reference" was used MODE needs to be interpreted as another FILE
|
||||
// it wasn't possible to implement this behavior directly with clap
|
||||
files.push(cmode);
|
||||
files.push(OsString::from(cmode));
|
||||
None
|
||||
} else {
|
||||
Some(cmode)
|
||||
@@ -236,6 +235,7 @@ pub fn uu_app() -> Command {
|
||||
Arg::new(options::REFERENCE)
|
||||
.long("reference")
|
||||
.value_hint(clap::ValueHint::FilePath)
|
||||
.value_parser(clap::value_parser!(OsString))
|
||||
.help(translate!("chmod-help-reference")),
|
||||
)
|
||||
.arg(
|
||||
@@ -248,7 +248,8 @@ pub fn uu_app() -> Command {
|
||||
Arg::new(options::FILE)
|
||||
.required_unless_present(options::MODE)
|
||||
.action(ArgAction::Append)
|
||||
.value_hint(clap::ValueHint::AnyPath),
|
||||
.value_hint(clap::ValueHint::AnyPath)
|
||||
.value_parser(clap::value_parser!(OsString)),
|
||||
)
|
||||
// Add common arguments with chgrp, chown & chmod
|
||||
.args(uucore::perms::common_args())
|
||||
@@ -267,11 +268,10 @@ struct Chmoder {
|
||||
}
|
||||
|
||||
impl Chmoder {
|
||||
fn chmod(&self, files: &[String]) -> UResult<()> {
|
||||
fn chmod(&self, files: &[OsString]) -> UResult<()> {
|
||||
let mut r = Ok(());
|
||||
|
||||
for filename in files {
|
||||
let filename = &filename[..];
|
||||
let file = Path::new(filename);
|
||||
if !file.exists() {
|
||||
if file.is_symlink() {
|
||||
@@ -285,18 +285,22 @@ impl Chmoder {
|
||||
}
|
||||
|
||||
if !self.quiet {
|
||||
show!(ChmodError::DanglingSymlink(filename.to_string()));
|
||||
show!(ChmodError::DanglingSymlink(
|
||||
filename.to_string_lossy().to_string()
|
||||
));
|
||||
set_exit_code(1);
|
||||
}
|
||||
|
||||
if self.verbose {
|
||||
println!(
|
||||
"{}",
|
||||
translate!("chmod-verbose-failed-dangling", "file" => filename.quote())
|
||||
translate!("chmod-verbose-failed-dangling", "file" => filename.to_string_lossy().quote())
|
||||
);
|
||||
}
|
||||
} else if !self.quiet {
|
||||
show!(ChmodError::NoSuchFile(filename.to_string()));
|
||||
show!(ChmodError::NoSuchFile(
|
||||
filename.to_string_lossy().to_string()
|
||||
));
|
||||
}
|
||||
// GNU exits with exit code 1 even if -q or --quiet are passed
|
||||
// So we set the exit code, because it hasn't been set yet if `self.quiet` is true.
|
||||
@@ -308,8 +312,8 @@ impl Chmoder {
|
||||
// should not change the permissions in this case
|
||||
continue;
|
||||
}
|
||||
if self.recursive && self.preserve_root && filename == "/" {
|
||||
return Err(ChmodError::PreserveRoot(filename.to_string()).into());
|
||||
if self.recursive && self.preserve_root && file == Path::new("/") {
|
||||
return Err(ChmodError::PreserveRoot("/".to_string()).into());
|
||||
}
|
||||
if self.recursive {
|
||||
r = self.walk_dir_with_context(file, true);
|
||||
|
||||
+18
-12
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use clap::{Arg, ArgAction, Command};
|
||||
use std::ffi::OsString;
|
||||
use std::path::Path;
|
||||
use uucore::LocalizedCommand;
|
||||
use uucore::display::print_verbatim;
|
||||
@@ -26,8 +27,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
|
||||
let line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO));
|
||||
|
||||
let dirnames: Vec<String> = matches
|
||||
.get_many::<String>(options::DIR)
|
||||
let dirnames: Vec<OsString> = matches
|
||||
.get_many::<OsString>(options::DIR)
|
||||
.unwrap_or_default()
|
||||
.cloned()
|
||||
.collect();
|
||||
@@ -47,7 +48,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if p.is_absolute() || path == "/" {
|
||||
if p.is_absolute() || path.as_os_str() == "/" {
|
||||
print!("/");
|
||||
} else {
|
||||
print!(".");
|
||||
@@ -79,6 +80,7 @@ pub fn uu_app() -> Command {
|
||||
Arg::new(options::DIR)
|
||||
.hide(true)
|
||||
.action(ArgAction::Append)
|
||||
.value_hint(clap::ValueHint::AnyPath),
|
||||
.value_hint(clap::ValueHint::AnyPath)
|
||||
.value_parser(clap::value_parser!(OsString)),
|
||||
)
|
||||
}
|
||||
|
||||
+14
-9
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ fn tabstops_parse(s: &str) -> Result<(RemainingMode, Vec<usize>), ParseError> {
|
||||
}
|
||||
|
||||
struct Options {
|
||||
files: Vec<String>,
|
||||
files: Vec<OsString>,
|
||||
tabstops: Vec<usize>,
|
||||
tspaces: String,
|
||||
iflag: bool,
|
||||
@@ -204,9 +204,9 @@ impl Options {
|
||||
.unwrap(); // length of tabstops is guaranteed >= 1
|
||||
let tspaces = " ".repeat(nspaces);
|
||||
|
||||
let files: Vec<String> = match matches.get_many::<String>(options::FILES) {
|
||||
Some(s) => s.map(|v| v.to_string()).collect(),
|
||||
None => vec!["-".to_owned()],
|
||||
let files: Vec<OsString> = match matches.get_many::<OsString>(options::FILES) {
|
||||
Some(s) => s.cloned().collect(),
|
||||
None => vec![OsString::from("-")],
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
@@ -283,16 +283,18 @@ pub fn uu_app() -> Command {
|
||||
Arg::new(options::FILES)
|
||||
.action(ArgAction::Append)
|
||||
.hide(true)
|
||||
.value_hint(clap::ValueHint::FilePath),
|
||||
.value_hint(clap::ValueHint::FilePath)
|
||||
.value_parser(clap::value_parser!(OsString)),
|
||||
)
|
||||
}
|
||||
|
||||
fn open(path: &str) -> UResult<BufReader<Box<dyn Read + 'static>>> {
|
||||
fn open(path: &OsString) -> UResult<BufReader<Box<dyn Read + 'static>>> {
|
||||
let file_buf;
|
||||
if path == "-" {
|
||||
Ok(BufReader::new(Box::new(stdin()) as Box<dyn Read>))
|
||||
} else {
|
||||
file_buf = File::open(path).map_err_context(|| path.to_string())?;
|
||||
let path_ref = Path::new(path);
|
||||
file_buf = File::open(path_ref).map_err_context(|| path.to_string_lossy().to_string())?;
|
||||
Ok(BufReader::new(Box::new(file_buf) as Box<dyn Read>))
|
||||
}
|
||||
}
|
||||
@@ -446,7 +448,7 @@ fn expand(options: &Options) -> UResult<()> {
|
||||
if Path::new(file).is_dir() {
|
||||
show_error!(
|
||||
"{}",
|
||||
translate!("expand-error-is-directory", "file" => file)
|
||||
translate!("expand-error-is-directory", "file" => file.to_string_lossy())
|
||||
);
|
||||
set_exit_code(1);
|
||||
continue;
|
||||
|
||||
+32
-25
@@ -6,8 +6,10 @@
|
||||
// spell-checker:ignore (ToDO) PSKIP linebreak ostream parasplit tabwidth xanti xprefix
|
||||
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter, Read, Stdout, Write, stdin, stdout};
|
||||
use std::path::Path;
|
||||
use uucore::display::Quotable;
|
||||
use uucore::error::{FromIo, UResult, USimpleError};
|
||||
use uucore::translate;
|
||||
@@ -205,27 +207,27 @@ impl FmtOptions {
|
||||
///
|
||||
/// A `UResult<()>` indicating success or failure.
|
||||
fn process_file(
|
||||
file_name: &str,
|
||||
file_name: &OsString,
|
||||
fmt_opts: &FmtOptions,
|
||||
ostream: &mut BufWriter<Stdout>,
|
||||
) -> UResult<()> {
|
||||
let mut fp = BufReader::new(match file_name {
|
||||
"-" => Box::new(stdin()) as Box<dyn Read + 'static>,
|
||||
_ => {
|
||||
let f = File::open(file_name).map_err_context(
|
||||
|| translate!("fmt-error-cannot-open-for-reading", "file" => file_name.quote()),
|
||||
)?;
|
||||
if f.metadata()
|
||||
.map_err_context(
|
||||
|| translate!("fmt-error-cannot-get-metadata", "file" => file_name.quote()),
|
||||
)?
|
||||
.is_dir()
|
||||
{
|
||||
return Err(FmtError::ReadError.into());
|
||||
}
|
||||
|
||||
Box::new(f) as Box<dyn Read + 'static>
|
||||
let mut fp = BufReader::new(if file_name == "-" {
|
||||
Box::new(stdin()) as Box<dyn Read + 'static>
|
||||
} else {
|
||||
let path = Path::new(file_name);
|
||||
let f = File::open(path).map_err_context(
|
||||
|| translate!("fmt-error-cannot-open-for-reading", "file" => path.quote()),
|
||||
)?;
|
||||
if f.metadata()
|
||||
.map_err_context(
|
||||
|| translate!("fmt-error-cannot-get-metadata", "file" => path.quote()),
|
||||
)?
|
||||
.is_dir()
|
||||
{
|
||||
return Err(FmtError::ReadError.into());
|
||||
}
|
||||
|
||||
Box::new(f) as Box<dyn Read + 'static>
|
||||
});
|
||||
|
||||
let p_stream = ParagraphStream::new(fmt_opts, &mut fp);
|
||||
@@ -258,23 +260,24 @@ fn process_file(
|
||||
/// # Returns
|
||||
/// A `UResult<()>` with the file names, or an error if one of the file names could not be parsed
|
||||
/// (e.g., it is given as a negative number not in the first argument and not after a --
|
||||
fn extract_files(matches: &ArgMatches) -> UResult<Vec<String>> {
|
||||
fn extract_files(matches: &ArgMatches) -> UResult<Vec<OsString>> {
|
||||
let in_first_pos = matches
|
||||
.index_of(options::FILES_OR_WIDTH)
|
||||
.is_some_and(|x| x == 1);
|
||||
let is_neg = |s: &str| s.parse::<isize>().is_ok_and(|w| w < 0);
|
||||
|
||||
let files: UResult<Vec<String>> = matches
|
||||
.get_many::<String>(options::FILES_OR_WIDTH)
|
||||
let files: UResult<Vec<OsString>> = matches
|
||||
.get_many::<OsString>(options::FILES_OR_WIDTH)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.filter_map(|(i, x)| {
|
||||
if is_neg(x) {
|
||||
let x_str = x.to_string_lossy();
|
||||
if is_neg(&x_str) {
|
||||
if in_first_pos && i == 0 {
|
||||
None
|
||||
} else {
|
||||
let first_num = x
|
||||
let first_num = x_str
|
||||
.chars()
|
||||
.nth(1)
|
||||
.expect("a negative number should be at least two characters long");
|
||||
@@ -287,7 +290,7 @@ fn extract_files(matches: &ArgMatches) -> UResult<Vec<String>> {
|
||||
.collect();
|
||||
|
||||
if files.as_ref().is_ok_and(|f| f.is_empty()) {
|
||||
Ok(vec!["-".into()])
|
||||
Ok(vec![OsString::from("-")])
|
||||
} else {
|
||||
files
|
||||
}
|
||||
@@ -304,8 +307,11 @@ fn extract_width(matches: &ArgMatches) -> UResult<Option<usize>> {
|
||||
}
|
||||
|
||||
if let Some(1) = matches.index_of(options::FILES_OR_WIDTH) {
|
||||
let width_arg = matches.get_one::<String>(options::FILES_OR_WIDTH).unwrap();
|
||||
if let Some(num) = width_arg.strip_prefix('-') {
|
||||
let width_arg = matches
|
||||
.get_one::<OsString>(options::FILES_OR_WIDTH)
|
||||
.unwrap();
|
||||
let width_str = width_arg.to_string_lossy();
|
||||
if let Some(num) = width_str.strip_prefix('-') {
|
||||
Ok(num.parse::<usize>().ok())
|
||||
} else {
|
||||
// will be treated as a file name
|
||||
@@ -456,6 +462,7 @@ pub fn uu_app() -> Command {
|
||||
.action(ArgAction::Append)
|
||||
.value_name("FILES")
|
||||
.value_hint(clap::ValueHint::FilePath)
|
||||
.value_parser(clap::value_parser!(OsString))
|
||||
.allow_negative_numbers(true),
|
||||
)
|
||||
}
|
||||
|
||||
+81
-74
@@ -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]
|
||||
|
||||
@@ -10,6 +10,7 @@ mod mode;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use file_diff::diff;
|
||||
use filetime::{FileTime, set_file_times};
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Debug;
|
||||
use std::fs::File;
|
||||
use std::fs::{self, metadata};
|
||||
@@ -168,9 +169,9 @@ static ARG_FILES: &str = "files";
|
||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let matches = uu_app().get_matches_from_localized(args);
|
||||
|
||||
let paths: Vec<String> = matches
|
||||
.get_many::<String>(ARG_FILES)
|
||||
.map(|v| v.map(ToString::to_string).collect())
|
||||
let paths: Vec<OsString> = matches
|
||||
.get_many::<OsString>(ARG_FILES)
|
||||
.map(|v| v.cloned().collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
let behavior = behavior(&matches)?;
|
||||
@@ -303,7 +304,8 @@ pub fn uu_app() -> Command {
|
||||
Arg::new(ARG_FILES)
|
||||
.action(ArgAction::Append)
|
||||
.num_args(1..)
|
||||
.value_hint(clap::ValueHint::AnyPath),
|
||||
.value_hint(clap::ValueHint::AnyPath)
|
||||
.value_parser(clap::value_parser!(OsString)),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -435,7 +437,7 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
|
||||
///
|
||||
/// Returns a Result type with the Err variant containing the error message.
|
||||
///
|
||||
fn directory(paths: &[String], b: &Behavior) -> UResult<()> {
|
||||
fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
|
||||
if paths.is_empty() {
|
||||
Err(InstallError::DirNeedsArg.into())
|
||||
} else {
|
||||
@@ -518,7 +520,7 @@ fn is_potential_directory_path(path: &Path) -> bool {
|
||||
/// Returns a Result type with the Err variant containing the error message.
|
||||
///
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
|
||||
fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
|
||||
// first check that paths contains at least one element
|
||||
if paths.is_empty() {
|
||||
return Err(UUsageError::new(
|
||||
@@ -528,7 +530,7 @@ fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
|
||||
}
|
||||
if b.no_target_dir && paths.len() > 2 {
|
||||
return Err(InstallError::ExtraOperand(
|
||||
paths[2].clone(),
|
||||
paths[2].to_string_lossy().into_owned(),
|
||||
format_usage(&translate!("install-usage")),
|
||||
)
|
||||
.into());
|
||||
@@ -544,7 +546,7 @@ fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
|
||||
if paths.is_empty() {
|
||||
return Err(UUsageError::new(
|
||||
1,
|
||||
translate!("install-error-missing-destination-operand", "path" => last_path.to_str().unwrap()),
|
||||
translate!("install-error-missing-destination-operand", "path" => last_path.to_string_lossy()),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -566,10 +568,18 @@ fn standard(mut paths: Vec<String>, b: &Behavior) -> UResult<()> {
|
||||
|
||||
if let Some(to_create) = to_create {
|
||||
// if the path ends in /, remove it
|
||||
let to_create = if to_create.to_string_lossy().ends_with('/') {
|
||||
Path::new(to_create.to_str().unwrap().trim_end_matches('/'))
|
||||
} else {
|
||||
to_create
|
||||
let to_create_owned;
|
||||
let to_create = match uucore::os_str_as_bytes(to_create.as_os_str()) {
|
||||
Ok(path_bytes) if path_bytes.ends_with(b"/") => {
|
||||
let mut trimmed_bytes = path_bytes;
|
||||
while trimmed_bytes.ends_with(b"/") {
|
||||
trimmed_bytes = &trimmed_bytes[..trimmed_bytes.len() - 1];
|
||||
}
|
||||
let trimmed_os_str = std::ffi::OsStr::from_bytes(trimmed_bytes);
|
||||
to_create_owned = PathBuf::from(trimmed_os_str);
|
||||
to_create_owned.as_path()
|
||||
}
|
||||
_ => to_create,
|
||||
};
|
||||
|
||||
if !to_create.exists() {
|
||||
@@ -835,7 +845,7 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> {
|
||||
///
|
||||
fn strip_file(to: &Path, b: &Behavior) -> UResult<()> {
|
||||
// Check if the filename starts with a hyphen and adjust the path
|
||||
let to_str = to.as_os_str().to_str().unwrap_or_default();
|
||||
let to_str = to.to_string_lossy();
|
||||
let to = if to_str.starts_with('-') {
|
||||
let mut new_path = PathBuf::from(".");
|
||||
new_path.push(to);
|
||||
@@ -1085,7 +1095,7 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool {
|
||||
}
|
||||
|
||||
// Check if the contents of the source and destination files differ.
|
||||
if !diff(from.to_str().unwrap(), to.to_str().unwrap()) {
|
||||
if !diff(&from.to_string_lossy(), &to.to_string_lossy()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+15
-7
@@ -413,7 +413,7 @@ impl Line {
|
||||
|
||||
struct State<'a> {
|
||||
key: usize,
|
||||
file_name: &'a str,
|
||||
file_name: &'a OsString,
|
||||
file_num: FileNum,
|
||||
print_unpaired: bool,
|
||||
lines: Split<Box<dyn BufRead + 'a>>,
|
||||
@@ -427,7 +427,7 @@ struct State<'a> {
|
||||
impl<'a> State<'a> {
|
||||
fn new(
|
||||
file_num: FileNum,
|
||||
name: &'a str,
|
||||
name: &'a OsString,
|
||||
stdin: &'a Stdin,
|
||||
key: usize,
|
||||
line_ending: LineEnding,
|
||||
@@ -436,7 +436,8 @@ impl<'a> State<'a> {
|
||||
let file_buf = if name == "-" {
|
||||
Box::new(stdin.lock()) as Box<dyn BufRead>
|
||||
} else {
|
||||
let file = File::open(name).map_err_context(|| format!("{}", name.maybe_quote()))?;
|
||||
let file = File::open(name)
|
||||
.map_err_context(|| format!("{}", name.to_string_lossy().maybe_quote()))?;
|
||||
Box::new(BufReader::new(file)) as Box<dyn BufRead>
|
||||
};
|
||||
|
||||
@@ -639,7 +640,7 @@ impl<'a> State<'a> {
|
||||
&& (input.check_order == CheckOrder::Enabled
|
||||
|| (self.has_unpaired && !self.has_failed))
|
||||
{
|
||||
let err_msg = translate!("join-error-not-sorted", "file" => self.file_name.maybe_quote(), "line_num" => self.line_num, "content" => String::from_utf8_lossy(&line.string));
|
||||
let err_msg = translate!("join-error-not-sorted", "file" => self.file_name.to_string_lossy().maybe_quote(), "line_num" => self.line_num, "content" => String::from_utf8_lossy(&line.string));
|
||||
// This is fatal if the check is enabled.
|
||||
if input.check_order == CheckOrder::Enabled {
|
||||
return Err(JoinError::UnorderedInput(err_msg));
|
||||
@@ -826,8 +827,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
|
||||
let settings = parse_settings(&matches)?;
|
||||
|
||||
let file1 = matches.get_one::<String>("file1").unwrap();
|
||||
let file2 = matches.get_one::<String>("file2").unwrap();
|
||||
let file1 = matches.get_one::<OsString>("file1").unwrap();
|
||||
let file2 = matches.get_one::<OsString>("file2").unwrap();
|
||||
|
||||
if file1 == "-" && file2 == "-" {
|
||||
return Err(USimpleError::new(
|
||||
@@ -951,6 +952,7 @@ pub fn uu_app() -> Command {
|
||||
.required(true)
|
||||
.value_name("FILE1")
|
||||
.value_hint(clap::ValueHint::FilePath)
|
||||
.value_parser(clap::value_parser!(OsString))
|
||||
.hide(true),
|
||||
)
|
||||
.arg(
|
||||
@@ -958,11 +960,17 @@ pub fn uu_app() -> Command {
|
||||
.required(true)
|
||||
.value_name("FILE2")
|
||||
.value_hint(clap::ValueHint::FilePath)
|
||||
.value_parser(clap::value_parser!(OsString))
|
||||
.hide(true),
|
||||
)
|
||||
}
|
||||
|
||||
fn exec<Sep: Separator>(file1: &str, file2: &str, settings: Settings, sep: Sep) -> UResult<()> {
|
||||
fn exec<Sep: Separator>(
|
||||
file1: &OsString,
|
||||
file2: &OsString,
|
||||
settings: Settings,
|
||||
sep: Sep,
|
||||
) -> UResult<()> {
|
||||
let stdin = stdin();
|
||||
|
||||
let mut state1 = State::new(
|
||||
|
||||
+18
-16
@@ -30,11 +30,11 @@ use uucore::fs::{MissingHandling, ResolveMode, canonicalize};
|
||||
pub struct Settings {
|
||||
overwrite: OverwriteMode,
|
||||
backup: BackupMode,
|
||||
suffix: String,
|
||||
suffix: OsString,
|
||||
symbolic: bool,
|
||||
relative: bool,
|
||||
logical: bool,
|
||||
target_dir: Option<String>,
|
||||
target_dir: Option<PathBuf>,
|
||||
no_target_dir: bool,
|
||||
no_dereference: bool,
|
||||
verbose: bool,
|
||||
@@ -61,7 +61,7 @@ enum LnError {
|
||||
#[error("{}", translate!("ln-error-missing-destination", "operand" => _0.quote()))]
|
||||
MissingDestination(PathBuf),
|
||||
|
||||
#[error("{}", translate!("ln-error-extra-operand", "operand" => format!("{_0:?}").trim_matches('"'), "program" => _1.clone()))]
|
||||
#[error("{}", translate!("ln-error-extra-operand", "operand" => _0.to_string_lossy(), "program" => _1.clone()))]
|
||||
ExtraOperand(OsString, String),
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
/* the list of files */
|
||||
|
||||
let paths: Vec<PathBuf> = matches
|
||||
.get_many::<String>(ARG_FILES)
|
||||
.get_many::<OsString>(ARG_FILES)
|
||||
.unwrap()
|
||||
.map(PathBuf::from)
|
||||
.collect();
|
||||
@@ -126,13 +126,13 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let settings = Settings {
|
||||
overwrite: overwrite_mode,
|
||||
backup: backup_mode,
|
||||
suffix: backup_suffix,
|
||||
suffix: OsString::from(backup_suffix),
|
||||
symbolic,
|
||||
logical,
|
||||
relative: matches.get_flag(options::RELATIVE),
|
||||
target_dir: matches
|
||||
.get_one::<String>(options::TARGET_DIRECTORY)
|
||||
.map(String::from),
|
||||
.get_one::<OsString>(options::TARGET_DIRECTORY)
|
||||
.map(PathBuf::from),
|
||||
no_target_dir: matches.get_flag(options::NO_TARGET_DIRECTORY),
|
||||
no_dereference: matches.get_flag(options::NO_DEREFERENCE),
|
||||
verbose: matches.get_flag(options::VERBOSE),
|
||||
@@ -210,6 +210,7 @@ pub fn uu_app() -> Command {
|
||||
.help(translate!("ln-help-target-directory"))
|
||||
.value_name("DIRECTORY")
|
||||
.value_hint(clap::ValueHint::DirPath)
|
||||
.value_parser(clap::value_parser!(OsString))
|
||||
.conflicts_with(options::NO_TARGET_DIRECTORY),
|
||||
)
|
||||
.arg(
|
||||
@@ -238,6 +239,7 @@ pub fn uu_app() -> Command {
|
||||
Arg::new(ARG_FILES)
|
||||
.action(ArgAction::Append)
|
||||
.value_hint(clap::ValueHint::AnyPath)
|
||||
.value_parser(clap::value_parser!(OsString))
|
||||
.required(true)
|
||||
.num_args(1..),
|
||||
)
|
||||
@@ -245,9 +247,9 @@ pub fn uu_app() -> Command {
|
||||
|
||||
fn exec(files: &[PathBuf], settings: &Settings) -> UResult<()> {
|
||||
// Handle cases where we create links in a directory first.
|
||||
if let Some(ref name) = settings.target_dir {
|
||||
if let Some(ref target_path) = settings.target_dir {
|
||||
// 4th form: a directory is specified by -t.
|
||||
return link_files_in_dir(files, &PathBuf::from(name), settings);
|
||||
return link_files_in_dir(files, target_path, settings);
|
||||
}
|
||||
if !settings.no_target_dir {
|
||||
if files.len() == 1 {
|
||||
@@ -445,16 +447,16 @@ fn link(src: &Path, dst: &Path, settings: &Settings) -> UResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn simple_backup_path(path: &Path, suffix: &str) -> PathBuf {
|
||||
let mut p = path.as_os_str().to_str().unwrap().to_owned();
|
||||
p.push_str(suffix);
|
||||
PathBuf::from(p)
|
||||
fn simple_backup_path(path: &Path, suffix: &OsString) -> PathBuf {
|
||||
let mut file_name = path.file_name().unwrap_or_default().to_os_string();
|
||||
file_name.push(suffix);
|
||||
path.with_file_name(file_name)
|
||||
}
|
||||
|
||||
fn numbered_backup_path(path: &Path) -> PathBuf {
|
||||
let mut i: u64 = 1;
|
||||
loop {
|
||||
let new_path = simple_backup_path(path, &format!(".~{i}~"));
|
||||
let new_path = simple_backup_path(path, &OsString::from(format!(".~{i}~")));
|
||||
if !new_path.exists() {
|
||||
return new_path;
|
||||
}
|
||||
@@ -462,8 +464,8 @@ fn numbered_backup_path(path: &Path) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
fn existing_backup_path(path: &Path, suffix: &str) -> PathBuf {
|
||||
let test_path = simple_backup_path(path, ".~1~");
|
||||
fn existing_backup_path(path: &Path, suffix: &OsString) -> PathBuf {
|
||||
let test_path = simple_backup_path(path, &OsString::from(".~1~"));
|
||||
if test_path.exists() {
|
||||
return numbered_backup_path(path);
|
||||
}
|
||||
|
||||
+50
-38
@@ -13,7 +13,7 @@ use uucore::format_usage;
|
||||
use uucore::translate;
|
||||
|
||||
use std::env;
|
||||
use std::ffi::OsStr;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io::ErrorKind;
|
||||
use std::iter;
|
||||
use std::path::{MAIN_SEPARATOR, Path, PathBuf};
|
||||
@@ -105,7 +105,7 @@ pub struct Options {
|
||||
pub treat_as_template: bool,
|
||||
|
||||
/// The template to use for the name of the temporary file.
|
||||
pub template: String,
|
||||
pub template: OsString,
|
||||
}
|
||||
|
||||
impl Options {
|
||||
@@ -123,12 +123,12 @@ impl Options {
|
||||
.ok()
|
||||
.map_or_else(env::temp_dir, PathBuf::from),
|
||||
});
|
||||
let (tmpdir, template) = match matches.get_one::<String>(ARG_TEMPLATE) {
|
||||
let (tmpdir, template) = match matches.get_one::<OsString>(ARG_TEMPLATE) {
|
||||
// If no template argument is given, `--tmpdir` is implied.
|
||||
None => {
|
||||
let tmpdir = Some(tmpdir.unwrap_or_else(env::temp_dir));
|
||||
let template = DEFAULT_TEMPLATE;
|
||||
(tmpdir, template.to_string())
|
||||
(tmpdir, OsString::from(template))
|
||||
}
|
||||
Some(template) => {
|
||||
let tmpdir = if env::var(TMPDIR_ENV_VAR).is_ok() && matches.get_flag(OPT_T) {
|
||||
@@ -142,7 +142,7 @@ impl Options {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(tmpdir, template.to_string())
|
||||
(tmpdir, template.clone())
|
||||
}
|
||||
};
|
||||
Self {
|
||||
@@ -200,23 +200,30 @@ fn find_last_contiguous_block_of_xs(s: &str) -> Option<(usize, usize)> {
|
||||
|
||||
impl Params {
|
||||
fn from(options: Options) -> Result<Self, MkTempError> {
|
||||
// Convert OsString template to string for processing
|
||||
let Some(template_str) = options.template.to_str() else {
|
||||
// For non-UTF-8 templates, return an error
|
||||
return Err(MkTempError::InvalidTemplate(
|
||||
options.template.to_string_lossy().into_owned(),
|
||||
));
|
||||
};
|
||||
|
||||
// The template argument must end in 'X' if a suffix option is given.
|
||||
if options.suffix.is_some() && !options.template.ends_with('X') {
|
||||
return Err(MkTempError::MustEndInX(options.template));
|
||||
if options.suffix.is_some() && !template_str.ends_with('X') {
|
||||
return Err(MkTempError::MustEndInX(template_str.to_string()));
|
||||
}
|
||||
|
||||
// Get the start and end indices of the randomized part of the template.
|
||||
//
|
||||
// For example, if the template is "abcXXXXyz", then `i` is 3 and `j` is 7.
|
||||
let Some((i, j)) = find_last_contiguous_block_of_xs(&options.template) else {
|
||||
let Some((i, j)) = find_last_contiguous_block_of_xs(template_str) else {
|
||||
let s = match options.suffix {
|
||||
// If a suffix is specified, the error message includes the template without the suffix.
|
||||
Some(_) => options
|
||||
.template
|
||||
Some(_) => template_str
|
||||
.chars()
|
||||
.take(options.template.len())
|
||||
.take(template_str.len())
|
||||
.collect::<String>(),
|
||||
None => options.template,
|
||||
None => template_str.to_string(),
|
||||
};
|
||||
return Err(MkTempError::TooFewXs(s));
|
||||
};
|
||||
@@ -227,35 +234,36 @@ impl Params {
|
||||
// then `prefix` is "a/b/c/d".
|
||||
let tmpdir = options.tmpdir;
|
||||
let prefix_from_option = tmpdir.clone().unwrap_or_default();
|
||||
let prefix_from_template = &options.template[..i];
|
||||
let prefix = Path::new(&prefix_from_option)
|
||||
.join(prefix_from_template)
|
||||
.display()
|
||||
.to_string();
|
||||
let prefix_from_template = &template_str[..i];
|
||||
let prefix_path = Path::new(&prefix_from_option).join(prefix_from_template);
|
||||
if options.treat_as_template && prefix_from_template.contains(MAIN_SEPARATOR) {
|
||||
return Err(MkTempError::PrefixContainsDirSeparator(options.template));
|
||||
return Err(MkTempError::PrefixContainsDirSeparator(
|
||||
template_str.to_string(),
|
||||
));
|
||||
}
|
||||
if tmpdir.is_some() && Path::new(prefix_from_template).is_absolute() {
|
||||
return Err(MkTempError::InvalidTemplate(options.template));
|
||||
return Err(MkTempError::InvalidTemplate(template_str.to_string()));
|
||||
}
|
||||
|
||||
// Split the parent directory from the file part of the prefix.
|
||||
//
|
||||
// For example, if `prefix` is "a/b/c/d", then `directory` is
|
||||
// "a/b/c" is `prefix` gets reassigned to "d".
|
||||
let (directory, prefix) = if prefix.ends_with(MAIN_SEPARATOR) {
|
||||
(prefix, String::new())
|
||||
} else {
|
||||
let path = Path::new(&prefix);
|
||||
let directory = match path.parent() {
|
||||
None => String::new(),
|
||||
Some(d) => d.display().to_string(),
|
||||
};
|
||||
let prefix = match path.file_name() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_str().unwrap().to_string(),
|
||||
};
|
||||
(directory, prefix)
|
||||
// For example, if `prefix_path` is "a/b/c/d", then `directory` is
|
||||
// "a/b/c" and `prefix` gets reassigned to "d".
|
||||
let (directory, prefix) = {
|
||||
let prefix_str = prefix_path.to_string_lossy();
|
||||
if prefix_str.ends_with(MAIN_SEPARATOR) {
|
||||
(prefix_path, String::new())
|
||||
} else {
|
||||
let directory = match prefix_path.parent() {
|
||||
None => PathBuf::new(),
|
||||
Some(d) => d.to_path_buf(),
|
||||
};
|
||||
let prefix = match prefix_path.file_name() {
|
||||
None => String::new(),
|
||||
Some(f) => f.to_str().unwrap().to_string(),
|
||||
};
|
||||
(directory, prefix)
|
||||
}
|
||||
};
|
||||
|
||||
// Combine the suffix from the template with the suffix given as an option.
|
||||
@@ -263,7 +271,7 @@ impl Params {
|
||||
// For example, if the suffix command-line argument is ".txt" and
|
||||
// the template is "XXXabc", then `suffix` is "abc.txt".
|
||||
let suffix_from_option = options.suffix.unwrap_or_default();
|
||||
let suffix_from_template = &options.template[j..];
|
||||
let suffix_from_template = &template_str[j..];
|
||||
let suffix = format!("{suffix_from_template}{suffix_from_option}");
|
||||
if suffix.contains(MAIN_SEPARATOR) {
|
||||
return Err(MkTempError::SuffixContainsDirSeparator(suffix));
|
||||
@@ -276,7 +284,7 @@ impl Params {
|
||||
let num_rand_chars = j - i;
|
||||
|
||||
Ok(Self {
|
||||
directory: directory.into(),
|
||||
directory,
|
||||
prefix,
|
||||
num_rand_chars,
|
||||
suffix,
|
||||
@@ -360,7 +368,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
// If POSIXLY_CORRECT was set, template MUST be the last argument.
|
||||
if matches.contains_id(ARG_TEMPLATE) {
|
||||
// Template argument was provided, check if was the last one.
|
||||
if args.last().unwrap() != OsStr::new(&options.template) {
|
||||
if args.last().unwrap() != &options.template {
|
||||
return Err(Box::new(MkTempError::TooManyTemplates));
|
||||
}
|
||||
}
|
||||
@@ -457,7 +465,11 @@ pub fn uu_app() -> Command {
|
||||
.help(translate!("mktemp-help-t"))
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(Arg::new(ARG_TEMPLATE).num_args(..=1))
|
||||
.arg(
|
||||
Arg::new(ARG_TEMPLATE)
|
||||
.num_args(..=1)
|
||||
.value_parser(clap::value_parser!(OsString)),
|
||||
)
|
||||
}
|
||||
|
||||
fn dry_exec(tmpdir: &Path, prefix: &str, rand: usize, suffix: &str) -> UResult<PathBuf> {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user