Rewrote head (#1911)

See https://github.com/uutils/coreutils/pull/1911
for the details
This commit is contained in:
Mikadore
2021-03-29 13:08:48 +02:00
committed by GitHub
parent da5f2f3a6c
commit 8320b1ec5f
8 changed files with 1235 additions and 256 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ edition = "2018"
path = "src/head.rs"
[dependencies]
libc = "0.2.42"
clap = "2.33"
uucore = { version=">=0.0.7", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.5", package="uucore_procs", path="../../uucore_procs" }
+615 -213
View File
File diff suppressed because it is too large Load Diff
+282
View File
@@ -0,0 +1,282 @@
use std::convert::TryFrom;
use std::ffi::OsString;
#[derive(PartialEq, Debug)]
pub enum ParseError {
Syntax,
Overflow,
}
/// Parses obsolete syntax
/// head -NUM[kmzv]
pub fn parse_obsolete(src: &str) -> Option<Result<impl Iterator<Item = OsString>, ParseError>> {
let mut chars = src.char_indices();
if let Some((_, '-')) = chars.next() {
let mut num_end = 0usize;
let mut has_num = false;
let mut last_char = 0 as char;
while let Some((n, c)) = chars.next() {
if c.is_numeric() {
has_num = true;
num_end = n;
} else {
last_char = c;
break;
}
}
if has_num {
match src[1..=num_end].parse::<usize>() {
Ok(num) => {
let mut quiet = false;
let mut verbose = false;
let mut zero_terminated = false;
let mut multiplier = None;
let mut c = last_char;
loop {
// not that here, we only match lower case 'k', 'c', and 'm'
match c {
// we want to preserve order
// this also saves us 1 heap allocation
'q' => {
quiet = true;
verbose = false
}
'v' => {
verbose = true;
quiet = false
}
'z' => zero_terminated = true,
'c' => multiplier = Some(1),
'b' => multiplier = Some(512),
'k' => multiplier = Some(1024),
'm' => multiplier = Some(1024 * 1024),
'\0' => {}
_ => return Some(Err(ParseError::Syntax)),
}
if let Some((_, next)) = chars.next() {
c = next
} else {
break;
}
}
let mut options = Vec::new();
if quiet {
options.push(OsString::from("-q"))
}
if verbose {
options.push(OsString::from("-v"))
}
if zero_terminated {
options.push(OsString::from("-z"))
}
if let Some(n) = multiplier {
options.push(OsString::from("-c"));
let num = match num.checked_mul(n) {
Some(n) => n,
None => return Some(Err(ParseError::Overflow)),
};
options.push(OsString::from(format!("{}", num)));
} else {
options.push(OsString::from("-n"));
options.push(OsString::from(format!("{}", num)));
}
Some(Ok(options.into_iter()))
}
Err(_) => Some(Err(ParseError::Overflow)),
}
} else {
None
}
} else {
None
}
}
/// Parses an -c or -n argument,
/// the bool specifies whether to read from the end
pub fn parse_num(src: &str) -> Result<(usize, bool), ParseError> {
let mut num_start = 0;
let mut chars = src.char_indices();
let (mut chars, all_but_last) = match chars.next() {
Some((_, c)) => {
if c == '-' {
num_start += 1;
(chars, true)
} else {
(src.char_indices(), false)
}
}
None => return Err(ParseError::Syntax),
};
let mut num_end = 0usize;
let mut last_char = 0 as char;
let mut num_count = 0usize;
while let Some((n, c)) = chars.next() {
if c.is_numeric() {
num_end = n;
num_count += 1;
} else {
last_char = c;
break;
}
}
let num = if num_count > 0 {
match src[num_start..=num_end].parse::<usize>() {
Ok(n) => Some(n),
Err(_) => return Err(ParseError::Overflow),
}
} else {
None
};
if last_char == 0 as char {
if let Some(n) = num {
Ok((n, all_but_last))
} else {
Err(ParseError::Syntax)
}
} else {
let base: u128 = match chars.next() {
Some((_, c)) => {
let b = match c {
'B' if last_char != 'b' => 1000,
'i' if last_char != 'b' => {
if let Some((_, 'B')) = chars.next() {
1024
} else {
return Err(ParseError::Syntax);
}
}
_ => return Err(ParseError::Syntax),
};
if chars.next().is_some() {
return Err(ParseError::Syntax);
} else {
b
}
}
None => 1024,
};
let mul = match last_char.to_lowercase().next().unwrap() {
'b' => 512,
'k' => base.pow(1),
'm' => base.pow(2),
'g' => base.pow(3),
't' => base.pow(4),
'p' => base.pow(5),
'e' => base.pow(6),
'z' => base.pow(7),
'y' => base.pow(8),
_ => return Err(ParseError::Syntax),
};
let mul = match usize::try_from(mul) {
Ok(n) => n,
Err(_) => return Err(ParseError::Overflow),
};
match num.unwrap_or(1).checked_mul(mul) {
Some(n) => Ok((n, all_but_last)),
None => Err(ParseError::Overflow),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn obsolete(src: &str) -> Option<Result<Vec<String>, ParseError>> {
let r = parse_obsolete(src);
match r {
Some(s) => match s {
Ok(v) => Some(Ok(v.map(|s| s.to_str().unwrap().to_owned()).collect())),
Err(e) => Some(Err(e)),
},
None => None,
}
}
fn obsolete_result(src: &[&str]) -> Option<Result<Vec<String>, ParseError>> {
Some(Ok(src.iter().map(|s| s.to_string()).collect()))
}
#[test]
#[cfg(not(target_pointer_width = "128"))]
fn test_parse_overflow_x64() {
assert_eq!(parse_num("1Y"), Err(ParseError::Overflow));
assert_eq!(parse_num("1Z"), Err(ParseError::Overflow));
assert_eq!(parse_num("100E"), Err(ParseError::Overflow));
assert_eq!(parse_num("100000P"), Err(ParseError::Overflow));
assert_eq!(parse_num("1000000000T"), Err(ParseError::Overflow));
assert_eq!(
parse_num("10000000000000000000000"),
Err(ParseError::Overflow)
);
}
#[test]
#[cfg(target_pointer_width = "32")]
fn test_parse_overflow_x32() {
assert_eq!(parse_num("1T"), Err(ParseError::Overflow));
assert_eq!(parse_num("1000G"), Err(ParseError::Overflow));
}
#[test]
fn test_parse_bad_syntax() {
assert_eq!(parse_num("5MiB nonsense"), Err(ParseError::Syntax));
assert_eq!(parse_num("Nonsense string"), Err(ParseError::Syntax));
assert_eq!(parse_num("5mib"), Err(ParseError::Syntax));
assert_eq!(parse_num("biB"), Err(ParseError::Syntax));
assert_eq!(parse_num("-"), Err(ParseError::Syntax));
assert_eq!(parse_num(""), Err(ParseError::Syntax));
}
#[test]
fn test_parse_numbers() {
assert_eq!(parse_num("k"), Ok((1024, false)));
assert_eq!(parse_num("MiB"), Ok((1024 * 1024, false)));
assert_eq!(parse_num("-5"), Ok((5, true)));
assert_eq!(parse_num("b"), Ok((512, false)));
assert_eq!(parse_num("-2GiB"), Ok((2 * 1024 * 1024 * 1024, true)));
assert_eq!(parse_num("5M"), Ok((5 * 1024 * 1024, false)));
assert_eq!(parse_num("5MB"), Ok((5 * 1000 * 1000, false)));
}
#[test]
fn test_parse_numbers_obsolete() {
assert_eq!(obsolete("-5"), obsolete_result(&["-n", "5"]));
assert_eq!(obsolete("-100"), obsolete_result(&["-n", "100"]));
assert_eq!(obsolete("-5m"), obsolete_result(&["-c", "5242880"]));
assert_eq!(obsolete("-1k"), obsolete_result(&["-c", "1024"]));
assert_eq!(obsolete("-2b"), obsolete_result(&["-c", "1024"]));
assert_eq!(obsolete("-1mmk"), obsolete_result(&["-c", "1024"]));
assert_eq!(obsolete("-1vz"), obsolete_result(&["-v", "-z", "-n", "1"]));
assert_eq!(
obsolete("-1vzqvq"),
obsolete_result(&["-q", "-z", "-n", "1"])
);
assert_eq!(obsolete("-1vzc"), obsolete_result(&["-v", "-z", "-c", "1"]));
assert_eq!(
obsolete("-105kzm"),
obsolete_result(&["-z", "-c", "110100480"])
);
}
#[test]
fn test_parse_errors_obsolete() {
assert_eq!(obsolete("-5n"), Some(Err(ParseError::Syntax)));
assert_eq!(obsolete("-5c5"), Some(Err(ParseError::Syntax)));
}
#[test]
fn test_parse_obsolete_nomatch() {
assert_eq!(obsolete("-k"), None);
assert_eq!(obsolete("asd"), None);
}
#[test]
#[cfg(target_pointer_width = "64")]
fn test_parse_obsolete_overflow_x64() {
assert_eq!(
obsolete("-1000000000000000m"),
Some(Err(ParseError::Overflow))
);
assert_eq!(
obsolete("-10000000000000000000000"),
Some(Err(ParseError::Overflow))
);
}
#[test]
#[cfg(target_pointer_width = "32")]
fn test_parse_obsolete_overflow_x32() {
assert_eq!(obsolete("-42949672960"), Some(Err(ParseError::Overflow)));
assert_eq!(obsolete("-42949672k"), Some(Err(ParseError::Overflow)));
}
}
+60
View File
@@ -0,0 +1,60 @@
#[derive(Debug)]
pub enum Event<'a> {
Data(&'a [u8]),
Line,
}
/// Loops over the lines read from a BufRead.
/// # Arguments
/// * `input` the ReadBuf to read from
/// * `zero` whether to use 0u8 as a line delimiter
/// * `on_event` a closure receiving some bytes read in a slice, or
/// event signalling a line was just read.
/// this is guaranteed to be signalled *directly* after the
/// slice containing the (CR on win)LF / 0 is passed
///
/// Return whether to continue
pub fn walk_lines<F>(
input: &mut impl std::io::BufRead,
zero: bool,
mut on_event: F,
) -> std::io::Result<()>
where
F: FnMut(Event) -> std::io::Result<bool>,
{
let mut buffer = [0u8; super::BUF_SIZE];
loop {
let read = loop {
match input.read(&mut buffer) {
Ok(n) => break n,
Err(e) => match e.kind() {
std::io::ErrorKind::Interrupted => {}
_ => return Err(e),
},
}
};
if read == 0 {
return Ok(());
}
let mut base = 0usize;
for (i, byte) in buffer[..read].iter().enumerate() {
match byte {
b'\n' if !zero => {
on_event(Event::Data(&buffer[base..=i]))?;
base = i + 1;
if !on_event(Event::Line)? {
return Ok(());
}
}
0u8 if zero => {
on_event(Event::Data(&buffer[base..=i]))?;
base = i + 1;
if !on_event(Event::Line)? {
return Ok(());
}
}
_ => {}
}
}
on_event(Event::Data(&buffer[base..read]))?;
}
}
Regular → Executable
+63 -42
View File
@@ -86,88 +86,74 @@ fn test_verbose() {
.stdout_is_fixture("lorem_ipsum_verbose.expected");
}
#[test]
fn test_zero_terminated() {
new_ucmd!()
.args(&["-z", "zero_terminated.txt"])
.run()
.stdout_is_fixture("zero_terminated.expected");
}
#[test]
#[ignore]
fn test_spams_newline() {
//this test is does not mirror what GNU does
new_ucmd!().pipe_in("a").succeeds().stdout_is("a\n");
}
#[test]
#[ignore]
fn test_unsupported_byte_syntax() {
fn test_byte_syntax() {
new_ucmd!()
.args(&["-1c"])
.pipe_in("abc")
.fails()
//GNU head returns "a"
.stdout_is("")
.stderr_is("head: error: Unrecognized option: \'1\'");
.run()
.stdout_is("a");
}
#[test]
#[ignore]
fn test_unsupported_line_syntax() {
fn test_line_syntax() {
new_ucmd!()
.args(&["-n", "2048m"])
.pipe_in("a\n")
.fails()
//.stdout_is("a\n"); What GNU head returns.
.stdout_is("")
.stderr_is("head: error: invalid line count \'2048m\': invalid digit found in string");
.run()
.stdout_is("a\n");
}
#[test]
#[ignore]
fn test_unsupported_zero_terminated_syntax() {
fn test_zero_terminated_syntax() {
new_ucmd!()
.args(&["-z -n 1"])
.args(&["-z", "-n", "1"])
.pipe_in("x\0y")
.fails()
//GNU Head returns "x\0"
.stderr_is("head: error: Unrecognized option: \'z\'");
.run()
.stdout_is("x\0");
}
#[test]
#[ignore]
fn test_unsupported_zero_terminated_syntax_2() {
fn test_zero_terminated_syntax_2() {
new_ucmd!()
.args(&["-z -n 2"])
.args(&["-z", "-n", "2"])
.pipe_in("x\0y")
.fails()
//GNU Head returns "x\0y"
.stderr_is("head: error: Unrecognized option: \'z\'");
.run()
.stdout_is("x\0y");
}
#[test]
#[ignore]
fn test_unsupported_negative_byte_syntax() {
fn test_negative_byte_syntax() {
new_ucmd!()
.args(&["--bytes=-2"])
.pipe_in("a\n")
.fails()
//GNU Head returns ""
.stderr_is("head: error: invalid byte count \'-2\': invalid digit found in string");
.run()
.stdout_is("");
}
#[test]
#[ignore]
fn test_bug_in_negative_zero_lines() {
fn test_negative_zero_lines() {
new_ucmd!()
.args(&["--lines=-0"])
.pipe_in("a\nb\n")
.succeeds()
//GNU Head returns "a\nb\n"
.stdout_is("");
.stdout_is("a\nb\n");
}
#[test]
fn test_negative_zero_bytes() {
new_ucmd!()
.args(&["--bytes=-0"])
.pipe_in("qwerty")
.succeeds()
.stdout_is("qwerty");
}
#[test]
fn test_no_such_file_or_directory() {
let result = new_ucmd!().arg("no_such_file.toml").run();
@@ -179,3 +165,38 @@ fn test_no_such_file_or_directory() {
.contains("cannot open 'no_such_file.toml' for reading: No such file or directory")
)
}
// there was a bug not caught by previous tests
// where for negative n > 3, the total amount of lines
// was correct, but it would eat from the second line
#[test]
fn test_sequence_fixture() {
new_ucmd!()
.args(&["-n", "-10", "sequence"])
.run()
.stdout_is_fixture("sequence.expected");
}
#[test]
fn test_file_backwards() {
new_ucmd!()
.args(&["-c", "-10", "lorem_ipsum.txt"])
.run()
.stdout_is_fixture("lorem_ipsum_backwards_file.expected");
}
#[test]
fn test_zero_terminated() {
new_ucmd!()
.args(&["-z", "zero_terminated.txt"])
.run()
.stdout_is_fixture("zero_terminated.expected");
}
#[test]
fn test_obsolete_extras() {
new_ucmd!()
.args(&["-5zv"])
.pipe_in("1\02\03\04\05\06")
.succeeds()
.stdout_is("==> standard input <==\n1\02\03\04\05\0");
}
+24
View File
@@ -0,0 +1,24 @@
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Nunc interdum suscipit sem vel ornare.
Proin euismod,
justo sed mollis dictum,
eros urna ultricies augue,
eu pharetra mi ex id ante.
Duis convallis porttitor aliquam.
Nunc vitae tincidunt ex.
Suspendisse iaculis ligula ac diam consectetur lacinia.
Donec vel velit dui.
Etiam fringilla,
dolor quis tempor vehicula,
lacus turpis bibendum velit,
et pellentesque elit odio a magna.
Cras vulputate tortor non libero vehicula euismod.
Aliquam tincidunt nisl eget enim cursus,
viverra sagittis magna commodo.
Cras rhoncus egestas leo nec blandit.
Suspendisse potenti.
Etiam ullamcorper leo vel lacus vestibulum,
cursus semper eros efficitur.
In hac habitasse platea dictumst.
Phasellus scelerisque vehicula f
+100
View File
@@ -0,0 +1,100 @@
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
+90
View File
@@ -0,0 +1,90 @@
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90