1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 11:37:44 +00:00

tac: support multi-char separator with overlap

Fix a bug in `tac` where multi-character line separators would cause
incorrect behavior when there was overlap between candidate matches in
the input string. This commit adds a dependency on `memchr` in order to
use the `memchr::memmem::rfind_iter()` function to scan for
non-overlapping instances of the specified line separator characters,
scanning from right to left.

Fixes #2580.
This commit is contained in:
Jeffrey Finkelstein 2021-08-22 16:21:08 -04:00
parent c77115ab51
commit 0e689e78aa
4 changed files with 107 additions and 22 deletions

1
Cargo.lock generated
View file

@ -2937,6 +2937,7 @@ name = "uu_tac"
version = "0.0.7" version = "0.0.7"
dependencies = [ dependencies = [
"clap", "clap",
"memchr 2.4.0",
"uucore", "uucore",
"uucore_procs", "uucore_procs",
] ]

View file

@ -15,6 +15,7 @@ edition = "2018"
path = "src/tac.rs" path = "src/tac.rs"
[dependencies] [dependencies]
memchr = "2"
clap = { version = "2.33", features = ["wrap_help"] } clap = { version = "2.33", features = ["wrap_help"] }
uucore = { version=">=0.0.9", package="uucore", path="../../uucore" } uucore = { version=">=0.0.9", package="uucore", path="../../uucore" }
uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" } uucore_procs = { version=">=0.0.6", package="uucore_procs", path="../../uucore_procs" }

View file

