mod error;
use clap::{Arg, ArgAction, Command};
use memchr::memmem;
use memmap2::Mmap;
use std::ffi::{OsStr, OsString};
use std::io::{BufWriter, Read, Write, stdin, stdout};
use std::{fs::File, io::copy, path::Path};
#[cfg(unix)]
use uucore::error::UError;
use uucore::error::UResult;
#[cfg(unix)]
use uucore::error::set_exit_code;
use uucore::{format_usage, show};
use crate::error::TacError;
use uucore::translate;
mod options {
pub static BEFORE: &str = "before";
pub static REGEX: &str = "regex";
pub static SEPARATOR: &str = "separator";
pub static FILE: &str = "file";
}
#[uucore::main]
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?;
let before = matches.get_flag(options::BEFORE);
let regex = matches.get_flag(options::REGEX);
let raw_separator = matches
.get_one::<OsString>(options::SEPARATOR)
.map_or(OsStr::new("\n"), |s| s.as_os_str());
let separator = if raw_separator.is_empty() {
OsStr::new("\0")
} else {
raw_separator
};
let files: Vec<OsString> = match matches.get_many::<OsString>(options::FILE) {
Some(v) => v.cloned().collect(),
None => vec![OsString::from("-")],
};
tac(&files, before, regex, separator)
}
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
.version(uucore::crate_version!())
.help_template(uucore::localized_help_template(uucore::util_name()))
.override_usage(format_usage(&translate!("tac-usage")))
.about(translate!("tac-about"))
.infer_long_args(true)
.arg(
Arg::new(options::BEFORE)
.short('b')
.long(options::BEFORE)
.help(translate!("tac-help-before"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::REGEX)
.short('r')
.long(options::REGEX)
.help(translate!("tac-help-regex"))
.action(ArgAction::SetTrue),
)
.arg(
Arg::new(options::SEPARATOR)
.short('s')
.long(options::SEPARATOR)
.help(translate!("tac-help-separator"))
.value_parser(clap::value_parser!(OsString))
.value_name("STRING"),
)
.arg(
Arg::new(options::FILE)
.hide(true)
.action(ArgAction::Append)
.value_parser(clap::value_parser!(OsString))
.value_hint(clap::ValueHint::FilePath),
)
}
fn buffer_tac_regex(
data: &[u8],
pattern: ®ex::bytes::Regex,
before: bool,
) -> std::io::Result<()> {
let out = stdout();
let mut out = BufWriter::new(out.lock());
let mut this_line_end = data.len();
let mut following_line_start = data.len();
for i in (0..data.len()).rev() {
if let Some(match_) = pattern.find_at(&data[..this_line_end], i)
&& match_.start() == i
{
this_line_end = i;
let slen = match_.end() - match_.start();
if before {
out.write_all(&data[i..following_line_start])?;
following_line_start = i;
} else {
out.write_all(&data[i + slen..following_line_start])?;
following_line_start = i + slen;
}
}
}
out.write_all(&data[0..following_line_start])?;
out.flush()?;
Ok(())
}
fn buffer_tac(data: &[u8], before: bool, separator: &OsStr) -> std::io::Result<()> {
let out = stdout();
let mut out = BufWriter::new(out.lock());
let slen = separator.len();
let mut following_line_start = data.len();
for i in memmem::rfind_iter(data, separator.as_encoded_bytes()) {
if before {
out.write_all(&data[i..following_line_start])?;
following_line_start = i;
} else {
out.write_all(&data[i + slen..following_line_start])?;
following_line_start = i + slen;
}
}
out.write_all(&data[0..following_line_start])?;
out.flush()?;
Ok(())
}
fn translate_regex_flavor(bytes: &[u8]) -> String {
let mut result = Vec::new();
let mut i = 0;
let mut inside_brackets = false;
let mut prev_was_backslash = false;
let mut last_byte: Option<u8> = None;
while let Some(b) = bytes.get(i) {
let is_escaped = prev_was_backslash;
prev_was_backslash = false;
match b {
_ if inside_brackets && !b.is_ascii() => {
i += 1;
continue;
}
b'\\' if !inside_brackets && !is_escaped => {
if let Some(next) = bytes.get(i + 1) {
if matches!(next, b'(' | b')' | b'|' | b'{' | b'}') {
result.push(*next);
last_byte = Some(*next);
i += 2;
continue;
}
}
result.push(b'\\');
last_byte = Some(b'\\');
prev_was_backslash = true;
}
b'[' => {
inside_brackets = true;
result.push(*b);
last_byte = Some(*b);
}
b']' => {
inside_brackets = false;
result.push(*b);
last_byte = Some(*b);
}
b'(' | b')' | b'|' | b'{' | b'}' if !inside_brackets && !is_escaped => {
result.push(b'\\');
result.push(*b);
last_byte = Some(*b);
}
b'^' if !inside_brackets && !is_escaped => {
let is_anchor_position =
result.is_empty() || matches!(last_byte, Some(b'(' | b'|'));
if !is_anchor_position {
result.push(b'\\');
}
result.push(*b);
last_byte = Some(*b);
}
b'$' if !inside_brackets && !is_escaped => {
let next_is_anchor_position = match bytes.get(i + 1) {
None => true,
Some(b')' | b'|') => true,
Some(b'\\') => {
matches!(bytes.get(i + 2), Some(b')' | b'|'))
}
_ => false,
};
if !next_is_anchor_position {
result.push(b'\\');
}
result.push(*b);
last_byte = Some(*b);
}
_ if !b.is_ascii() => {
let _ = write!(result, r"(?-u:\x{b:02x})");
last_byte = None;
}
_ => {
result.push(*b);
last_byte = Some(*b);
}
}
i += 1;
}
String::from_utf8(result).expect("produces ASCII bytes")
}
#[allow(clippy::cognitive_complexity)]
fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &OsStr) -> UResult<()> {
let maybe_pattern = if regex {
match regex::bytes::RegexBuilder::new(&translate_regex_flavor(separator.as_encoded_bytes()))
.multi_line(true)
.build()
{
Ok(p) => Some(p),
Err(e) => return Err(TacError::InvalidRegex(e).into()),
}
} else {
None
};
for filename in filenames {
let mmap;
let buf;
let data: &[u8] = if filename == "-" {
#[cfg(unix)]
if uucore::signals::stdin_was_closed() {
let e: Box<dyn UError> = TacError::ReadError(
OsString::from("-"),
std::io::Error::from_raw_os_error(libc::EBADF),
)
.into();
show!(e);
set_exit_code(1);
continue;
}
if let Some(mmap1) = try_mmap_stdin() {
mmap = mmap1;
&mmap
} else {
match buffer_stdin() {
Ok(StdinData::Mmap(mmap1)) => {
mmap = mmap1;
&mmap
}
Ok(StdinData::Vec(buf1)) => {
buf = buf1;
&buf
}
Err(e) => {
show!(TacError::ReadError(OsString::from("stdin"), e));
continue;
}
}
}
} else {
let path = Path::new(filename);
let mut file = match File::open(path) {
Ok(f) => f,
Err(e) => {
show!(TacError::OpenError(filename.clone(), e));
continue;
}
};
if let Some(mmap1) = try_mmap_file(&file) {
mmap = mmap1;
&mmap
} else {
let mut contents = Vec::new();
match file.read_to_end(&mut contents) {
Ok(_) => {
buf = contents;
&buf
}
Err(e) => {
show!(TacError::ReadError(filename.clone(), e));
continue;
}
}
}
};
let result = match maybe_pattern {
Some(ref pattern) => buffer_tac_regex(data, pattern, before),
None => buffer_tac(data, before, separator),
};
if let Err(e) = result {
return Err(TacError::WriteError(e).into());
}
}
Ok(())
}
fn try_mmap_stdin() -> Option<Mmap> {
let mmap = unsafe { Mmap::map(&stdin()).ok()? };
if mmap.is_empty() { None } else { Some(mmap) }
}
enum StdinData {
Mmap(Mmap),
Vec(Vec<u8>),
}
fn buffer_stdin() -> std::io::Result<StdinData> {
if let Ok(mut tmp) = tempfile::tempfile() {
copy(&mut stdin(), &mut tmp)?;
let mmap = unsafe { Mmap::map(&tmp)? };
Ok(StdinData::Mmap(mmap))
} else {
let mut buf = Vec::new();
stdin().read_to_end(&mut buf)?;
Ok(StdinData::Vec(buf))
}
}
fn try_mmap_file(file: &File) -> Option<Mmap> {
unsafe { Mmap::map(file).ok() }
}
#[cfg(test)]
mod tests_hybrid_flavor {
use super::translate_regex_flavor;
#[test]
fn test_grouping_and_alternation() {
assert_eq!(translate_regex_flavor(br"\(abc\)"), r"(abc)");
assert_eq!(translate_regex_flavor(br"(abc)"), r"\(abc\)");
assert_eq!(translate_regex_flavor(br"a\|b"), r"a|b");
assert_eq!(translate_regex_flavor(br"a|b"), r"a\|b");
}
#[test]
fn test_quantifiers() {
assert_eq!(translate_regex_flavor(b"a+"), "a+");
assert_eq!(translate_regex_flavor(b"a*"), "a*");
assert_eq!(translate_regex_flavor(b"a?"), "a?");
assert_eq!(translate_regex_flavor(br"a\+"), r"a\+");
assert_eq!(translate_regex_flavor(br"a\*"), r"a\*");
assert_eq!(translate_regex_flavor(br"a\?"), r"a\?");
}
#[test]
fn test_intervals() {
assert_eq!(translate_regex_flavor(br"a\{1,3\}"), r"a{1,3}");
assert_eq!(translate_regex_flavor(br"a{1,3}"), r"a\{1,3\}");
}
#[test]
fn test_anchors_context() {
assert_eq!(translate_regex_flavor(br"^abc$"), r"^abc$");
assert_eq!(translate_regex_flavor(br"a^b"), r"a\^b");
assert_eq!(translate_regex_flavor(br"a$b"), r"a\$b");
assert_eq!(translate_regex_flavor(br"\(^abc\)"), r"(^abc)");
assert_eq!(translate_regex_flavor(br"z\(^abc\)"), r"z(^abc)");
assert_eq!(translate_regex_flavor(br"\(abc$\)"), r"(abc$)");
assert_eq!(translate_regex_flavor(br"\(abc$\)z"), r"(abc$)z");
assert_eq!(translate_regex_flavor(br"^a\|^b"), r"^a|^b");
assert_eq!(translate_regex_flavor(br"x\|^b"), r"x|^b");
assert_eq!(translate_regex_flavor(br"a$\|b$"), r"a$|b$");
}
#[test]
fn test_character_classes() {
assert_eq!(translate_regex_flavor(br"[a-z]"), r"[a-z]");
assert_eq!(translate_regex_flavor(br"[.]"), r"[.]");
assert_eq!(translate_regex_flavor(br"[+]"), r"[+]");
assert_eq!(translate_regex_flavor(br"[]abc]"), r"[]abc]");
assert_eq!(translate_regex_flavor(br"[^]abc]"), r"[^]abc]");
}
#[test]
fn test_complex_strings() {
assert_eq!(translate_regex_flavor(br"(\d+)[+*]"), r"\(\d+\)[+*]");
assert_eq!(translate_regex_flavor(br"\(\d+\)\{2\}"), r"(\d+){2}");
}
#[test]
fn test_edge_cases() {
assert_eq!(translate_regex_flavor(br"abc\"), r"abc\");
assert_eq!(translate_regex_flavor(br"\\"), r"\\");
assert_eq!(translate_regex_flavor(br"\^"), r"\^");
}
}