uu_head/
head.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
6// spell-checker:ignore (vars) seekable memrchr
7
8use clap::{Arg, ArgAction, ArgMatches, Command};
9use memchr::memrchr_iter;
10use std::ffi::OsString;
11use std::fs::File;
12use std::io::{self, BufWriter, Read, Seek, SeekFrom, Write};
13use std::num::TryFromIntError;
14#[cfg(unix)]
15use std::os::fd::{AsRawFd, FromRawFd};
16use thiserror::Error;
17use uucore::display::Quotable;
18use uucore::error::{FromIo, UError, UResult};
19use uucore::line_ending::LineEnding;
20use uucore::translate;
21use uucore::{format_usage, show};
22
23const BUF_SIZE: usize = 65536;
24
25mod options {
26    pub const BYTES: &str = "BYTES";
27    pub const LINES: &str = "LINES";
28    pub const QUIET: &str = "QUIET";
29    pub const VERBOSE: &str = "VERBOSE";
30    pub const ZERO: &str = "ZERO";
31    pub const FILES: &str = "FILE";
32    pub const PRESUME_INPUT_PIPE: &str = "-PRESUME-INPUT-PIPE";
33}
34
35mod parse;
36mod take;
37use take::copy_all_but_n_bytes;
38use take::copy_all_but_n_lines;
39use take::take_lines;
40
41#[derive(Error, Debug)]
42enum HeadError {
43    /// Wrapper around `io::Error`
44    #[error("{}", translate!("head-error-reading-file", "name" => name.clone(), "err" => err))]
45    Io { name: String, err: io::Error },
46
47    #[error("{}", translate!("head-error-parse-error", "err" => 0))]
48    ParseError(String),
49
50    #[error("{}", translate!("head-error-num-too-large"))]
51    NumTooLarge(#[from] TryFromIntError),
52
53    #[error("{}", translate!("head-error-clap", "err" => 0))]
54    Clap(#[from] clap::Error),
55
56    #[error("{0}")]
57    MatchOption(String),
58}
59
60impl UError for HeadError {
61    fn code(&self) -> i32 {
62        1
63    }
64}
65
66type HeadResult<T> = Result<T, HeadError>;
67
68pub fn uu_app() -> Command {
69    Command::new(uucore::util_name())
70        .version(uucore::crate_version!())
71        .help_template(uucore::localized_help_template(uucore::util_name()))
72        .about(translate!("head-about"))
73        .override_usage(format_usage(&translate!("head-usage")))
74        .infer_long_args(true)
75        .arg(
76            Arg::new(options::BYTES)
77                .short('c')
78                .long("bytes")
79                .value_name("[-]NUM")
80                .help(translate!("head-help-bytes"))
81                .overrides_with_all([options::BYTES, options::LINES])
82                .allow_hyphen_values(true),
83        )
84        .arg(
85            Arg::new(options::LINES)
86                .short('n')
87                .long("lines")
88                .value_name("[-]NUM")
89                .help(translate!("head-help-lines"))
90                .overrides_with_all([options::LINES, options::BYTES])
91                .allow_hyphen_values(true),
92        )
93        .arg(
94            Arg::new(options::QUIET)
95                .short('q')
96                .long("quiet")
97                .visible_alias("silent")
98                .help(translate!("head-help-quiet"))
99                .overrides_with_all([options::VERBOSE, options::QUIET])
100                .action(ArgAction::SetTrue),
101        )
102        .arg(
103            Arg::new(options::VERBOSE)
104                .short('v')
105                .long("verbose")
106                .help(translate!("head-help-verbose"))
107                .overrides_with_all([options::QUIET, options::VERBOSE])
108                .action(ArgAction::SetTrue),
109        )
110        .arg(
111            Arg::new(options::PRESUME_INPUT_PIPE)
112                .long("presume-input-pipe")
113                .alias("-presume-input-pipe")
114                .hide(true)
115                .action(ArgAction::SetTrue),
116        )
117        .arg(
118            Arg::new(options::ZERO)
119                .short('z')
120                .long("zero-terminated")
121                .help(translate!("head-help-zero-terminated"))
122                .overrides_with(options::ZERO)
123                .action(ArgAction::SetTrue),
124        )
125        .arg(
126            Arg::new(options::FILES)
127                .action(ArgAction::Append)
128                .value_parser(clap::value_parser!(OsString))
129                .value_hint(clap::ValueHint::FilePath),
130        )
131}
132
133#[derive(Debug, PartialEq)]
134enum Mode {
135    FirstLines(u64),
136    AllButLastLines(u64),
137    FirstBytes(u64),
138    AllButLastBytes(u64),
139}
140
141impl Default for Mode {
142    fn default() -> Self {
143        Self::FirstLines(10)
144    }
145}
146
147impl Mode {
148    fn from(matches: &ArgMatches) -> Result<Self, String> {
149        if let Some(v) = matches.get_one::<String>(options::BYTES) {
150            let (n, all_but_last) = parse::parse_num(v)
151                .map_err(|err| translate!("head-error-invalid-bytes", "err" => err))?;
152            if all_but_last {
153                Ok(Self::AllButLastBytes(n))
154            } else {
155                Ok(Self::FirstBytes(n))
156            }
157        } else if let Some(v) = matches.get_one::<String>(options::LINES) {
158            let (n, all_but_last) = parse::parse_num(v)
159                .map_err(|err| translate!("head-error-invalid-lines", "err" => err))?;
160            if all_but_last {
161                Ok(Self::AllButLastLines(n))
162            } else {
163                Ok(Self::FirstLines(n))
164            }
165        } else {
166            Ok(Self::default())
167        }
168    }
169}
170
171fn arg_iterate<'a>(
172    mut args: impl uucore::Args + 'a,
173) -> HeadResult<Box<dyn Iterator<Item = OsString> + 'a>> {
174    // argv[0] is always present
175    let first = args.next().unwrap();
176    if let Some(second) = args.next() {
177        if let Some(s) = second.to_str() {
178            if let Some(v) = parse::parse_obsolete(s) {
179                match v {
180                    Ok(iter) => Ok(Box::new(vec![first].into_iter().chain(iter).chain(args))),
181                    Err(parse::ParseError) => Err(HeadError::ParseError(
182                        translate!("head-error-bad-argument-format", "arg" => s.quote()),
183                    )),
184                }
185            } else {
186                // The second argument contains non-UTF-8 sequences, so it can't be an obsolete option
187                // like "-5". Treat it as a regular file argument.
188                Ok(Box::new(vec![first, second].into_iter().chain(args)))
189            }
190        } else {
191            // The second argument contains non-UTF-8 sequences, so it can't be an obsolete option
192            // like "-5". Treat it as a regular file argument.
193            Ok(Box::new(vec![first, second].into_iter().chain(args)))
194        }
195    } else {
196        Ok(Box::new(vec![first].into_iter()))
197    }
198}
199
200#[derive(Debug, PartialEq, Default)]
201struct HeadOptions {
202    pub quiet: bool,
203    pub verbose: bool,
204    pub line_ending: LineEnding,
205    pub presume_input_pipe: bool,
206    pub mode: Mode,
207    pub files: Vec<OsString>,
208}
209
210impl HeadOptions {
211    ///Construct options from matches
212    pub fn get_from(matches: &ArgMatches) -> Result<Self, String> {
213        let mut options = Self::default();
214
215        options.quiet = matches.get_flag(options::QUIET);
216        options.verbose = matches.get_flag(options::VERBOSE);
217        options.line_ending = LineEnding::from_zero_flag(matches.get_flag(options::ZERO));
218        options.presume_input_pipe = matches.get_flag(options::PRESUME_INPUT_PIPE);
219
220        options.mode = Mode::from(matches)?;
221
222        options.files = match matches.get_many::<OsString>(options::FILES) {
223            Some(v) => v.cloned().collect(),
224            None => vec![OsString::from("-")],
225        };
226
227        Ok(options)
228    }
229}
230
231#[inline]
232fn wrap_in_stdout_error(err: io::Error) -> io::Error {
233    io::Error::new(
234        err.kind(),
235        translate!("head-error-writing-stdout", "err" => err),
236    )
237}
238
239fn read_n_bytes(input: impl Read, n: u64) -> io::Result<u64> {
240    // Read the first `n` bytes from the `input` reader.
241    let mut reader = input.take(n);
242
243    // Write those bytes to `stdout`.
244    let stdout = io::stdout();
245    let mut stdout = stdout.lock();
246
247    let bytes_written = io::copy(&mut reader, &mut stdout).map_err(wrap_in_stdout_error)?;
248
249    // Make sure we finish writing everything to the target before
250    // exiting. Otherwise, when Rust is implicitly flushing, any
251    // error will be silently ignored.
252    stdout.flush().map_err(wrap_in_stdout_error)?;
253
254    Ok(bytes_written)
255}
256
257fn read_n_lines(input: &mut impl io::BufRead, n: u64, separator: u8) -> io::Result<u64> {
258    // Read the first `n` lines from the `input` reader.
259    let mut reader = take_lines(input, n, separator);
260
261    // Write those bytes to `stdout`.
262    let stdout = io::stdout();
263    let stdout = stdout.lock();
264    let mut writer = BufWriter::with_capacity(BUF_SIZE, stdout);
265
266    let bytes_written = io::copy(&mut reader, &mut writer).map_err(wrap_in_stdout_error)?;
267
268    // Make sure we finish writing everything to the target before
269    // exiting. Otherwise, when Rust is implicitly flushing, any
270    // error will be silently ignored.
271    writer.flush().map_err(wrap_in_stdout_error)?;
272
273    Ok(bytes_written)
274}
275
276fn catch_too_large_numbers_in_backwards_bytes_or_lines(n: u64) -> Option<usize> {
277    usize::try_from(n).ok()
278}
279
280fn read_but_last_n_bytes(mut input: impl Read, n: u64) -> io::Result<u64> {
281    let mut bytes_written: u64 = 0;
282    if let Some(n) = catch_too_large_numbers_in_backwards_bytes_or_lines(n) {
283        let stdout = io::stdout();
284        let mut stdout = stdout.lock();
285
286        bytes_written = copy_all_but_n_bytes(&mut input, &mut stdout, n)
287            .map_err(wrap_in_stdout_error)?
288            .try_into()
289            .unwrap();
290
291        // Make sure we finish writing everything to the target before
292        // exiting. Otherwise, when Rust is implicitly flushing, any
293        // error will be silently ignored.
294        stdout.flush().map_err(wrap_in_stdout_error)?;
295    }
296    Ok(bytes_written)
297}
298
299fn read_but_last_n_lines(mut input: impl Read, n: u64, separator: u8) -> io::Result<u64> {
300    let stdout = io::stdout();
301    let mut stdout = stdout.lock();
302    if n == 0 {
303        return io::copy(&mut input, &mut stdout).map_err(wrap_in_stdout_error);
304    }
305    let mut bytes_written: u64 = 0;
306    if let Some(n) = catch_too_large_numbers_in_backwards_bytes_or_lines(n) {
307        bytes_written = copy_all_but_n_lines(input, &mut stdout, n, separator)
308            .map_err(wrap_in_stdout_error)?
309            .try_into()
310            .unwrap();
311        // Make sure we finish writing everything to the target before
312        // exiting. Otherwise, when Rust is implicitly flushing, any
313        // error will be silently ignored.
314        stdout.flush().map_err(wrap_in_stdout_error)?;
315    }
316    Ok(bytes_written)
317}
318
319/// Return the index in `input` just after the `n`th line from the end.
320///
321/// If `n` exceeds the number of lines in this file, then return 0.
322/// This function rewinds the cursor to the
323/// beginning of the input just before returning unless there is an
324/// I/O error.
325///
326/// # Errors
327///
328/// This function returns an error if there is a problem seeking
329/// through or reading the input.
330///
331/// # Examples
332///
333/// The function returns the index of the byte immediately following
334/// the line ending character of the `n`th line from the end of the
335/// input:
336///
337/// ```rust,ignore
338/// let mut input = Cursor::new("x\ny\nz\n");
339/// assert_eq!(find_nth_line_from_end(&mut input, 0, false).unwrap(), 6);
340/// assert_eq!(find_nth_line_from_end(&mut input, 1, false).unwrap(), 4);
341/// assert_eq!(find_nth_line_from_end(&mut input, 2, false).unwrap(), 2);
342/// ```
343///
344/// If `n` exceeds the number of lines in the file, always return 0:
345///
346/// ```rust,ignore
347/// let mut input = Cursor::new("x\ny\nz\n");
348/// assert_eq!(find_nth_line_from_end(&mut input, 3, false).unwrap(), 0);
349/// assert_eq!(find_nth_line_from_end(&mut input, 4, false).unwrap(), 0);
350/// assert_eq!(find_nth_line_from_end(&mut input, 1000, false).unwrap(), 0);
351/// ```
352fn find_nth_line_from_end<R>(input: &mut R, n: u64, separator: u8) -> io::Result<u64>
353where
354    R: Read + Seek,
355{
356    let file_size = input.seek(SeekFrom::End(0))?;
357
358    let mut buffer = [0u8; BUF_SIZE];
359
360    let mut lines = 0u64;
361    let mut check_last_byte_first_loop = true;
362    let mut bytes_remaining_to_search = file_size;
363
364    loop {
365        // the casts here are ok, `buffer.len()` should never be above a few k
366        let bytes_to_read_this_loop =
367            bytes_remaining_to_search.min(buffer.len().try_into().unwrap());
368        let read_start_offset = bytes_remaining_to_search - bytes_to_read_this_loop;
369        let buffer = &mut buffer[..bytes_to_read_this_loop.try_into().unwrap()];
370        bytes_remaining_to_search -= bytes_to_read_this_loop;
371
372        input.seek(SeekFrom::Start(read_start_offset))?;
373        input.read_exact(buffer)?;
374
375        // Unfortunately need special handling for the case that the input file doesn't have
376        // a terminating `separator` character.
377        // If the input file doesn't end with a `separator` character, add an extra line to our
378        // `line` counter. In the case that `n` is 0 we need to return here since we've
379        // obviously found our 0th-line-from-the-end offset.
380        if check_last_byte_first_loop {
381            check_last_byte_first_loop = false;
382            if let Some(last_byte_of_file) = buffer.last() {
383                if last_byte_of_file != &separator {
384                    if n == 0 {
385                        input.rewind()?;
386                        return Ok(file_size);
387                    }
388                    assert_eq!(lines, 0);
389                    lines = 1;
390                }
391            }
392        }
393
394        for separator_offset in memrchr_iter(separator, &buffer[..]) {
395            lines += 1;
396            if lines == n + 1 {
397                input.rewind()?;
398                return Ok(read_start_offset
399                    + TryInto::<u64>::try_into(separator_offset).unwrap()
400                    + 1);
401            }
402        }
403        if read_start_offset == 0 {
404            input.rewind()?;
405            return Ok(0);
406        }
407    }
408}
409
410fn is_seekable(input: &mut File) -> bool {
411    let current_pos = input.stream_position();
412    current_pos.is_ok()
413        && input.seek(SeekFrom::End(0)).is_ok()
414        && input.seek(SeekFrom::Start(current_pos.unwrap())).is_ok()
415}
416
417fn head_backwards_file(input: &mut File, options: &HeadOptions) -> io::Result<u64> {
418    let st = input.metadata()?;
419    let seekable = is_seekable(input);
420    let blksize_limit = uucore::fs::sane_blksize::sane_blksize_from_metadata(&st);
421    if !seekable || st.len() <= blksize_limit || options.presume_input_pipe {
422        head_backwards_without_seek_file(input, options)
423    } else {
424        head_backwards_on_seekable_file(input, options)
425    }
426}
427
428fn head_backwards_without_seek_file(input: &mut File, options: &HeadOptions) -> io::Result<u64> {
429    match options.mode {
430        Mode::AllButLastBytes(n) => read_but_last_n_bytes(input, n),
431        Mode::AllButLastLines(n) => read_but_last_n_lines(input, n, options.line_ending.into()),
432        _ => unreachable!(),
433    }
434}
435
436fn head_backwards_on_seekable_file(input: &mut File, options: &HeadOptions) -> io::Result<u64> {
437    match options.mode {
438        Mode::AllButLastBytes(n) => {
439            let size = input.metadata()?.len();
440            if n >= size {
441                Ok(0)
442            } else {
443                read_n_bytes(input, size - n)
444            }
445        }
446        Mode::AllButLastLines(n) => {
447            let found = find_nth_line_from_end(input, n, options.line_ending.into())?;
448            read_n_bytes(input, found)
449        }
450        _ => unreachable!(),
451    }
452}
453
454fn head_file(input: &mut File, options: &HeadOptions) -> io::Result<u64> {
455    match options.mode {
456        Mode::FirstBytes(n) => read_n_bytes(input, n),
457        Mode::FirstLines(n) => read_n_lines(
458            &mut io::BufReader::with_capacity(BUF_SIZE, input),
459            n,
460            options.line_ending.into(),
461        ),
462        Mode::AllButLastBytes(_) | Mode::AllButLastLines(_) => head_backwards_file(input, options),
463    }
464}
465
466#[allow(clippy::cognitive_complexity)]
467fn uu_head(options: &HeadOptions) -> UResult<()> {
468    let mut first = true;
469    for file in &options.files {
470        let res = if file == "-" {
471            if (options.files.len() > 1 && !options.quiet) || options.verbose {
472                if !first {
473                    println!();
474                }
475                println!("{}", translate!("head-header-stdin"));
476            }
477            let stdin = io::stdin();
478
479            #[cfg(unix)]
480            {
481                let stdin_raw_fd = stdin.as_raw_fd();
482                let mut stdin_file = unsafe { File::from_raw_fd(stdin_raw_fd) };
483                let current_pos = stdin_file.stream_position();
484                if let Ok(current_pos) = current_pos {
485                    // We have a seekable file. Ensure we set the input stream to the
486                    // last byte read so that any tools that parse the remainder of
487                    // the stdin stream read from the correct place.
488
489                    let bytes_read = head_file(&mut stdin_file, options)?;
490                    stdin_file.seek(SeekFrom::Start(current_pos + bytes_read))?;
491                } else {
492                    let _bytes_read = head_file(&mut stdin_file, options)?;
493                }
494            }
495
496            #[cfg(not(unix))]
497            {
498                let mut stdin = stdin.lock();
499
500                match options.mode {
501                    Mode::FirstBytes(n) => read_n_bytes(&mut stdin, n),
502                    Mode::AllButLastBytes(n) => read_but_last_n_bytes(&mut stdin, n),
503                    Mode::FirstLines(n) => read_n_lines(&mut stdin, n, options.line_ending.into()),
504                    Mode::AllButLastLines(n) => {
505                        read_but_last_n_lines(&mut stdin, n, options.line_ending.into())
506                    }
507                }?;
508            }
509
510            Ok(())
511        } else {
512            let mut file_handle = match File::open(file) {
513                Ok(f) => f,
514                Err(err) => {
515                    show!(err.map_err_context(
516                        || translate!("head-error-cannot-open", "name" => file.to_string_lossy().quote())
517                    ));
518                    continue;
519                }
520            };
521            if (options.files.len() > 1 && !options.quiet) || options.verbose {
522                if !first {
523                    println!();
524                }
525                match file.to_str() {
526                    Some(name) => println!("==> {name} <=="),
527                    None => println!("==> {} <==", file.to_string_lossy()),
528                }
529            }
530            head_file(&mut file_handle, options)?;
531            Ok(())
532        };
533        if let Err(e) = res {
534            let name = if file == "-" {
535                "standard input".to_string()
536            } else {
537                file.to_string_lossy().into_owned()
538            };
539            return Err(HeadError::Io {
540                name: name.to_string(),
541                err: e,
542            }
543            .into());
544        }
545        first = false;
546    }
547    // Even though this is returning `Ok`, it is possible that a call
548    // to `show!()` and thus a call to `set_exit_code()` has been
549    // called above. If that happens, then this process will exit with
550    // a non-zero exit code.
551    Ok(())
552}
553
554#[uucore::main]
555pub fn uumain(args: impl uucore::Args) -> UResult<()> {
556    let args: Vec<_> = arg_iterate(args)?.collect();
557    let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
558    let options = HeadOptions::get_from(&matches).map_err(HeadError::MatchOption)?;
559    uu_head(&options)
560}
561
562#[cfg(test)]
563mod tests {
564    use io::Cursor;
565    use std::ffi::OsString;
566
567    use super::*;
568
569    fn options(args: &str) -> Result<HeadOptions, String> {
570        let combined = "head ".to_owned() + args;
571        let args = combined.split_whitespace().map(OsString::from);
572        let matches = uu_app()
573            .get_matches_from(arg_iterate(args).map_err(|_| String::from("Arg iterate failed"))?);
574        HeadOptions::get_from(&matches)
575    }
576
577    #[test]
578    fn test_args_modes() {
579        let args = options("-n -10M -vz").unwrap();
580        assert_eq!(args.line_ending, LineEnding::Nul);
581        assert!(args.verbose);
582        assert_eq!(args.mode, Mode::AllButLastLines(10 * 1024 * 1024));
583    }
584
585    #[test]
586    fn test_gnu_compatibility() {
587        let args = options("-n 1 -c 1 -n 5 -c kiB -vqvqv").unwrap(); // spell-checker:disable-line
588        assert_eq!(args.mode, Mode::FirstBytes(1024));
589        assert!(args.verbose);
590        assert_eq!(options("-5").unwrap().mode, Mode::FirstLines(5));
591        assert_eq!(options("-2b").unwrap().mode, Mode::FirstBytes(1024));
592        assert_eq!(options("-5 -c 1").unwrap().mode, Mode::FirstBytes(1));
593    }
594
595    #[test]
596    #[allow(clippy::cognitive_complexity)]
597    fn all_args_test() {
598        assert!(options("--silent").unwrap().quiet);
599        assert!(options("--quiet").unwrap().quiet);
600        assert!(options("-q").unwrap().quiet);
601        assert!(options("--verbose").unwrap().verbose);
602        assert!(options("-v").unwrap().verbose);
603        assert_eq!(
604            options("--zero-terminated").unwrap().line_ending,
605            LineEnding::Nul
606        );
607        assert_eq!(options("-z").unwrap().line_ending, LineEnding::Nul);
608        assert_eq!(options("--lines 15").unwrap().mode, Mode::FirstLines(15));
609        assert_eq!(options("-n 15").unwrap().mode, Mode::FirstLines(15));
610        assert_eq!(options("--bytes 15").unwrap().mode, Mode::FirstBytes(15));
611        assert_eq!(options("-c 15").unwrap().mode, Mode::FirstBytes(15));
612    }
613
614    #[test]
615    fn test_options_errors() {
616        assert!(options("-n IsThisTheRealLife?").is_err());
617        assert!(options("-c IsThisJustFantasy").is_err());
618    }
619
620    #[test]
621    fn test_options_correct_defaults() {
622        let opts = HeadOptions::default();
623
624        assert!(!opts.verbose);
625        assert!(!opts.quiet);
626        assert_eq!(opts.line_ending, LineEnding::Newline);
627        assert_eq!(opts.mode, Mode::FirstLines(10));
628        assert!(opts.files.is_empty());
629    }
630
631    fn arg_outputs(src: &str) -> Result<String, ()> {
632        let split = src.split_whitespace().map(OsString::from);
633        match arg_iterate(split) {
634            Ok(args) => {
635                let vec = args
636                    .map(|s| s.to_str().unwrap().to_owned())
637                    .collect::<Vec<_>>();
638                Ok(vec.join(" "))
639            }
640            Err(_) => Err(()),
641        }
642    }
643
644    #[test]
645    fn test_arg_iterate() {
646        // test that normal args remain unchanged
647        assert_eq!(
648            arg_outputs("head -n -5 -zv"),
649            Ok("head -n -5 -zv".to_owned())
650        );
651        // tests that nonsensical args are unchanged
652        assert_eq!(
653            arg_outputs("head -to_be_or_not_to_be,..."),
654            Ok("head -to_be_or_not_to_be,...".to_owned())
655        );
656        //test that the obsolete syntax is unrolled
657        assert_eq!(
658            arg_outputs("head -123qvqvqzc"), // spell-checker:disable-line
659            Ok("head -q -z -c 123".to_owned())
660        );
661        //test that bad obsoletes are an error
662        assert!(arg_outputs("head -123FooBar").is_err());
663        //test overflow
664        assert!(arg_outputs("head -100000000000000000000000000000000000000000").is_ok());
665        //test that empty args remain unchanged
666        assert_eq!(arg_outputs("head"), Ok("head".to_owned()));
667    }
668
669    #[test]
670    #[cfg(target_os = "linux")]
671    fn test_arg_iterate_bad_encoding() {
672        use std::os::unix::ffi::OsStringExt;
673        let invalid = OsString::from_vec(vec![b'\x80', b'\x81']);
674        // this arises from a conversion from OsString to &str
675        assert!(arg_iterate(vec![OsString::from("head"), invalid].into_iter()).is_ok());
676    }
677
678    #[test]
679    fn read_early_exit() {
680        let mut empty = io::BufReader::new(Cursor::new(Vec::new()));
681        assert!(read_n_bytes(&mut empty, 0).is_ok());
682        assert!(read_n_lines(&mut empty, 0, b'\n').is_ok());
683    }
684
685    #[test]
686    fn test_find_nth_line_from_end() {
687        // Make sure our input buffer is several multiples of BUF_SIZE in size
688        // such that we can be reasonably confident we've exercised all logic paths.
689        // Make the contents of the buffer look like...
690        // aaaa\n
691        // aaaa\n
692        // aaaa\n
693        // aaaa\n
694        // aaaa\n
695        // ...
696        // This will make it easier to validate the results since each line will have
697        // 5 bytes in it.
698
699        let minimum_buffer_size = BUF_SIZE * 4;
700        let mut input_buffer = vec![];
701        let mut loop_iteration: u64 = 0;
702        while input_buffer.len() < minimum_buffer_size {
703            for _n in 0..4 {
704                input_buffer.push(b'a');
705            }
706            loop_iteration += 1;
707            input_buffer.push(b'\n');
708        }
709
710        let lines_in_input_file = loop_iteration;
711        let input_length = lines_in_input_file * 5;
712        assert_eq!(input_length, input_buffer.len().try_into().unwrap());
713        let mut input = Cursor::new(input_buffer);
714        // We now have loop_iteration lines in the buffer Now walk backwards through the buffer
715        // to confirm everything parses correctly.
716        // Use a large step size to prevent the test from taking too long, but don't use a power
717        // of 2 in case we miss some corner case.
718        let step_size = 511;
719        for n in (0..lines_in_input_file).filter(|v| v % step_size == 0) {
720            // The 5*n comes from 5-bytes per row.
721            assert_eq!(
722                find_nth_line_from_end(&mut input, n, b'\n').unwrap(),
723                input_length - 5 * n
724            );
725        }
726
727        // Now confirm that if we query with a value >= lines_in_input_file we get an offset
728        // of 0
729        assert_eq!(
730            find_nth_line_from_end(&mut input, lines_in_input_file, b'\n').unwrap(),
731            0
732        );
733        assert_eq!(
734            find_nth_line_from_end(&mut input, lines_in_input_file + 1, b'\n').unwrap(),
735            0
736        );
737        assert_eq!(
738            find_nth_line_from_end(&mut input, lines_in_input_file + 1000, b'\n').unwrap(),
739            0
740        );
741    }
742
743    #[test]
744    fn test_find_nth_line_from_end_non_terminated() {
745        // Validate the find_nth_line_from_end for files that are not terminated with a final
746        // newline character.
747        let input_file = "a\nb";
748        let mut input = Cursor::new(input_file);
749        assert_eq!(find_nth_line_from_end(&mut input, 0, b'\n').unwrap(), 3);
750        assert_eq!(find_nth_line_from_end(&mut input, 1, b'\n').unwrap(), 2);
751    }
752
753    #[test]
754    fn test_find_nth_line_from_end_empty() {
755        // Validate the find_nth_line_from_end for files that are empty.
756        let input_file = "";
757        let mut input = Cursor::new(input_file);
758        assert_eq!(find_nth_line_from_end(&mut input, 0, b'\n').unwrap(), 0);
759        assert_eq!(find_nth_line_from_end(&mut input, 1, b'\n').unwrap(), 0);
760    }
761}