mod error;
use clap::{Arg, ArgAction, Command};
use memchr::memmem;
use memmap2::Mmap;
use std::ffi::OsString;
use std::io::{BufWriter, Read, Write, stdin, stdout};
use std::{
fs::{File, read},
io::copy,
path::Path,
};
#[cfg(unix)]
use uucore::error::set_exit_code;
use uucore::error::{UError, UResult};
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::<String>(options::SEPARATOR)
.map_or("\n", |s| s.as_str());
let separator = if raw_separator.is_empty() {
"\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_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) {
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: &str) -> 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) {
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(regex: &str) -> String {
let mut result = String::new();
let mut chars = regex.chars().peekable();
let mut inside_brackets = false;
let mut prev_was_backslash = false;
let mut last_char: Option<char> = None;
while let Some(c) = chars.next() {
let is_escaped = prev_was_backslash;
prev_was_backslash = false;
match c {
'\\' if !inside_brackets && !is_escaped => {
if let Some(&next) = chars.peek() {
if matches!(next, '(' | ')' | '|' | '{' | '}') {
result.push(next);
last_char = Some(next);
chars.next();
continue;
}
}
result.push('\\');
last_char = Some('\\');
prev_was_backslash = true;
}
'[' => {
inside_brackets = true;
result.push(c);
last_char = Some(c);
}
']' => {
inside_brackets = false;
result.push(c);
last_char = Some(c);
}
'(' | ')' | '|' | '{' | '}' if !inside_brackets && !is_escaped => {
result.push('\\');
result.push(c);
last_char = Some(c);
}
'^' if !inside_brackets && !is_escaped => {
let is_anchor_position = result.is_empty() || matches!(last_char, Some('(' | '|'));
if !is_anchor_position {
result.push('\\');
}
result.push(c);
last_char = Some(c);
}
'$' if !inside_brackets && !is_escaped => {
let next_is_anchor_position = match chars.peek() {
None => true,
Some(&')' | &'|') => true,
Some(&'\\') => {
let chars_vec: Vec<char> = chars.clone().take(2).collect();
matches!(chars_vec.get(1), Some(&')' | &'|'))
}
_ => false,
};
if !next_is_anchor_position {
result.push('\\');
}
result.push(c);
last_char = Some(c);
}
_ => {
result.push(c);
last_char = Some(c);
}
}
}
result
}
#[allow(clippy::cognitive_complexity)]
fn tac(filenames: &[OsString], before: bool, regex: bool, separator: &str) -> UResult<()> {
let maybe_pattern = if regex {
match regex::bytes::RegexBuilder::new(&translate_regex_flavor(separator))
.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);
if path.is_dir() {
let e: Box<dyn UError> =
TacError::InvalidDirectoryArgument(filename.clone()).into();
show!(e);
continue;
}
if path.metadata().is_err() {
let e: Box<dyn UError> = TacError::FileNotFound(filename.clone()).into();
show!(e);
continue;
}
if let Some(mmap1) = try_mmap_path(path) {
mmap = mmap1;
&mmap
} else {
match read(path) {
Ok(buf1) => {
buf = buf1;
&buf
}
Err(e) => {
let e: Box<dyn UError> = TacError::ReadError(filename.clone(), e).into();
show!(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> {
unsafe { Mmap::map(&stdin()).ok() }
}
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_path(path: &Path) -> Option<Mmap> {
let file = File::open(path).ok()?;
let mmap = unsafe { Mmap::map(&file).ok()? };
Some(mmap)
}
#[cfg(test)]
mod tests_hybrid_flavor {
use super::translate_regex_flavor;
#[test]
fn test_grouping_and_alternation() {
assert_eq!(translate_regex_flavor(r"\(abc\)"), r"(abc)");
assert_eq!(translate_regex_flavor(r"(abc)"), r"\(abc\)");
assert_eq!(translate_regex_flavor(r"a\|b"), r"a|b");
assert_eq!(translate_regex_flavor(r"a|b"), r"a\|b");
}
#[test]
fn test_quantifiers() {
assert_eq!(translate_regex_flavor("a+"), "a+");
assert_eq!(translate_regex_flavor("a*"), "a*");
assert_eq!(translate_regex_flavor("a?"), "a?");
assert_eq!(translate_regex_flavor(r"a\+"), r"a\+");
assert_eq!(translate_regex_flavor(r"a\*"), r"a\*");
assert_eq!(translate_regex_flavor(r"a\?"), r"a\?");
}
#[test]
fn test_intervals() {
assert_eq!(translate_regex_flavor(r"a\{1,3\}"), r"a{1,3}");
assert_eq!(translate_regex_flavor(r"a{1,3}"), r"a\{1,3\}");
}
#[test]
fn test_anchors_context() {
assert_eq!(translate_regex_flavor(r"^abc$"), r"^abc$");
assert_eq!(translate_regex_flavor(r"a^b"), r"a\^b");
assert_eq!(translate_regex_flavor(r"a$b"), r"a\$b");
assert_eq!(translate_regex_flavor(r"\(^abc\)"), r"(^abc)");
assert_eq!(translate_regex_flavor(r"z\(^abc\)"), r"z(^abc)");
assert_eq!(translate_regex_flavor(r"\(abc$\)"), r"(abc$)");
assert_eq!(translate_regex_flavor(r"\(abc$\)z"), r"(abc$)z");
assert_eq!(translate_regex_flavor(r"^a\|^b"), r"^a|^b");
assert_eq!(translate_regex_flavor(r"x\|^b"), r"x|^b");
assert_eq!(translate_regex_flavor(r"a$\|b$"), r"a$|b$");
}
#[test]
fn test_character_classes() {
assert_eq!(translate_regex_flavor(r"[a-z]"), r"[a-z]");
assert_eq!(translate_regex_flavor(r"[.]"), r"[.]");
assert_eq!(translate_regex_flavor(r"[+]"), r"[+]");
assert_eq!(translate_regex_flavor(r"[]abc]"), r"[]abc]");
assert_eq!(translate_regex_flavor(r"[^]abc]"), r"[^]abc]");
}
#[test]
fn test_complex_strings() {
assert_eq!(translate_regex_flavor(r"(\d+)[+*]"), r"\(\d+\)[+*]");
assert_eq!(translate_regex_flavor(r"\(\d+\)\{2\}"), r"(\d+){2}");
}
#[test]
fn test_edge_cases() {
assert_eq!(translate_regex_flavor(r"abc\"), r"abc\");
assert_eq!(translate_regex_flavor(r"\\"), r"\\");
assert_eq!(translate_regex_flavor(r"\^"), r"\^");
}
}