Skip to main content

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