Skip to content

Commit c17117d

Browse files
committed
kernel of sdiff tool is ready
1 parent c0fd8fd commit c17117d

File tree

3 files changed

+173
-8
lines changed

3 files changed

+173
-8
lines changed

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ fn main() -> ExitCode {
7272
match util_name.to_str() {
7373
Some("diff") => diff::main(args),
7474
Some("cmp") => cmp::main(args),
75-
Some("sdiff") => sdiff::main(),
75+
Some("sdiff") => sdiff::main(args),
7676
Some(name) => {
7777
eprintln!("{}: utility not supported", name);
7878
ExitCode::from(2)

src/sdiff.rs

Lines changed: 168 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,170 @@
1-
use std::process::ExitCode;
1+
use core::{fmt, panic};
2+
use std::{
3+
env::ArgsOs,
4+
ffi::OsString,
5+
fs,
6+
io::{stdin, Read, Write},
7+
iter::Peekable,
8+
process::ExitCode,
9+
vec,
10+
};
11+
12+
#[derive(Debug, PartialEq, Eq)]
13+
struct Params {
14+
file1: OsString,
15+
file2: OsString,
16+
}
17+
18+
#[derive(Debug, PartialEq, Eq)]
19+
enum ParseErr {
20+
InsufficientArgs,
21+
}
22+
23+
impl fmt::Display for ParseErr {
24+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result {
25+
match self {
26+
ParseErr::InsufficientArgs => write!(f, "Insufficient args passed"),
27+
}
28+
}
29+
}
30+
31+
impl std::error::Error for ParseErr {}
32+
33+
// Exit codes are documented at
34+
// https://www.gnu.org/software/diffutils/manual/html_node/Invoking-sdiff.html.
35+
// An exit status of 0 means no differences were found,
36+
// 1 means some differences were found,
37+
// and 2 means trouble.
38+
pub fn main(opts: Peekable<ArgsOs>) -> ExitCode {
39+
let Ok(params) = parse_params(opts) else {
40+
// if we have insufficient args ...
41+
eprintln!("Usage: <exe> <file1> <file2>");
42+
return ExitCode::from(2);
43+
};
44+
45+
// first we need to get the properly files
46+
let file1 = read_file_contents(&params.file1);
47+
let file2 = read_file_contents(&params.file2);
48+
49+
// now we get the lines from the files as bytes, cuz the sdiff
50+
// must be compatible with ut8, ascii etc.
51+
let mut lines_left: Vec<&[u8]> = file1.split(|&c| c == b'\n').collect();
52+
let mut lines_rght: Vec<&[u8]> = file2.split(|&c| c == b'\n').collect();
53+
54+
// for some reason, the original file appends a empty line at
55+
// the end of file. I did not search for it, but my guess is
56+
// that this is EOL or an zeroed terminated file. Just remove it
57+
if lines_left.last() == Some(&&b""[..]) {
58+
lines_left.pop();
59+
}
60+
61+
if lines_rght.last() == Some(&&b""[..]) {
62+
lines_rght.pop();
63+
}
64+
65+
let mut output: Vec<u8> = Vec::new();
66+
let width = 60;
67+
let max_lines = lines_left.len().max(lines_rght.len());
68+
69+
// ok, now we start running over the lines and get the lines right
70+
// and left file
71+
for i in 0..max_lines {
72+
let left = lines_left.get(i).map(|l| String::from_utf8_lossy(l)); // we can convert this is ut8?
73+
let right = lines_rght.get(i).map(|r| String::from_utf8_lossy(r));
74+
75+
match (left, right) {
76+
(Some(l), Some(r)) if l == r => {
77+
// this is nice, cuz if the line is empty we stiill can print it, cause it equal : )
78+
writeln!(output, "{:<width$} {}", l, r, width = width).unwrap();
79+
}
80+
(Some(l), Some(r)) => {
81+
// if both lines are present but not equal, they are different, just print with |
82+
writeln!(output, "{:<width$} | {}", l, r, width = width).unwrap();
83+
}
84+
(Some(l), None) => {
85+
// we have only left val, so print it with <
86+
writeln!(output, "{:<width$} <", l, width = width).unwrap();
87+
}
88+
(None, Some(r)) => {
89+
// we have only the ...
90+
writeln!(output, "{:<width$} > {}", "", r, width = width).unwrap();
91+
}
92+
_ => {}
93+
}
94+
}
95+
96+
// now print the line at stdout
97+
println!("{}", String::from_utf8(output).unwrap());
298

3-
pub fn main() -> ExitCode {
499
ExitCode::SUCCESS
5-
}
100+
}
101+
102+
fn parse_params<I: Iterator<Item = OsString>>(mut opts: Peekable<I>) -> Result<Params, ParseErr> {
103+
opts.next(); // this is the executable name, just jmp it
104+
105+
let Some(arg1) = opts.next() else {
106+
return Err(ParseErr::InsufficientArgs);
107+
};
108+
let Some(arg2) = opts.next() else {
109+
return Err(ParseErr::InsufficientArgs);
110+
};
111+
112+
Ok(Params {
113+
file1: arg1,
114+
file2: arg2,
115+
})
116+
}
117+
118+
fn read_file_contents(filepath: &OsString) -> Vec<u8> {
119+
if filepath == "-" {
120+
get_file_from_stdin()
121+
} else {
122+
fs::read(filepath).unwrap()
123+
}
124+
}
125+
126+
fn get_file_from_stdin() -> Vec<u8> {
127+
let mut stdin = stdin().lock();
128+
let mut buf: Vec<u8> = vec![];
129+
130+
if let Ok(_) = stdin.read_to_end(&mut buf) {
131+
return buf;
132+
} else {
133+
panic!("Failed to read from stdin")
134+
}
135+
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use std::ffi::OsString;
140+
141+
use crate::sdiff::{parse_params, Params, ParseErr};
142+
143+
fn str_os(str: &str) -> OsString {
144+
OsString::from(str)
145+
}
146+
147+
#[test]
148+
fn test_params_convert() {
149+
assert_eq!(
150+
Ok(Params {
151+
file1: str_os("file1"),
152+
file2: str_os("file2")
153+
}),
154+
parse_params(
155+
[str_os("file1"), str_os("file2")]
156+
.iter()
157+
.cloned()
158+
.peekable()
159+
)
160+
);
161+
}
162+
163+
#[test]
164+
fn parse_params_returns_err_insufficient_args_when_opts_iter_has_not_even_one_item() {
165+
assert_eq!(
166+
Err(ParseErr::InsufficientArgs),
167+
parse_params([].iter().cloned().peekable())
168+
)
169+
}
170+
}

0 commit comments

Comments
 (0)