Skip to main content

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