1use 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 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 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 let multiplier: usize = match ¶m[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 #[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, "Z" => usize::MAX, "YB" => usize::MAX, "Y" => usize::MAX, _ => {
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(¶m_str, ¶m_str)?);
228 } else if skip_pos2.is_none() {
229 skip_pos2 = Some(parse_skip(¶m_str, ¶m_str)?);
230 } else {
231 return Err(usage_string(&executable_str));
232 }
233 }
234
235 #[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 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(¶m_str, ¶m_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(¶m_str, ¶m_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 ¶ms.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 ¶ms.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(¶ms.from, ¶ms.skip_a, params)?;
327 let mut to = prepare_reader(¶ms.to, ¶ms.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(¶ms.from), fs::metadata(¶ms.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 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 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 let from_buf = match from.fill_buf() {
361 Ok(buf) => buf,
362 Err(e) => {
363 return Err(format_failure_to_read_input_file(
364 ¶ms.executable,
365 ¶ms.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 ¶ms.executable,
376 ¶ms.to,
377 &e,
378 ));
379 }
380 };
381
382 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 ¶ms.from.to_string_lossy()
390 } else {
391 ¶ms.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 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 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 from.consume(consumed);
465 to.consume(consumed);
466 }
467
468 Ok(compare)
469}
470
471pub 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(¶ms.from, ¶ms.to).unwrap_or(false)
487 {
488 return ExitCode::SUCCESS;
489 }
490
491 match cmp(¶ms) {
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; while num > 0 {
518 buf[idx] = b'0' + num % 8;
519 num /= 8;
520 idx = idx.saturating_sub(1);
521 }
522
523 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 unsafe { String::from_utf8_unchecked(quoted) }
554}
555
556#[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]; let mut to_oct = [0u8; 3];
573
574 if params.print_bytes {
575 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 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 ¶ms.from.to_string_lossy(),
700 ¶ms.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 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 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 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 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 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 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}