[go: up one dir, main page]

uucore/
lib.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5//! library ~ (core/bundler file)
6// #![deny(missing_docs)] //TODO: enable this
7//
8// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal myutil logind
9
10// * feature-gated external crates (re-shared as public internal modules)
11#[cfg(feature = "libc")]
12pub extern crate libc;
13#[cfg(all(feature = "windows-sys", target_os = "windows"))]
14pub extern crate windows_sys;
15
16//## internal modules
17
18mod features; // feature-gated code modules
19mod macros; // crate macros (macro_rules-type; exported to `crate::...`)
20mod mods; // core cross-platform modules
21
22pub use uucore_procs::*;
23
24// * cross-platform modules
25pub use crate::mods::clap_localization;
26pub use crate::mods::display;
27pub use crate::mods::error;
28#[cfg(feature = "fs")]
29pub use crate::mods::io;
30pub use crate::mods::line_ending;
31pub use crate::mods::locale;
32pub use crate::mods::os;
33pub use crate::mods::panic;
34pub use crate::mods::posix;
35
36// * feature-gated modules
37#[cfg(feature = "backup-control")]
38pub use crate::features::backup_control;
39#[cfg(feature = "benchmark")]
40pub use crate::features::benchmark;
41#[cfg(feature = "buf-copy")]
42pub use crate::features::buf_copy;
43#[cfg(feature = "checksum")]
44pub use crate::features::checksum;
45#[cfg(feature = "colors")]
46pub use crate::features::colors;
47#[cfg(feature = "encoding")]
48pub use crate::features::encoding;
49#[cfg(feature = "extendedbigdecimal")]
50pub use crate::features::extendedbigdecimal;
51#[cfg(feature = "fast-inc")]
52pub use crate::features::fast_inc;
53#[cfg(feature = "format")]
54pub use crate::features::format;
55#[cfg(feature = "fs")]
56pub use crate::features::fs;
57#[cfg(feature = "hardware")]
58pub use crate::features::hardware;
59#[cfg(feature = "i18n-common")]
60pub use crate::features::i18n;
61#[cfg(feature = "lines")]
62pub use crate::features::lines;
63#[cfg(any(
64    feature = "parser",
65    feature = "parser-num",
66    feature = "parser-size",
67    feature = "parser-glob"
68))]
69pub use crate::features::parser;
70#[cfg(feature = "quoting-style")]
71pub use crate::features::quoting_style;
72#[cfg(feature = "ranges")]
73pub use crate::features::ranges;
74#[cfg(feature = "ringbuffer")]
75pub use crate::features::ringbuffer;
76#[cfg(feature = "sum")]
77pub use crate::features::sum;
78#[cfg(feature = "feat_systemd_logind")]
79pub use crate::features::systemd_logind;
80#[cfg(feature = "time")]
81pub use crate::features::time;
82#[cfg(feature = "update-control")]
83pub use crate::features::update_control;
84#[cfg(feature = "uptime")]
85pub use crate::features::uptime;
86#[cfg(feature = "version-cmp")]
87pub use crate::features::version_cmp;
88
89// * (platform-specific) feature-gated modules
90// ** non-windows (i.e. Unix + Fuchsia)
91#[cfg(all(not(windows), feature = "mode"))]
92pub use crate::features::mode;
93// ** unix-only
94#[cfg(all(unix, feature = "entries"))]
95pub use crate::features::entries;
96#[cfg(all(unix, feature = "perms"))]
97pub use crate::features::perms;
98#[cfg(all(unix, any(feature = "pipes", feature = "buf-copy")))]
99pub use crate::features::pipes;
100#[cfg(all(unix, feature = "process"))]
101pub use crate::features::process;
102#[cfg(target_os = "linux")]
103pub use crate::features::safe_traversal;
104#[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))]
105pub use crate::features::signals;
106#[cfg(all(
107    unix,
108    not(target_os = "android"),
109    not(target_os = "fuchsia"),
110    not(target_os = "openbsd"),
111    not(target_os = "redox"),
112    feature = "utmpx"
113))]
114pub use crate::features::utmpx;
115// ** windows-only
116#[cfg(all(windows, feature = "wide"))]
117pub use crate::features::wide;
118
119#[cfg(feature = "fsext")]
120pub use crate::features::fsext;
121
122#[cfg(all(unix, feature = "fsxattr"))]
123pub use crate::features::fsxattr;
124
125#[cfg(all(target_os = "linux", feature = "selinux"))]
126pub use crate::features::selinux;
127
128//## core functions
129
130#[cfg(unix)]
131use nix::errno::Errno;
132#[cfg(unix)]
133use nix::sys::signal::{
134    SaFlags, SigAction, SigHandler::SigDfl, SigSet, Signal::SIGBUS, Signal::SIGSEGV, sigaction,
135};
136use std::borrow::Cow;
137use std::ffi::{OsStr, OsString};
138use std::io::{BufRead, BufReader};
139use std::iter;
140#[cfg(unix)]
141use std::os::unix::ffi::{OsStrExt, OsStringExt};
142use std::str;
143use std::str::Utf8Chunk;
144use std::sync::{LazyLock, atomic::Ordering};
145
146/// Disables the custom signal handlers installed by Rust for stack-overflow handling. With those custom signal handlers processes ignore the first SIGBUS and SIGSEGV signal they receive.
147/// See <https://github.com/rust-lang/rust/blob/8ac1525e091d3db28e67adcbbd6db1e1deaa37fb/src/libstd/sys/unix/stack_overflow.rs#L71-L92> for details.
148#[cfg(unix)]
149pub fn disable_rust_signal_handlers() -> Result<(), Errno> {
150    unsafe {
151        sigaction(
152            SIGSEGV,
153            &SigAction::new(SigDfl, SaFlags::empty(), SigSet::all()),
154        )
155    }?;
156    unsafe {
157        sigaction(
158            SIGBUS,
159            &SigAction::new(SigDfl, SaFlags::empty(), SigSet::all()),
160        )
161    }?;
162    Ok(())
163}
164
165pub fn get_canonical_util_name(util_name: &str) -> &str {
166    // remove the "uu_" prefix
167    let util_name = &util_name[3..];
168    match util_name {
169        // uu_test aliases - '[' is an alias for test
170        "[" => "test",
171
172        // hashsum aliases - all these hash commands are aliases for hashsum
173        "md5sum" | "sha1sum" | "sha224sum" | "sha256sum" | "sha384sum" | "sha512sum" | "b2sum" => {
174            "hashsum"
175        }
176
177        "dir" => "ls", // dir is an alias for ls
178
179        // Default case - return the util name as is
180        _ => util_name,
181    }
182}
183
184/// Execute utility code for `util`.
185///
186/// This macro expands to a main function that invokes the `uumain` function in `util`
187/// Exits with code returned by `uumain`.
188#[macro_export]
189macro_rules! bin {
190    ($util:ident) => {
191        pub fn main() {
192            use std::io::Write;
193            use uucore::locale;
194            // suppress extraneous error output for SIGPIPE failures/panics
195            uucore::panic::mute_sigpipe_panic();
196            locale::setup_localization(uucore::get_canonical_util_name(stringify!($util)))
197                .unwrap_or_else(|err| {
198                    match err {
199                        uucore::locale::LocalizationError::ParseResource {
200                            error: err_msg,
201                            snippet,
202                        } => eprintln!("Localization parse error at {snippet}: {err_msg:?}"),
203                        other => eprintln!("Could not init the localization system: {other}"),
204                    }
205                    std::process::exit(99)
206                });
207
208            // execute utility code
209            let code = $util::uumain(uucore::args_os());
210            // (defensively) flush stdout for utility prior to exit; see <https://github.com/rust-lang/rust/issues/23818>
211            if let Err(e) = std::io::stdout().flush() {
212                eprintln!("Error flushing stdout: {e}");
213            }
214
215            std::process::exit(code);
216        }
217    };
218}
219
220/// Generate the version string for clap.
221///
222/// The generated string has the format `(<project name>) <version>`, for
223/// example: "(uutils coreutils) 0.30.0". clap will then prefix it with the util name.
224#[macro_export]
225macro_rules! crate_version {
226    () => {
227        concat!("(uutils coreutils) ", env!("CARGO_PKG_VERSION"))
228    };
229}
230
231/// Generate the usage string for clap.
232///
233/// This function does two things. It indents all but the first line to align
234/// the lines because clap adds "Usage: " to the first line. And it replaces
235/// all occurrences of `{}` with the execution phrase and returns the resulting
236/// `String`. It does **not** support more advanced formatting features such
237/// as `{0}`.
238pub fn format_usage(s: &str) -> String {
239    let s = s.replace('\n', &format!("\n{}", " ".repeat(7)));
240    s.replace("{}", crate::execution_phrase())
241}
242
243/// Creates a localized help template for clap commands.
244///
245/// This function returns a help template that uses the localized
246/// "Usage:" label from the translation files. This ensures consistent
247/// localization across all utilities.
248///
249/// Note: We avoid using clap's `{usage-heading}` placeholder because it is
250/// hardcoded to "Usage:" and cannot be localized. Instead, we manually
251/// construct the usage line with the localized label.
252///
253/// # Parameters
254/// - `util_name`: The name of the utility (for localization setup)
255///
256/// # Example
257/// ```no_run
258/// use clap::Command;
259/// use uucore::localized_help_template;
260///
261/// let app = Command::new("myutil")
262///     .help_template(localized_help_template("myutil"));
263/// ```
264pub fn localized_help_template(util_name: &str) -> clap::builder::StyledStr {
265    use std::io::IsTerminal;
266
267    // Determine if colors should be enabled - same logic as configure_localized_command
268    let colors_enabled = if std::env::var("NO_COLOR").is_ok() {
269        false
270    } else if std::env::var("CLICOLOR_FORCE").is_ok() || std::env::var("FORCE_COLOR").is_ok() {
271        true
272    } else {
273        IsTerminal::is_terminal(&std::io::stdout())
274            && std::env::var("TERM").unwrap_or_default() != "dumb"
275    };
276
277    localized_help_template_with_colors(util_name, colors_enabled)
278}
279
280/// Create a localized help template with explicit color control
281/// This ensures color detection consistency between clap and our template
282pub fn localized_help_template_with_colors(
283    util_name: &str,
284    colors_enabled: bool,
285) -> clap::builder::StyledStr {
286    use std::fmt::Write;
287
288    // Ensure localization is initialized for this utility
289    let _ = crate::locale::setup_localization(util_name);
290
291    // Get the localized "Usage" label
292    let usage_label = crate::locale::translate!("common-usage");
293
294    // Create a styled template
295    let mut template = clap::builder::StyledStr::new();
296
297    // Add the basic template parts
298    writeln!(template, "{{before-help}}{{about-with-newline}}").unwrap();
299
300    // Add styled usage header (bold + underline like clap's default)
301    if colors_enabled {
302        write!(
303            template,
304            "\x1b[1m\x1b[4m{usage_label}:\x1b[0m {{usage}}\n\n"
305        )
306        .unwrap();
307    } else {
308        write!(template, "{usage_label}: {{usage}}\n\n").unwrap();
309    }
310
311    // Add the rest
312    write!(template, "{{all-args}}{{after-help}}").unwrap();
313
314    template
315}
316
317/// Used to check if the utility is the second argument.
318/// Used to check if we were called as a multicall binary (`coreutils <utility>`)
319pub fn get_utility_is_second_arg() -> bool {
320    crate::macros::UTILITY_IS_SECOND_ARG.load(Ordering::SeqCst)
321}
322
323/// Change the value of `UTILITY_IS_SECOND_ARG` to true
324/// Used to specify that the utility is the second argument.
325pub fn set_utility_is_second_arg() {
326    crate::macros::UTILITY_IS_SECOND_ARG.store(true, Ordering::SeqCst);
327}
328
329// args_os() can be expensive to call, it copies all of argv before iterating.
330// So if we want only the first arg or so it's overkill. We cache it.
331#[cfg(windows)]
332static ARGV: LazyLock<Vec<OsString>> = LazyLock::new(|| wild::args_os().collect());
333#[cfg(not(windows))]
334static ARGV: LazyLock<Vec<OsString>> = LazyLock::new(|| std::env::args_os().collect());
335
336static UTIL_NAME: LazyLock<String> = LazyLock::new(|| {
337    let base_index = usize::from(get_utility_is_second_arg());
338    let is_man = usize::from(ARGV[base_index].eq("manpage"));
339    let argv_index = base_index + is_man;
340
341    // Strip directory path to show only utility name
342    // (e.g., "mkdir" instead of "./target/debug/mkdir")
343    // in version output, error messages, and other user-facing output
344    std::path::Path::new(&ARGV[argv_index])
345        .file_name()
346        .unwrap_or(&ARGV[argv_index])
347        .to_string_lossy()
348        .into_owned()
349});
350
351/// Derive the utility name.
352pub fn util_name() -> &'static str {
353    &UTIL_NAME
354}
355
356static EXECUTION_PHRASE: LazyLock<String> = LazyLock::new(|| {
357    if get_utility_is_second_arg() {
358        ARGV.iter()
359            .take(2)
360            .map(|os_str| os_str.to_string_lossy().into_owned())
361            .collect::<Vec<_>>()
362            .join(" ")
363    } else {
364        ARGV[0].to_string_lossy().into_owned()
365    }
366});
367
368/// Derive the complete execution phrase for "usage".
369pub fn execution_phrase() -> &'static str {
370    &EXECUTION_PHRASE
371}
372
373/// Args contains arguments passed to the utility.
374/// It is a trait that extends `Iterator<Item = OsString>`.
375/// It provides utility functions to collect the arguments into a `Vec<String>`.
376/// The collected `Vec<String>` can be lossy or ignore invalid encoding.
377pub trait Args: Iterator<Item = OsString> + Sized {
378    /// Collects the iterator into a `Vec<String>`, lossily converting the `OsString`s to `Strings`.
379    fn collect_lossy(self) -> Vec<String> {
380        self.map(|s| s.to_string_lossy().into_owned()).collect()
381    }
382
383    /// Collects the iterator into a `Vec<String>`, removing any elements that contain invalid encoding.
384    fn collect_ignore(self) -> Vec<String> {
385        self.filter_map(|s| s.into_string().ok()).collect()
386    }
387}
388
389impl<T: Iterator<Item = OsString> + Sized> Args for T {}
390
391/// Returns an iterator over the command line arguments as `OsString`s.
392/// args_os() can be expensive to call
393pub fn args_os() -> impl Iterator<Item = OsString> {
394    ARGV.iter().cloned()
395}
396
397/// Returns an iterator over the command line arguments as `OsString`s, filtering out empty arguments.
398/// This is useful for handling cases where extra whitespace or empty arguments are present.
399/// args_os_filtered() can be expensive to call
400pub fn args_os_filtered() -> impl Iterator<Item = OsString> {
401    ARGV.iter().filter(|arg| !arg.is_empty()).cloned()
402}
403
404/// Read a line from stdin and check whether the first character is `'y'` or `'Y'`
405pub fn read_yes() -> bool {
406    let mut s = String::new();
407    match std::io::stdin().read_line(&mut s) {
408        Ok(_) => matches!(s.chars().next(), Some('y' | 'Y')),
409        _ => false,
410    }
411}
412
413#[derive(Debug)]
414pub struct NonUtf8OsStrError {
415    input_lossy_string: String,
416}
417
418impl std::fmt::Display for NonUtf8OsStrError {
419    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420        use os_display::Quotable;
421        let quoted = self.input_lossy_string.quote();
422        f.write_fmt(format_args!(
423            "invalid UTF-8 input {quoted} encountered when converting to bytes on a platform that doesn't expose byte arguments",
424        ))
425    }
426}
427
428impl std::error::Error for NonUtf8OsStrError {}
429impl error::UError for NonUtf8OsStrError {}
430
431/// Converts an `OsStr` to a UTF-8 `&[u8]`.
432///
433/// This always succeeds on unix platforms,
434/// and fails on other platforms if the string can't be coerced to UTF-8.
435pub fn os_str_as_bytes(os_string: &OsStr) -> Result<&[u8], NonUtf8OsStrError> {
436    #[cfg(unix)]
437    return Ok(os_string.as_bytes());
438
439    #[cfg(not(unix))]
440    os_string
441        .to_str()
442        .ok_or_else(|| NonUtf8OsStrError {
443            input_lossy_string: os_string.to_string_lossy().into_owned(),
444        })
445        .map(|s| s.as_bytes())
446}
447
448/// Performs a potentially lossy conversion from `OsStr` to UTF-8 bytes.
449///
450/// This is always lossless on unix platforms,
451/// and wraps [`OsStr::to_string_lossy`] on non-unix platforms.
452pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<'_, [u8]> {
453    #[cfg(unix)]
454    return Cow::from(os_string.as_bytes());
455
456    #[cfg(not(unix))]
457    match os_string.to_string_lossy() {
458        Cow::Borrowed(slice) => Cow::from(slice.as_bytes()),
459        Cow::Owned(owned) => Cow::from(owned.into_bytes()),
460    }
461}
462
463/// Converts a `&[u8]` to an `&OsStr`,
464/// or parses it as UTF-8 into an [`OsString`] on non-unix platforms.
465///
466/// This always succeeds on unix platforms,
467/// and fails on other platforms if the bytes can't be parsed as UTF-8.
468pub fn os_str_from_bytes(bytes: &[u8]) -> mods::error::UResult<Cow<'_, OsStr>> {
469    #[cfg(unix)]
470    return Ok(Cow::Borrowed(OsStr::from_bytes(bytes)));
471
472    #[cfg(not(unix))]
473    Ok(Cow::Owned(OsString::from(str::from_utf8(bytes).map_err(
474        |_| mods::error::UUsageError::new(1, "Unable to transform bytes into OsStr"),
475    )?)))
476}
477
478/// Converts a `Vec<u8>` into an `OsString`, parsing as UTF-8 on non-unix platforms.
479///
480/// This always succeeds on unix platforms,
481/// and fails on other platforms if the bytes can't be parsed as UTF-8.
482pub fn os_string_from_vec(vec: Vec<u8>) -> mods::error::UResult<OsString> {
483    #[cfg(unix)]
484    return Ok(OsString::from_vec(vec));
485
486    #[cfg(not(unix))]
487    Ok(OsString::from(String::from_utf8(vec).map_err(|_| {
488        mods::error::UUsageError::new(1, "invalid UTF-8 was detected in one or more arguments")
489    })?))
490}
491
492/// Converts an `OsString` into a `Vec<u8>`, parsing as UTF-8 on non-unix platforms.
493///
494/// This always succeeds on unix platforms,
495/// and fails on other platforms if the bytes can't be parsed as UTF-8.
496pub fn os_string_to_vec(s: OsString) -> mods::error::UResult<Vec<u8>> {
497    #[cfg(unix)]
498    let v = s.into_vec();
499    #[cfg(not(unix))]
500    let v = s
501        .into_string()
502        .map_err(|_| {
503            mods::error::UUsageError::new(1, "invalid UTF-8 was detected in one or more arguments")
504        })?
505        .into();
506
507    Ok(v)
508}
509
510/// Equivalent to `std::BufRead::lines` which outputs each line as a `Vec<u8>`,
511/// which avoids panicking on non UTF-8 input.
512pub fn read_byte_lines<R: std::io::Read>(
513    mut buf_reader: BufReader<R>,
514) -> impl Iterator<Item = Vec<u8>> {
515    iter::from_fn(move || {
516        let mut buf = Vec::with_capacity(256);
517        let size = buf_reader.read_until(b'\n', &mut buf).ok()?;
518
519        if size == 0 {
520            return None;
521        }
522
523        // Trim (\r)\n
524        if buf.ends_with(b"\n") {
525            buf.pop();
526            if buf.ends_with(b"\r") {
527                buf.pop();
528            }
529        }
530
531        Some(buf)
532    })
533}
534
535/// Equivalent to `std::BufRead::lines` which outputs each line as an `OsString`
536/// This won't panic on non UTF-8 characters on Unix,
537/// but it still will on Windows.
538pub fn read_os_string_lines<R: std::io::Read>(
539    buf_reader: BufReader<R>,
540) -> impl Iterator<Item = OsString> {
541    read_byte_lines(buf_reader).map(|byte_line| os_string_from_vec(byte_line).expect("UTF-8 error"))
542}
543
544/// Prompt the user with a formatted string and returns `true` if they reply `'y'` or `'Y'`
545///
546/// This macro functions accepts the same syntax as `format!`. The prompt is written to
547/// `stderr`. A space is also printed at the end for nice spacing between the prompt and
548/// the user input. Any input starting with `'y'` or `'Y'` is interpreted as `yes`.
549///
550/// # Examples
551/// ```
552/// use uucore::prompt_yes;
553/// let file = "foo.rs";
554/// prompt_yes!("Do you want to delete '{file}'?");
555/// ```
556/// will print something like below to `stderr` (with `util_name` substituted by the actual
557/// util name) and will wait for user input.
558/// ```txt
559/// util_name: Do you want to delete 'foo.rs'?
560/// ```
561#[macro_export]
562macro_rules! prompt_yes(
563    ($($args:tt)+) => ({
564        use std::io::Write;
565        eprint!("{}: ", uucore::util_name());
566        eprint!($($args)+);
567        eprint!(" ");
568        let res = std::io::stderr().flush().map_err(|err| {
569            $crate::error::USimpleError::new(1, err.to_string())
570        });
571        uucore::show_if_err!(res);
572        uucore::read_yes()
573    })
574);
575
576/// Represent either a character or a byte.
577/// Used to iterate on partially valid UTF-8 data
578#[derive(Debug, Clone, Copy, PartialEq, Eq)]
579pub enum CharByte {
580    Char(char),
581    Byte(u8),
582}
583
584impl From<char> for CharByte {
585    fn from(value: char) -> Self {
586        Self::Char(value)
587    }
588}
589
590impl From<u8> for CharByte {
591    fn from(value: u8) -> Self {
592        Self::Byte(value)
593    }
594}
595
596impl From<&u8> for CharByte {
597    fn from(value: &u8) -> Self {
598        Self::Byte(*value)
599    }
600}
601
602struct Utf8ChunkIterator<'a> {
603    iter: Box<dyn Iterator<Item = CharByte> + 'a>,
604}
605
606impl Iterator for Utf8ChunkIterator<'_> {
607    type Item = CharByte;
608
609    fn next(&mut self) -> Option<Self::Item> {
610        self.iter.next()
611    }
612}
613
614impl<'a> From<Utf8Chunk<'a>> for Utf8ChunkIterator<'a> {
615    fn from(chk: Utf8Chunk<'a>) -> Self {
616        Self {
617            iter: Box::new(
618                chk.valid()
619                    .chars()
620                    .map(CharByte::from)
621                    .chain(chk.invalid().iter().map(CharByte::from)),
622            ),
623        }
624    }
625}
626
627/// Iterates on the valid and invalid parts of a byte sequence with regard to
628/// the UTF-8 encoding.
629pub struct CharByteIterator<'a> {
630    iter: Box<dyn Iterator<Item = CharByte> + 'a>,
631}
632
633impl<'a> CharByteIterator<'a> {
634    /// Make a `CharByteIterator` from a byte slice.
635    /// [`CharByteIterator`]
636    pub fn new(input: &'a [u8]) -> Self {
637        Self {
638            iter: Box::new(input.utf8_chunks().flat_map(Utf8ChunkIterator::from)),
639        }
640    }
641}
642
643impl Iterator for CharByteIterator<'_> {
644    type Item = CharByte;
645
646    fn next(&mut self) -> Option<Self::Item> {
647        self.iter.next()
648    }
649}
650
651pub trait IntoCharByteIterator<'a> {
652    fn iter_char_bytes(self) -> CharByteIterator<'a>;
653}
654
655impl<'a> IntoCharByteIterator<'a> for &'a [u8] {
656    fn iter_char_bytes(self) -> CharByteIterator<'a> {
657        CharByteIterator::new(self)
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664    use std::ffi::OsStr;
665
666    fn make_os_vec(os_str: &OsStr) -> Vec<OsString> {
667        vec![
668            OsString::from("test"),
669            OsString::from("สวัสดี"), // spell-checker:disable-line
670            os_str.to_os_string(),
671        ]
672    }
673
674    #[cfg(any(unix, target_os = "redox"))]
675    fn test_invalid_utf8_args_lossy(os_str: &OsStr) {
676        // assert our string is invalid utf8
677        assert!(os_str.to_os_string().into_string().is_err());
678        let test_vec = make_os_vec(os_str);
679        let collected_to_str = test_vec.clone().into_iter().collect_lossy();
680        // conservation of length - when accepting lossy conversion no arguments may be dropped
681        assert_eq!(collected_to_str.len(), test_vec.len());
682        // first indices identical
683        for index in 0..2 {
684            assert_eq!(collected_to_str[index], test_vec[index].to_str().unwrap());
685        }
686        // lossy conversion for string with illegal encoding is done
687        assert_eq!(
688            *collected_to_str[2],
689            os_str.to_os_string().to_string_lossy()
690        );
691    }
692
693    #[cfg(any(unix, target_os = "redox"))]
694    fn test_invalid_utf8_args_ignore(os_str: &OsStr) {
695        // assert our string is invalid utf8
696        assert!(os_str.to_os_string().into_string().is_err());
697        let test_vec = make_os_vec(os_str);
698        let collected_to_str = test_vec.clone().into_iter().collect_ignore();
699        // assert that the broken entry is filtered out
700        assert_eq!(collected_to_str.len(), test_vec.len() - 1);
701        // assert that the unbroken indices are converted as expected
702        for index in 0..2 {
703            assert_eq!(
704                collected_to_str.get(index).unwrap(),
705                test_vec.get(index).unwrap().to_str().unwrap()
706            );
707        }
708    }
709
710    #[test]
711    fn valid_utf8_encoding_args() {
712        // create a vector containing only correct encoding
713        let test_vec = make_os_vec(&OsString::from("test2"));
714        // expect complete conversion without losses, even when lossy conversion is accepted
715        let _ = test_vec.into_iter().collect_lossy();
716    }
717
718    #[cfg(any(unix, target_os = "redox"))]
719    #[test]
720    fn invalid_utf8_args_unix() {
721        use std::os::unix::ffi::OsStrExt;
722
723        let source = [0x66, 0x6f, 0x80, 0x6f];
724        let os_str = OsStr::from_bytes(&source[..]);
725        test_invalid_utf8_args_lossy(os_str);
726        test_invalid_utf8_args_ignore(os_str);
727    }
728
729    #[test]
730    fn test_format_usage() {
731        assert_eq!(format_usage("expr EXPRESSION"), "expr EXPRESSION");
732        assert_eq!(
733            format_usage("expr EXPRESSION\nexpr OPTION"),
734            "expr EXPRESSION\n       expr OPTION"
735        );
736    }
737}