@ -5,12 +5,13 @@
// * For the full copyright and license information, please view the LICENSE // * For the full copyright and license information, please view the LICENSE
// * file that was distributed with this source code. // * file that was distributed with this source code.
// spell-checker:ignore (ToDO) sbytes slen dlen // spell-checker:ignore (ToDO) sbytes slen dlen memmem
#[macro_use] #[macro_use]
extern crate uucore; extern crate uucore;
use clap::{crate_version, App, Arg}; use clap::{crate_version, App, Arg};
use memchr::memmem;
use std::io::{stdin, stdout, BufReader, Read, Write}; use std::io::{stdin, stdout, BufReader, Read, Write};
use std::{fs::File, path::Path}; use std::{fs::File, path::Path};
use uucore::InvalidEncodingHandling; use uucore::InvalidEncodingHandling;
@ -80,20 +81,33 @@ pub fn uu_app() -> App<'static, 'static> {
.arg(Arg::with_name(options::FILE).hidden(true).multiple(true)) .arg(Arg::with_name(options::FILE).hidden(true).multiple(true))
} }
/// Write lines from `data` to stdout in reverse.
///
/// This function writes to [`stdout`] each line appearing in `data`,
/// starting with the last line and ending with the first line. The
/// `separator` parameter defines what characters to use as a line
/// separator.
///
/// If `before` is `false`, then this function assumes that the
/// `separator` appears at the end of each line, as in `"abc\ndef\n"`.
/// If `before` is `true`, then this function assumes that the
/// `separator` appears at the beginning of each line, as in
/// `"/abc/def"`.
fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> { fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()> {
let mut out = stdout(); let mut out = stdout();
// Convert the line separator to a byte sequence. // The number of bytes in the line separator.
let sbytes = separator.as_bytes(); let slen = separator.as_bytes().len();
let slen = sbytes.len();
// If there are more characters in the separator than in the data, // The index of the start of the next line in the `data`.
// we can't possibly split the data on the separator. Write the //
// entire buffer to stdout. // As we scan through the `data` from right to left, we update this
let dlen = data.len(); // variable each time we find a new line.
if dlen < slen { //
return out.write_all(data); // If `before` is `true`, then each line starts immediately before
} // the line separator. Otherwise, each line starts immediately after
// the line separator.
let mut following_line_start = data.len();
// Iterate over each byte in the buffer in reverse. When we find a // Iterate over each byte in the buffer in reverse. When we find a
// line separator, write the line to stdout. // line separator, write the line to stdout.
@ -101,9 +115,7 @@ fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()>
// The `before` flag controls whether the line separator appears at // The `before` flag controls whether the line separator appears at
// the end of the line (as in "abc\ndef\n") or at the beginning of // the end of the line (as in "abc\ndef\n") or at the beginning of
// the line (as in "/abc/def"). // the line (as in "/abc/def").
let mut following_line_start = data.len(); for i in memmem::rfind_iter(data, separator) {
for i in (0..dlen - slen + 1).rev() {
if &data[i..i + slen] == sbytes {
if before { if before {
out.write_all(&data[i..following_line_start])?; out.write_all(&data[i..following_line_start])?;
following_line_start = i; following_line_start = i;
@ -112,7 +124,6 @@ fn buffer_tac(data: &[u8], before: bool, separator: &str) -> std::io::Result<()>
following_line_start = i + slen; following_line_start = i + slen;
} }
} }
}
// After the loop terminates, write whatever bytes are remaining at // After the loop terminates, write whatever bytes are remaining at
// the beginning of the buffer. // the beginning of the buffer.

View file

@ -1,4 +1,4 @@
// spell-checker:ignore axxbxx bxxaxx // spell-checker:ignore axxbxx bxxaxx axxx axxxx xxaxx xxax xxxxa
use crate::common::util::*; use crate::common::util::*;
#[test] #[test]
@ -125,6 +125,78 @@ fn test_multi_char_separator() {
.stdout_is("bxxaxx"); .stdout_is("bxxaxx");
} }
#[test]
fn test_multi_char_separator_overlap() {
// The right-most pair of "x" characters in the input is treated as
// the only line separator. That is, "axxx" is interpreted as having
// one line comprising the string "ax" followed by the line
// separator "xx".
new_ucmd!()
.args(&["-s", "xx"])
.pipe_in("axxx")
.succeeds()
.stdout_is("axxx");
// Each non-overlapping pair of "x" characters in the input is
// treated as a line separator. That is, "axxxx" is interpreted as
// having two lines:
//
// * the second line is the empty string "" followed by the line
// separator "xx",
// * the first line is the string "a" followed by the line separator
// "xx".
//
// The lines are printed in reverse, resulting in "xx" followed by
// "axx".
new_ucmd!()
.args(&["-s", "xx"])
.pipe_in("axxxx")
.succeeds()
.stdout_is("xxaxx");
}
#[test]
fn test_multi_char_separator_overlap_before() {
// With the "-b" option, the line separator is assumed to be at the
// beginning of the line. In this case, That is, "axxx" is
// interpreted as having two lines:
//
// * the second line is the empty string "" preceded by the line
// separator "xx",
// * the first line is the string "ax" preceded by no line
// separator, since there are no more characters preceding it.
//
// The lines are printed in reverse, resulting in "xx" followed by
// "ax".
new_ucmd!()
.args(&["-b", "-s", "xx"])
.pipe_in("axxx")
.succeeds()
.stdout_is("xxax");
// With the "-b" option, the line separator is assumed to be at the
// beginning of the line. Each non-overlapping pair of "x"
// characters in the input is treated as a line separator. That is,
// "axxxx" is interpreted as having three lines:
//
// * the third line is the empty string "" preceded by the line
// separator "xx" (the last two "x" characters in the input
// string),
// * the second line is the empty string "" preceded by the line
// separator "xx" (the first two "x" characters in the input
// string),
// * the first line is the string "a" preceded by no line separator,
// since there are no more characters preceding it.
//
// The lines are printed in reverse, resulting in "xx" followed by
// "xx" followed by "a".
new_ucmd!()
.args(&["-b", "-s", "xx"])
.pipe_in("axxxx")
.succeeds()
.stdout_is("xxxxa");
}
#[test] #[test]
fn test_null_separator() { fn test_null_separator() {
new_ucmd!() new_ucmd!()