fcmp/
ops.rs

1////////////////////////////////////////////////////////////////////////////////
2// Fcmp file compare utility
3////////////////////////////////////////////////////////////////////////////////
4// Copyright 2020 Skylor R. Schermer
5// This code is dual licenced using the MIT or Apache 2 license.
6// See licence-mit.md and licence-apache.md for details.
7////////////////////////////////////////////////////////////////////////////////
8//! File compare operations.
9////////////////////////////////////////////////////////////////////////////////
10
11
12// External library imports.
13#[cfg(feature = "serde")]
14use serde::Serialize;
15#[cfg(feature = "serde")]
16use serde::Deserialize;
17
18// Standard library imports.
19use std::path::Path;
20use std::io::BufRead as _;
21use std::io::BufReader;
22use std::io::ErrorKind;
23use std::process::Command;
24use std::ops::Not;
25use std::fs::File;
26
27
28/// A diff operation.
29#[derive(Debug, Clone)]
30#[cfg_attr(feature = "serde", derive(Serialize))]
31#[cfg_attr(feature = "serde", derive(Deserialize))]
32pub enum DiffOp {
33	/// No diff will be performed.
34	None,
35	
36	/// An internal diff will be used.
37	Internal,
38
39	/// A diff command will be run as a subprocess.
40	Subprocess {
41		/// The command to execute.
42		command: &'static str,
43		/// The arguments to pass to it.
44		args: Vec<&'static str>,
45	},
46}
47
48
49impl DiffOp {
50	/// Returns a `DiffOp` that will execute a POSIX diff subprocess.
51	#[must_use]
52	pub fn posix_diff() -> Self {
53		Self::Subprocess {
54			command: "diff",
55			args: vec![],
56		}
57	}
58
59	/// Returns a `DiffOp` that will execute a POSIX cmp subprocess.
60	#[must_use]
61	pub fn posix_cmp() -> Self {
62		Self::Subprocess {
63			command: "cmp",
64			args: vec!["-s"],
65		}
66	}
67	
68
69	/// Returns true if the files at the given paths are different.
70	pub fn diff(&self, a: &Path, b: &Path) -> Result<bool, std::io::Error> {
71		match self {
72			Self::None => Ok(a != b),
73
74			Self::Internal => {
75				let file_a = match File::options().read(true).open(a) {
76					Ok(f)  => Some(f),
77					Err(e) if matches!(e.kind(), ErrorKind::NotFound) => None,
78					Err(e) => return Err(e),
79				};
80
81				let file_b = match File::options().read(true).open(b) {
82					Ok(f)  => Some(f),
83					Err(e) if matches!(e.kind(), ErrorKind::NotFound) => None,
84					Err(e) => return Err(e),
85				};
86
87				match (file_a, file_b) {
88					(Some(a), Some(b)) => {
89						let meta_a = a.metadata().expect("get file metadata");
90						let meta_b = b.metadata().expect("get file metadata");
91
92						if meta_a.len() != meta_b.len()
93							|| meta_a.is_symlink()
94							|| meta_b.is_symlink()
95							|| meta_a.file_type() != meta_b.file_type()
96						{
97							 Ok(false)
98						} else {
99							Self::internal_eq(&a, &b)
100								.map(bool::not)
101						}
102					},
103
104					(None, None) => Ok(true),
105					_            => Ok(false),
106				}
107			},
108
109			Self::Subprocess { command, args } => {
110				let status = Command::new(command)
111					.args(args)
112					.arg(a)
113					.arg(b)
114					.status()?;
115
116				match status.code() {
117					Some(0) => Ok(false),
118					Some(1) => Ok(true),
119					Some(_) => Err(std::io::Error::from(ErrorKind::Other)),
120					None => Err(std::io::Error::from(ErrorKind::Interrupted)),
121				}
122			},
123			
124		}
125	}
126
127	/// Returns `true` if the given files have the same content.
128	///
129	/// ### Errors
130	///
131	/// Returns a [`std::io::Error`] if the file's contents fail to read
132	/// correctly.
133	///
134	/// [`std::io::Error`]: std::io::Error
135	fn internal_eq(a: &File, b: &File) -> Result<bool, std::io::Error> {
136		let mut buf_reader_a = BufReader::new(a);
137		let mut buf_reader_b = BufReader::new(b);
138
139		loop {
140			let buf_a = buf_reader_a.fill_buf()?;
141			let buf_b = buf_reader_b.fill_buf()?;
142
143			if buf_a.is_empty() && buf_b.is_empty() {
144				return Ok(true);
145			}
146
147			let read_len = if buf_a.len() <= buf_b.len() {
148				buf_a.len()
149			} else {
150				buf_b.len()
151			};
152
153			if buf_a[0..read_len] != buf_b[0..read_len] {
154				return Ok(false);
155			}
156
157			buf_reader_a.consume(read_len);
158			buf_reader_b.consume(read_len);
159		}
160	}
161}