diffutilslib/
cmp.rs

1// This file is part of the uutils diffutils package.
2//
3// For the full copyright and license information, please view the LICENSE-*
4// files that was distributed with this source code.
5
6use crate::utils::format_failure_to_read_input_file;
7use std::env::{self, ArgsOs};
8use std::ffi::OsString;
9use std::io::{BufRead, BufReader, BufWriter, Read, Write};
10use std::iter::Peekable;
11use std::process::ExitCode;
12use std::{cmp, fs, io};
13
14#[cfg(not(target_os = "windows"))]
15use std::os::fd::{AsRawFd, FromRawFd};
16
17#[cfg(not(target_os = "windows"))]
18use std::os::unix::fs::MetadataExt;
19
20#[cfg(target_os = "windows")]
21use std::os::windows::fs::MetadataExt;
22
23#[derive(Clone, Debug, Default, Eq, PartialEq)]
24pub struct Params {
25    executable: OsString,
26    from: OsString,
27    to: OsString,
28    print_bytes: bool,
29    skip_a: Option<usize>,
30    skip_b: Option<usize>,
31    max_bytes: Option<usize>,
32    verbose: bool,
33    quiet: bool,
34}
35
36#[inline]
37fn usage_string(executable: &str) -> String {
38    format!("Usage: {executable} <from> <to>")
39}
40
41#[cfg(not(target_os = "windows"))]
42fn is_stdout_dev_null() -> bool {
43    let Ok(dev_null) = fs::metadata("/dev/null") else {
44        return false;
45    };
46
47    let stdout_fd = io::stdout().lock().as_raw_fd();
48
49    // SAFETY: we have exclusive access to stdout right now.
50    let stdout_file = unsafe { fs::File::from_raw_fd(stdout_fd) };
51    let Ok(stdout) = stdout_file.metadata() else {
52        return false;
53    };
54
55    let is_dev_null = stdout.dev() == dev_null.dev() && stdout.ino() == dev_null.ino();
56
57    // Don't let File close the fd. It's unfortunate that File doesn't have a leak_fd().
58    std::mem::forget(stdout_file);
59
60    is_dev_null
61}
62
63pub fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Result<Params, String> {
64    let Some(executable) = opts.next() else {
65        return Err("Usage: <exe> <from> <to>".to_string());
66    };
67    let executable_str = executable.to_string_lossy().to_string();
68
69    let parse_skip = |param: &str, skip_desc: &str| -> Result<usize, String> {
70        let suffix_start = param
71            .find(|b: char| !b.is_ascii_digit())
72            .unwrap_or(param.len());
73        let mut num = match param[..suffix_start].parse::<usize>() {
74            Ok(num) => num,
75            Err(e) if *e.kind() == std::num::IntErrorKind::PosOverflow => usize::MAX,
76            Err(_) => {
77                return Err(format!(
78                    "{executable_str}: invalid --ignore-initial value '{skip_desc}'"
79                ))
80            }
81        };
82
83        if suffix_start != param.len() {
84            // Note that GNU cmp advertises supporting up to Y, but fails if you try
85            // to actually use anything beyond E.
86            let multiplier: usize = match &param[suffix_start..] {
87                "kB" => 1_000,
88                "K" => 1_024,
89                "MB" => 1_000_000,
90                "M" => 1_048_576,
91                "GB" => 1_000_000_000,
92                "G" => 1_073_741_824,
93                // This only generates a warning when compiling for target_pointer_width < 64
94                #[allow(unused_variables)]
95                suffix @ ("TB" | "T" | "PB" | "P" | "EB" | "E") => {
96                    #[cfg(target_pointer_width = "64")]
97                    match suffix {
98                        "TB" => 1_000_000_000_000,
99                        "T" => 1_099_511_627_776,
100                        "PB" => 1_000_000_000_000_000,
101                        "P" => 1_125_899_906_842_624,
102                        "EB" => 1_000_000_000_000_000_000,
103                        "E" => 1_152_921_504_606_846_976,
104                        _ => unreachable!(),
105                    }
106                    #[cfg(not(target_pointer_width = "64"))]
107                    usize::MAX
108                }
109                "ZB" => usize::MAX, // 1_000_000_000_000_000_000_000,
110                "Z" => usize::MAX,  // 1_180_591_620_717_411_303_424,
111                "YB" => usize::MAX, // 1_000_000_000_000_000_000_000_000,
112                "Y" => usize::MAX,  // 1_208_925_819_614_629_174_706_176,
113                _ => {
114                    return Err(format!(
115                        "{executable_str}: invalid --ignore-initial value '{skip_desc}'"
116                    ));
117                }
118            };
119
120            num = match num.overflowing_mul(multiplier) {
121                (n, false) => n,
122                _ => usize::MAX,
123            }
124        }
125
126        Ok(num)
127    };
128
129    let mut params = Params {
130        executable,
131        ..Default::default()
132    };
133    let mut from = None;
134    let mut to = None;
135    let mut skip_pos1 = None;
136    let mut skip_pos2 = None;
137    while let Some(param) = opts.next() {
138        if param == "--" {
139            break;
140        }
141        if param == "-" {
142            if from.is_none() {
143                from = Some(param);
144            } else if to.is_none() {
145                to = Some(param);
146            } else {
147                return Err(usage_string(&executable_str));
148            }
149            continue;
150        }
151        if param == "-b" || param == "--print-bytes" {
152            params.print_bytes = true;
153            continue;
154        }
155        if param == "-l" || param == "--verbose" {
156            params.verbose = true;
157            continue;
158        }
159        if param == "-lb" || param == "-bl" {
160            params.print_bytes = true;
161            params.verbose = true;
162            continue;
163        }
164
165        let param_str = param.to_string_lossy().to_string();
166        if param == "-n" || param_str.starts_with("--bytes=") {
167            let max_bytes = if param == "-n" {
168                opts.next()
169                    .ok_or_else(|| usage_string(&executable_str))?
170                    .to_string_lossy()
171                    .to_string()
172            } else {
173                let (_, arg) = param_str.split_once('=').unwrap();
174                arg.to_string()
175            };
176            let max_bytes = match max_bytes.parse::<usize>() {
177                Ok(num) => num,
178                Err(e) if *e.kind() == std::num::IntErrorKind::PosOverflow => usize::MAX,
179                Err(_) => {
180                    return Err(format!(
181                        "{executable_str}: invalid --bytes value '{max_bytes}'"
182                    ))
183                }
184            };
185            params.max_bytes = Some(max_bytes);
186            continue;
187        }
188        if param == "-i" || param_str.starts_with("--ignore-initial=") {
189            let skip_desc = if param == "-i" {
190                opts.next()
191                    .ok_or_else(|| usage_string(&executable_str))?
192                    .to_string_lossy()
193                    .to_string()
194            } else {
195                let (_, arg) = param_str.split_once('=').unwrap();
196                arg.to_string()
197            };
198            let (skip_a, skip_b) = if let Some((skip_a, skip_b)) = skip_desc.split_once(':') {
199                (
200                    parse_skip(skip_a, &skip_desc)?,
201                    parse_skip(skip_b, &skip_desc)?,
202                )
203            } else {
204                let skip = parse_skip(&skip_desc, &skip_desc)?;
205                (skip, skip)
206            };
207            params.skip_a = Some(skip_a);
208            params.skip_b = Some(skip_b);
209            continue;
210        }
211        if param == "-s" || param == "--quiet" || param == "--silent" {
212            params.quiet = true;
213            continue;
214        }
215        if param == "--help" {
216            println!("{}", usage_string(&executable_str));
217            std::process::exit(0);
218        }
219        if param_str.starts_with('-') {
220            return Err(format!("Unknown option: {param:?}"));
221        }
222        if from.is_none() {
223            from = Some(param);
224        } else if to.is_none() {
225            to = Some(param);
226        } else if skip_pos1.is_none() {
227            skip_pos1 = Some(parse_skip(&param_str, &param_str)?);
228        } else if skip_pos2.is_none() {
229            skip_pos2 = Some(parse_skip(&param_str, &param_str)?);
230        } else {
231            return Err(usage_string(&executable_str));
232        }
233    }
234
235    // Do as GNU cmp, and completely disable printing if we are
236    // outputing to /dev/null.
237    #[cfg(not(target_os = "windows"))]
238    if is_stdout_dev_null() {
239        params.quiet = true;
240        params.verbose = false;
241        params.print_bytes = false;
242    }
243
244    if params.quiet && params.verbose {
245        return Err(format!(
246            "{executable_str}: options -l and -s are incompatible"
247        ));
248    }
249
250    params.from = if let Some(from) = from {
251        from
252    } else if let Some(param) = opts.next() {
253        param
254    } else {
255        return Err(usage_string(&executable_str));
256    };
257    params.to = if let Some(to) = to {
258        to
259    } else if let Some(param) = opts.next() {
260        param
261    } else {
262        OsString::from("-")
263    };
264
265    // GNU cmp ignores positional skip arguments if -i is provided.
266    if params.skip_a.is_none() {
267        if skip_pos1.is_some() {
268            params.skip_a = skip_pos1;
269        } else if let Some(param) = opts.next() {
270            let param_str = param.to_string_lossy().to_string();
271            params.skip_a = Some(parse_skip(&param_str, &param_str)?);
272        }
273    };
274    if params.skip_b.is_none() {
275        if skip_pos2.is_some() {
276            params.skip_b = skip_pos2;
277        } else if let Some(param) = opts.next() {
278            let param_str = param.to_string_lossy().to_string();
279            params.skip_b = Some(parse_skip(&param_str, &param_str)?);
280        }
281    }
282
283    Ok(params)
284}
285
286fn prepare_reader(
287    path: &OsString,
288    skip: &Option<usize>,
289    params: &Params,
290) -> Result<Box<dyn BufRead>, String> {
291    let mut reader: Box<dyn BufRead> = if path == "-" {
292        Box::new(BufReader::new(io::stdin()))
293    } else {
294        match fs::File::open(path) {
295            Ok(file) => Box::new(BufReader::new(file)),
296            Err(e) => {
297                return Err(format_failure_to_read_input_file(
298                    &params.executable,
299                    path,
300                    &e,
301                ));
302            }
303        }
304    };
305
306    if let Some(skip) = skip {
307        if let Err(e) = io::copy(&mut reader.by_ref().take(*skip as u64), &mut io::sink()) {
308            return Err(format_failure_to_read_input_file(
309                &params.executable,
310                path,
311                &e,
312            ));
313        }
314    }
315
316    Ok(reader)
317}
318
319#[derive(Debug)]
320pub enum Cmp {
321    Equal,
322    Different,
323}
324
325pub fn cmp(params: &Params) -> Result<Cmp, String> {
326    let mut from = prepare_reader(&params.from, &params.skip_a, params)?;
327    let mut to = prepare_reader(&params.to, &params.skip_b, params)?;
328
329    let mut offset_width = params.max_bytes.unwrap_or(usize::MAX);
330
331    if let (Ok(a_meta), Ok(b_meta)) = (fs::metadata(&params.from), fs::metadata(&params.to)) {
332        #[cfg(not(target_os = "windows"))]
333        let (a_size, b_size) = (a_meta.size(), b_meta.size());
334
335        #[cfg(target_os = "windows")]
336        let (a_size, b_size) = (a_meta.file_size(), b_meta.file_size());
337
338        // If the files have different sizes, we already know they are not identical. If we have not
339        // been asked to show even the first difference, we can quit early.
340        if params.quiet && a_size != b_size {
341            return Ok(Cmp::Different);
342        }
343
344        let smaller = cmp::min(a_size, b_size) as usize;
345        offset_width = cmp::min(smaller, offset_width);
346    }
347
348    let offset_width = 1 + offset_width.checked_ilog10().unwrap_or(1) as usize;
349
350    // Capacity calc: at_byte width + 2 x 3-byte octal numbers + 2 x 4-byte value + 4 spaces
351    let mut output = Vec::<u8>::with_capacity(offset_width + 3 * 2 + 4 * 2 + 4);
352
353    let mut at_byte = 1;
354    let mut at_line = 1;
355    let mut start_of_line = true;
356    let mut stdout = BufWriter::new(io::stdout().lock());
357    let mut compare = Cmp::Equal;
358    loop {
359        // Fill up our buffers.
360        let from_buf = match from.fill_buf() {
361            Ok(buf) => buf,
362            Err(e) => {
363                return Err(format_failure_to_read_input_file(
364                    &params.executable,
365                    &params.from,
366                    &e,
367                ));
368            }
369        };
370
371        let to_buf = match to.fill_buf() {
372            Ok(buf) => buf,
373            Err(e) => {
374                return Err(format_failure_to_read_input_file(
375                    &params.executable,
376                    &params.to,
377                    &e,
378                ));
379            }
380        };
381
382        // Check for EOF conditions.
383        if from_buf.is_empty() && to_buf.is_empty() {
384            break;
385        }
386
387        if from_buf.is_empty() || to_buf.is_empty() {
388            let eof_on = if from_buf.is_empty() {
389                &params.from.to_string_lossy()
390            } else {
391                &params.to.to_string_lossy()
392            };
393
394            report_eof(at_byte, at_line, start_of_line, eof_on, params);
395            return Ok(Cmp::Different);
396        }
397
398        // Fast path - for long files in which almost all bytes are the same we
399        // can do a direct comparison to let the compiler optimize.
400        let consumed = std::cmp::min(from_buf.len(), to_buf.len());
401        if from_buf[..consumed] == to_buf[..consumed] {
402            let last = from_buf[..consumed].last().unwrap();
403
404            at_byte += consumed;
405            at_line += from_buf[..consumed].iter().filter(|&c| *c == b'\n').count();
406
407            start_of_line = *last == b'\n';
408
409            if let Some(max_bytes) = params.max_bytes {
410                if at_byte > max_bytes {
411                    break;
412                }
413            }
414
415            from.consume(consumed);
416            to.consume(consumed);
417
418            continue;
419        }
420
421        // Iterate over the buffers, the zip iterator will stop us as soon as the
422        // first one runs out.
423        for (&from_byte, &to_byte) in from_buf.iter().zip(to_buf.iter()) {
424            if from_byte != to_byte {
425                compare = Cmp::Different;
426
427                if params.verbose {
428                    format_verbose_difference(
429                        from_byte,
430                        to_byte,
431                        at_byte,
432                        offset_width,
433                        &mut output,
434                        params,
435                    )?;
436                    stdout.write_all(output.as_slice()).map_err(|e| {
437                        format!(
438                            "{}: error printing output: {e}",
439                            params.executable.to_string_lossy()
440                        )
441                    })?;
442                    output.clear();
443                } else {
444                    report_difference(from_byte, to_byte, at_byte, at_line, params);
445                    return Ok(Cmp::Different);
446                }
447            }
448
449            start_of_line = from_byte == b'\n';
450            if start_of_line {
451                at_line += 1;
452            }
453
454            at_byte += 1;
455
456            if let Some(max_bytes) = params.max_bytes {
457                if at_byte > max_bytes {
458                    break;
459                }
460            }
461        }
462
463        // Notify our readers about the bytes we went over.
464        from.consume(consumed);
465        to.consume(consumed);
466    }
467
468    Ok(compare)
469}
470
471// Exit codes are documented at
472// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-cmp.html
473//     An exit status of 0 means no differences were found,
474//     1 means some differences were found,
475//     and 2 means trouble.
476pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
477    let params = match parse_params(opts) {
478        Ok(param) => param,
479        Err(e) => {
480            eprintln!("{e}");
481            return ExitCode::from(2);
482        }
483    };
484
485    if params.from == "-" && params.to == "-"
486        || same_file::is_same_file(&params.from, &params.to).unwrap_or(false)
487    {
488        return ExitCode::SUCCESS;
489    }
490
491    match cmp(&params) {
492        Ok(Cmp::Equal) => ExitCode::SUCCESS,
493        Ok(Cmp::Different) => ExitCode::from(1),
494        Err(e) => {
495            if !params.quiet {
496                eprintln!("{e}");
497            }
498            ExitCode::from(2)
499        }
500    }
501}
502
503#[inline]
504fn is_ascii_printable(byte: u8) -> bool {
505    let c = byte as char;
506    c.is_ascii() && !c.is_ascii_control()
507}
508
509#[inline]
510fn format_octal(byte: u8, buf: &mut [u8; 3]) -> &str {
511    *buf = [b' ', b' ', b'0'];
512
513    let mut num = byte;
514    let mut idx = 2; // Start at the last position in the buffer
515
516    // Generate octal digits
517    while num > 0 {
518        buf[idx] = b'0' + num % 8;
519        num /= 8;
520        idx = idx.saturating_sub(1);
521    }
522
523    // SAFETY: the operations we do above always land within ascii range.
524    unsafe { std::str::from_utf8_unchecked(&buf[..]) }
525}
526
527#[inline]
528fn format_byte(byte: u8) -> String {
529    let mut byte = byte;
530    let mut quoted = vec![];
531
532    if !is_ascii_printable(byte) {
533        if byte >= 128 {
534            quoted.push(b'M');
535            quoted.push(b'-');
536            byte -= 128;
537        }
538
539        if byte < 32 {
540            quoted.push(b'^');
541            byte += 64;
542        } else if byte == 127 {
543            quoted.push(b'^');
544            byte = b'?';
545        }
546        assert!((byte as char).is_ascii());
547    }
548
549    quoted.push(byte);
550
551    // SAFETY: the checks and shifts we do above match what cat and GNU
552    // cmp do to ensure characters fall inside the ascii range.
553    unsafe { String::from_utf8_unchecked(quoted) }
554}
555
556// This function has been optimized to not use the Rust fmt system, which
557// leads to a massive speed up when processing large files: cuts the time
558// for comparing 2 ~36MB completely different files in half on an M1 Max.
559#[inline]
560fn format_verbose_difference(
561    from_byte: u8,
562    to_byte: u8,
563    at_byte: usize,
564    offset_width: usize,
565    output: &mut Vec<u8>,
566    params: &Params,
567) -> Result<(), String> {
568    assert!(!params.quiet);
569
570    let mut at_byte_buf = itoa::Buffer::new();
571    let mut from_oct = [0u8; 3]; // for octal conversions
572    let mut to_oct = [0u8; 3];
573
574    if params.print_bytes {
575        // "{:>width$} {:>3o} {:4} {:>3o} {}",
576        let at_byte_str = at_byte_buf.format(at_byte);
577        let at_byte_padding = offset_width.saturating_sub(at_byte_str.len());
578
579        for _ in 0..at_byte_padding {
580            output.push(b' ')
581        }
582
583        output.extend_from_slice(at_byte_str.as_bytes());
584
585        output.push(b' ');
586
587        output.extend_from_slice(format_octal(from_byte, &mut from_oct).as_bytes());
588
589        output.push(b' ');
590
591        let from_byte_str = format_byte(from_byte);
592        let from_byte_padding = 4 - from_byte_str.len();
593
594        output.extend_from_slice(from_byte_str.as_bytes());
595
596        for _ in 0..from_byte_padding {
597            output.push(b' ')
598        }
599
600        output.push(b' ');
601
602        output.extend_from_slice(format_octal(to_byte, &mut to_oct).as_bytes());
603
604        output.push(b' ');
605
606        output.extend_from_slice(format_byte(to_byte).as_bytes());
607
608        output.push(b'\n');
609    } else {
610        // "{:>width$} {:>3o} {:>3o}"
611        let at_byte_str = at_byte_buf.format(at_byte);
612        let at_byte_padding = offset_width - at_byte_str.len();
613
614        for _ in 0..at_byte_padding {
615            output.push(b' ')
616        }
617
618        output.extend_from_slice(at_byte_str.as_bytes());
619
620        output.push(b' ');
621
622        output.extend_from_slice(format_octal(from_byte, &mut from_oct).as_bytes());
623
624        output.push(b' ');
625
626        output.extend_from_slice(format_octal(to_byte, &mut to_oct).as_bytes());
627
628        output.push(b'\n');
629    }
630
631    Ok(())
632}
633
634#[inline]
635fn report_eof(at_byte: usize, at_line: usize, start_of_line: bool, eof_on: &str, params: &Params) {
636    if params.quiet {
637        return;
638    }
639
640    if at_byte == 1 {
641        eprintln!(
642            "{}: EOF on '{}' which is empty",
643            params.executable.to_string_lossy(),
644            eof_on
645        );
646    } else if params.verbose {
647        eprintln!(
648            "{}: EOF on '{}' after byte {}",
649            params.executable.to_string_lossy(),
650            eof_on,
651            at_byte - 1,
652        );
653    } else if start_of_line {
654        eprintln!(
655            "{}: EOF on '{}' after byte {}, line {}",
656            params.executable.to_string_lossy(),
657            eof_on,
658            at_byte - 1,
659            at_line - 1
660        );
661    } else {
662        eprintln!(
663            "{}: EOF on '{}' after byte {}, in line {}",
664            params.executable.to_string_lossy(),
665            eof_on,
666            at_byte - 1,
667            at_line
668        );
669    }
670}
671
672fn is_posix_locale() -> bool {
673    let locale = if let Ok(locale) = env::var("LC_ALL") {
674        locale
675    } else if let Ok(locale) = env::var("LC_MESSAGES") {
676        locale
677    } else if let Ok(locale) = env::var("LANG") {
678        locale
679    } else {
680        "C".to_string()
681    };
682
683    locale == "C" || locale == "POSIX"
684}
685
686#[inline]
687fn report_difference(from_byte: u8, to_byte: u8, at_byte: usize, at_line: usize, params: &Params) {
688    if params.quiet {
689        return;
690    }
691
692    let term = if is_posix_locale() && !params.print_bytes {
693        "char"
694    } else {
695        "byte"
696    };
697    print!(
698        "{} {} differ: {term} {}, line {}",
699        &params.from.to_string_lossy(),
700        &params.to.to_string_lossy(),
701        at_byte,
702        at_line
703    );
704    if params.print_bytes {
705        let char_width = if to_byte >= 0x7F { 2 } else { 1 };
706        print!(
707            " is {:>3o} {:char_width$} {:>3o} {:char_width$}",
708            from_byte,
709            format_byte(from_byte),
710            to_byte,
711            format_byte(to_byte)
712        );
713    }
714    println!();
715}
716
717#[cfg(test)]
718mod tests {
719    use super::*;
720    fn os(s: &str) -> OsString {
721        OsString::from(s)
722    }
723
724    #[test]
725    fn positional() {
726        assert_eq!(
727            Ok(Params {
728                executable: os("cmp"),
729                from: os("foo"),
730                to: os("bar"),
731                ..Default::default()
732            }),
733            parse_params([os("cmp"), os("foo"), os("bar")].iter().cloned().peekable())
734        );
735
736        assert_eq!(
737            Ok(Params {
738                executable: os("cmp"),
739                from: os("foo"),
740                to: os("-"),
741                ..Default::default()
742            }),
743            parse_params([os("cmp"), os("foo")].iter().cloned().peekable())
744        );
745
746        assert_eq!(
747            Ok(Params {
748                executable: os("cmp"),
749                from: os("foo"),
750                to: os("--help"),
751                ..Default::default()
752            }),
753            parse_params(
754                [os("cmp"), os("foo"), os("--"), os("--help")]
755                    .iter()
756                    .cloned()
757                    .peekable()
758            )
759        );
760
761        assert_eq!(
762            Ok(Params {
763                executable: os("cmp"),
764                from: os("foo"),
765                to: os("bar"),
766                skip_a: Some(1),
767                skip_b: None,
768                ..Default::default()
769            }),
770            parse_params(
771                [os("cmp"), os("foo"), os("bar"), os("1")]
772                    .iter()
773                    .cloned()
774                    .peekable()
775            )
776        );
777
778        assert_eq!(
779            Ok(Params {
780                executable: os("cmp"),
781                from: os("foo"),
782                to: os("bar"),
783                skip_a: Some(1),
784                skip_b: Some(usize::MAX),
785                ..Default::default()
786            }),
787            parse_params(
788                [os("cmp"), os("foo"), os("bar"), os("1"), os("2Y")]
789                    .iter()
790                    .cloned()
791                    .peekable()
792            )
793        );
794
795        // Bad positional arguments.
796        assert_eq!(
797            Err("Usage: cmp <from> <to>".to_string()),
798            parse_params(
799                [os("cmp"), os("foo"), os("bar"), os("1"), os("2"), os("3")]
800                    .iter()
801                    .cloned()
802                    .peekable()
803            )
804        );
805        assert_eq!(
806            Err("Usage: cmp <from> <to>".to_string()),
807            parse_params([os("cmp")].iter().cloned().peekable())
808        );
809    }
810
811    #[test]
812    fn execution_modes() {
813        let print_bytes = Params {
814            executable: os("cmp"),
815            from: os("foo"),
816            to: os("bar"),
817            print_bytes: true,
818            ..Default::default()
819        };
820        assert_eq!(
821            Ok(print_bytes.clone()),
822            parse_params(
823                [os("cmp"), os("-b"), os("foo"), os("bar")]
824                    .iter()
825                    .cloned()
826                    .peekable()
827            )
828        );
829        assert_eq!(
830            Ok(print_bytes),
831            parse_params(
832                [os("cmp"), os("--print-bytes"), os("foo"), os("bar")]
833                    .iter()
834                    .cloned()
835                    .peekable()
836            )
837        );
838
839        let verbose = Params {
840            executable: os("cmp"),
841            from: os("foo"),
842            to: os("bar"),
843            verbose: true,
844            ..Default::default()
845        };
846        assert_eq!(
847            Ok(verbose.clone()),
848            parse_params(
849                [os("cmp"), os("-l"), os("foo"), os("bar")]
850                    .iter()
851                    .cloned()
852                    .peekable()
853            )
854        );
855        assert_eq!(
856            Ok(verbose),
857            parse_params(
858                [os("cmp"), os("--verbose"), os("foo"), os("bar")]
859                    .iter()
860                    .cloned()
861                    .peekable()
862            )
863        );
864
865        let verbose_and_print_bytes = Params {
866            executable: os("cmp"),
867            from: os("foo"),
868            to: os("bar"),
869            print_bytes: true,
870            verbose: true,
871            ..Default::default()
872        };
873        assert_eq!(
874            Ok(verbose_and_print_bytes.clone()),
875            parse_params(
876                [os("cmp"), os("-l"), os("-b"), os("foo"), os("bar")]
877                    .iter()
878                    .cloned()
879                    .peekable()
880            )
881        );
882        assert_eq!(
883            Ok(verbose_and_print_bytes.clone()),
884            parse_params(
885                [os("cmp"), os("-lb"), os("foo"), os("bar")]
886                    .iter()
887                    .cloned()
888                    .peekable()
889            )
890        );
891        assert_eq!(
892            Ok(verbose_and_print_bytes),
893            parse_params(
894                [os("cmp"), os("-bl"), os("foo"), os("bar")]
895                    .iter()
896                    .cloned()
897                    .peekable()
898            )
899        );
900
901        assert_eq!(
902            Ok(Params {
903                executable: os("cmp"),
904                from: os("foo"),
905                to: os("bar"),
906                quiet: true,
907                ..Default::default()
908            }),
909            parse_params(
910                [os("cmp"), os("-s"), os("foo"), os("bar")]
911                    .iter()
912                    .cloned()
913                    .peekable()
914            )
915        );
916
917        // Some options do not mix.
918        assert_eq!(
919            Err("cmp: options -l and -s are incompatible".to_string()),
920            parse_params(
921                [os("cmp"), os("-l"), os("-s"), os("foo"), os("bar")]
922                    .iter()
923                    .cloned()
924                    .peekable()
925            )
926        );
927    }
928
929    #[test]
930    fn max_bytes() {
931        let max_bytes = Params {
932            executable: os("cmp"),
933            from: os("foo"),
934            to: os("bar"),
935            max_bytes: Some(1),
936            ..Default::default()
937        };
938        assert_eq!(
939            Ok(max_bytes.clone()),
940            parse_params(
941                [os("cmp"), os("-n"), os("1"), os("foo"), os("bar")]
942                    .iter()
943                    .cloned()
944                    .peekable()
945            )
946        );
947        assert_eq!(
948            Ok(max_bytes),
949            parse_params(
950                [os("cmp"), os("--bytes=1"), os("foo"), os("bar")]
951                    .iter()
952                    .cloned()
953                    .peekable()
954            )
955        );
956
957        assert_eq!(
958            Ok(Params {
959                executable: os("cmp"),
960                from: os("foo"),
961                to: os("bar"),
962                max_bytes: Some(usize::MAX),
963                ..Default::default()
964            }),
965            parse_params(
966                [
967                    os("cmp"),
968                    os("--bytes=99999999999999999999999999999999999999999999999999999999999"),
969                    os("foo"),
970                    os("bar")
971                ]
972                .iter()
973                .cloned()
974                .peekable()
975            )
976        );
977
978        // Failure case
979        assert_eq!(
980            Err("cmp: invalid --bytes value '1K'".to_string()),
981            parse_params(
982                [os("cmp"), os("--bytes=1K"), os("foo"), os("bar")]
983                    .iter()
984                    .cloned()
985                    .peekable()
986            )
987        );
988    }
989
990    #[test]
991    fn skips() {
992        let skips = Params {
993            executable: os("cmp"),
994            from: os("foo"),
995            to: os("bar"),
996            skip_a: Some(1),
997            skip_b: Some(1),
998            ..Default::default()
999        };
1000        assert_eq!(
1001            Ok(skips.clone()),
1002            parse_params(
1003                [os("cmp"), os("-i"), os("1"), os("foo"), os("bar")]
1004                    .iter()
1005                    .cloned()
1006                    .peekable()
1007            )
1008        );
1009        assert_eq!(
1010            Ok(skips),
1011            parse_params(
1012                [os("cmp"), os("--ignore-initial=1"), os("foo"), os("bar")]
1013                    .iter()
1014                    .cloned()
1015                    .peekable()
1016            )
1017        );
1018
1019        assert_eq!(
1020            Ok(Params {
1021                executable: os("cmp"),
1022                from: os("foo"),
1023                to: os("bar"),
1024                skip_a: Some(usize::MAX),
1025                skip_b: Some(usize::MAX),
1026                ..Default::default()
1027            }),
1028            parse_params(
1029                [
1030                    os("cmp"),
1031                    os("-i"),
1032                    os("99999999999999999999999999999999999999999999999999999999999"),
1033                    os("foo"),
1034                    os("bar")
1035                ]
1036                .iter()
1037                .cloned()
1038                .peekable()
1039            )
1040        );
1041
1042        assert_eq!(
1043            Ok(Params {
1044                executable: os("cmp"),
1045                from: os("foo"),
1046                to: os("bar"),
1047                skip_a: Some(1),
1048                skip_b: Some(2),
1049                ..Default::default()
1050            }),
1051            parse_params(
1052                [os("cmp"), os("--ignore-initial=1:2"), os("foo"), os("bar")]
1053                    .iter()
1054                    .cloned()
1055                    .peekable()
1056            )
1057        );
1058
1059        assert_eq!(
1060            Ok(Params {
1061                executable: os("cmp"),
1062                from: os("foo"),
1063                to: os("bar"),
1064                skip_a: Some(1_000_000_000),
1065                skip_b: Some(1_152_921_504_606_846_976 * 2),
1066                ..Default::default()
1067            }),
1068            parse_params(
1069                [
1070                    os("cmp"),
1071                    os("--ignore-initial=1GB:2E"),
1072                    os("foo"),
1073                    os("bar")
1074                ]
1075                .iter()
1076                .cloned()
1077                .peekable()
1078            )
1079        );
1080
1081        // All special suffixes.
1082        for (i, suffixes) in [
1083            ["kB", "K"],
1084            ["MB", "M"],
1085            ["GB", "G"],
1086            ["TB", "T"],
1087            ["PB", "P"],
1088            ["EB", "E"],
1089            ["ZB", "Z"],
1090            ["YB", "Y"],
1091        ]
1092        .iter()
1093        .enumerate()
1094        {
1095            let values = [
1096                1_000usize.checked_pow((i + 1) as u32).unwrap_or(usize::MAX),
1097                1024usize.checked_pow((i + 1) as u32).unwrap_or(usize::MAX),
1098            ];
1099            for (j, v) in values.iter().enumerate() {
1100                assert_eq!(
1101                    Ok(Params {
1102                        executable: os("cmp"),
1103                        from: os("foo"),
1104                        to: os("bar"),
1105                        skip_a: Some(*v),
1106                        skip_b: Some(2),
1107                        ..Default::default()
1108                    }),
1109                    parse_params(
1110                        [
1111                            os("cmp"),
1112                            os("-i"),
1113                            os(&format!("1{}:2", suffixes[j])),
1114                            os("foo"),
1115                            os("bar"),
1116                        ]
1117                        .iter()
1118                        .cloned()
1119                        .peekable()
1120                    )
1121                );
1122            }
1123        }
1124
1125        // Ignores positional arguments when -i is provided.
1126        assert_eq!(
1127            Ok(Params {
1128                executable: os("cmp"),
1129                from: os("foo"),
1130                to: os("bar"),
1131                skip_a: Some(1),
1132                skip_b: Some(2),
1133                ..Default::default()
1134            }),
1135            parse_params(
1136                [
1137                    os("cmp"),
1138                    os("-i"),
1139                    os("1:2"),
1140                    os("foo"),
1141                    os("bar"),
1142                    os("3"),
1143                    os("4")
1144                ]
1145                .iter()
1146                .cloned()
1147                .peekable()
1148            )
1149        );
1150
1151        // Failure cases
1152        assert_eq!(
1153            Err("cmp: invalid --ignore-initial value '1mb'".to_string()),
1154            parse_params(
1155                [os("cmp"), os("--ignore-initial=1mb"), os("foo"), os("bar")]
1156                    .iter()
1157                    .cloned()
1158                    .peekable()
1159            )
1160        );
1161        assert_eq!(
1162            Err("cmp: invalid --ignore-initial value '1:2:3'".to_string()),
1163            parse_params(
1164                [
1165                    os("cmp"),
1166                    os("--ignore-initial=1:2:3"),
1167                    os("foo"),
1168                    os("bar")
1169                ]
1170                .iter()
1171                .cloned()
1172                .peekable()
1173            )
1174        );
1175        assert_eq!(
1176            Err("cmp: invalid --ignore-initial value '-1'".to_string()),
1177            parse_params(
1178                [os("cmp"), os("--ignore-initial=-1"), os("foo"), os("bar")]
1179                    .iter()
1180                    .cloned()
1181                    .peekable()
1182            )
1183        );
1184    }
1185}