use std::cmp::Ordering;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::path::Path;
use std::sync::LazyLock;
use std::{fmt, net, str};
use regex::bytes::Regex;
#[cfg(test)]
mod tests;
static RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(" \\[[0-9]{1,2}/").expect("BUG: Invalid Regex")
});
#[allow(dead_code)]
trait Replace {
fn replace(&self, old: &[u8], new: &[u8]) -> Vec<u8>;
fn kmpsearch(&self, pattern: &[u8]) -> Option<Vec<usize>>;
fn bmsearch(&self, pattern: &[u8]) -> Option<Vec<usize>>;
fn research(&self, pattern: &[u8]) -> Option<Vec<usize>>;
fn windowsearch(&self, pattern: &[u8]) -> Option<Vec<usize>>;
fn prefix_table(pattern: &[u8]) -> Vec<isize>;
fn bad_char_table(pattern: &[u8]) -> [usize; 256];
}
impl Replace for [u8] {
fn replace(&self, old: &[u8], new: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(self.len());
let mut i = 0;
if let Some(matches) = self.windowsearch(old) {
for m in matches {
result.extend_from_slice(&self[i..m]);
result.extend_from_slice(new);
i = m + old.len();
}
result.extend_from_slice(&self[i..]);
} else {
return self.to_vec();
}
result
}
#[allow(clippy::cast_sign_loss)]
fn kmpsearch(&self, pattern: &[u8]) -> Option<Vec<usize>> {
let m = self.len();
let n = pattern.len();
let mut i = 0;
let mut j = 0;
if pattern.is_empty() || n > m {
return None;
}
let table = Self::prefix_table(pattern);
let mut indices = Vec::new();
while i < m {
while j >= 0 && self[i] != pattern[j as usize] {
j = table[j as usize];
}
i += 1;
j += 1;
if (j as usize) == n {
indices.push(i - n);
j = table[j as usize];
i += n;
}
}
if indices.is_empty() {
None
} else {
Some(indices)
}
}
fn bmsearch(&self, pattern: &[u8]) -> Option<Vec<usize>> {
let m = self.len();
let n = pattern.len();
if pattern.is_empty() || n > m {
return None;
}
let table = Self::bad_char_table(pattern);
let mut indices = Vec::new();
let mut i = 0;
while i <= m - n {
let mut j = n - 1;
while pattern[j] == self[i + j] {
if j == 0 {
indices.push(i);
i += n;
break;
}
j -= 1;
}
if j != 0 {
i += table[self[i + j] as usize];
}
}
if indices.is_empty() {
None
} else {
Some(indices)
}
}
fn research(&self, pattern: &[u8]) -> Option<Vec<usize>> {
let re = Regex::new(str::from_utf8(pattern).unwrap()).unwrap();
let indices: Vec<_> = re.find_iter(self).map(|m| m.start()).collect();
if indices.is_empty() {
None
} else {
Some(indices)
}
}
fn windowsearch(&self, pattern: &[u8]) -> Option<Vec<usize>> {
let m = self.len();
let n = pattern.len();
if pattern.is_empty() || n > m {
return None;
}
let mut indices = Vec::new();
let mut i = 0;
while i <= self.len() - n {
if &self[i..i + n] == pattern {
indices.push(i);
i += n;
} else {
i += 1;
}
}
if indices.is_empty() {
None
} else {
Some(indices)
}
}
#[allow(clippy::cast_sign_loss)]
fn prefix_table(pattern: &[u8]) -> Vec<isize> {
let mut i = 0;
let mut j: isize = -1;
let mut table = vec![0; pattern.len() + 1];
table[i] = j;
while i < pattern.len() {
while j >= 0 && pattern[j as usize] != pattern[i] {
j = table[j as usize];
}
i += 1;
j += 1;
table[i] = j;
}
table
}
fn bad_char_table(pattern: &[u8]) -> [usize; 256] {
let n = pattern.len();
let mut table = [n; 256];
for (i, &byte) in pattern.iter().enumerate().take(n - 1) {
table[byte as usize] = n - i - 1;
}
table
}
}
#[derive(Debug)]
pub struct IOError {
message: String,
}
impl fmt::Display for IOError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl From<io::Error> for IOError {
fn from(error: io::Error) -> Self {
IOError {
message: error.to_string(),
}
}
}
#[derive(Debug)]
pub struct IOConfig<'a> {
input: Option<Vec<&'a Path>>,
output: Option<&'a Path>,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
pub struct Config<'a> {
pub ipv4: &'a str,
pub ipv6: &'a str,
pub host: &'a str,
pub skip: bool,
pub authuser: bool,
pub trim: bool,
pub thorough: bool,
pub optimize: bool,
pub flush: bool,
}
impl Default for IOConfig<'_> {
fn default() -> Self {
IOConfig {
input: None,
output: None,
}
}
}
impl Default for Config<'_> {
fn default() -> Self {
Config {
ipv4: "127.0.0.1",
ipv6: "::1",
host: "localhost",
skip: false,
authuser: false,
trim: true,
thorough: false,
optimize: true,
flush: false,
}
}
}
impl<'a> Config<'a> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get_ipv4_value(&self) -> &'a str {
self.ipv4
}
#[must_use]
pub fn get_ipv6_value(&self) -> &'a str {
self.ipv6
}
#[must_use]
pub fn get_host_value(&self) -> &'a str {
self.host
}
#[must_use]
pub fn get_skip(&self) -> bool {
self.skip
}
#[must_use]
pub fn get_authuser(&self) -> bool {
self.authuser
}
#[must_use]
pub fn get_trim(&self) -> bool {
self.trim
}
#[must_use]
pub fn get_thorough(&self) -> bool {
self.thorough
}
#[must_use]
pub fn get_optimize(&self) -> bool {
self.optimize
}
#[must_use]
pub fn get_flush(&self) -> bool {
self.flush
}
pub fn set_ipv4_value(&mut self, ipv4: &'a str) {
self.ipv4 = ipv4;
}
pub fn set_ipv6_value(&mut self, ipv6: &'a str) {
self.ipv6 = ipv6;
}
pub fn set_host_value(&mut self, host: &'a str) {
self.host = host;
}
pub fn set_flush(&mut self, b: bool) {
self.flush = b;
}
pub fn set_authuser(&mut self, b: bool) {
self.authuser = b;
}
pub fn set_trim(&mut self, b: bool) {
self.trim = b;
}
pub fn set_thorough(&mut self, b: bool) {
self.thorough = b;
}
pub fn set_optimize(&mut self, b: bool) {
self.optimize = b;
}
pub fn set_skip(&mut self, b: bool) {
self.skip = b;
}
}
impl<'a> IOConfig<'a> {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get_input(&self) -> Option<&Vec<&'a Path>> {
self.input.as_ref()
}
#[must_use]
pub fn get_output(&self) -> Option<&'a Path> {
self.output
}
pub fn push_input<P: AsRef<Path> + ?Sized>(&mut self, i: &'a P) {
if let Some(input) = &mut self.input {
input.push(i.as_ref());
} else {
self.input = Some(vec![]);
self.push_input(i);
}
}
pub fn set_output(&mut self, o: &'a Path) {
self.output = Some(o);
}
}
fn replace_remote_address<R: BufRead, W: Write>(
config: &Config,
mut reader: R,
mut writer: W,
) -> Result<(), io::Error> {
let mut buf = vec![];
let mut repl;
'lines: loop {
buf.clear();
let bytes_read = reader.read_until(b'\n', &mut buf)?;
if bytes_read == 0 {
break;
}
if config.get_trim() {
let s = buf
.iter()
.position(|&x| !x.is_ascii_whitespace())
.unwrap_or(0);
buf.drain(..s);
}
for (i, byte) in buf.iter().enumerate() {
if byte.is_ascii_whitespace() {
let needle = &String::from_utf8_lossy(&buf[..i]);
repl = match needle {
s if s.parse::<net::Ipv4Addr>().is_ok() => config.get_ipv4_value(),
s if s.parse::<net::Ipv6Addr>().is_ok() => config.get_ipv6_value(),
s if s.is_empty() && config.get_skip() => continue 'lines,
_ => config.get_host_value(),
};
write!(&mut writer, "{repl}")?;
let is_authuser = config.get_authuser();
let is_thorough = config.get_thorough();
let is_optimized = config.get_optimize() && buf.len() >= i + 6;
if is_authuser {
if is_optimized && buf[i + 3..i + 6].iter().cmp(b"- [") == Ordering::Equal {
write_or_replace(&buf[i..], needle, repl, is_thorough, &mut writer)?;
} else if let Some(time_field) = RE.find_at(&buf, i) {
write!(&mut writer, " - -")?;
write_or_replace(
&buf[time_field.start()..],
needle,
repl,
is_thorough,
&mut writer,
)?;
} else {
write_or_replace(&buf[i..], needle, repl, is_thorough, &mut writer)?;
}
} else if is_thorough {
write_or_replace(&buf[i..], needle, repl, true, &mut writer)?;
} else {
writer.write_all(&buf[i..])?;
}
if config.get_flush() {
writer.flush()?;
}
continue 'lines;
}
}
}
writer.flush()?;
Ok(())
}
fn write_or_replace<W: Write>(
slice: &[u8],
needle: &str,
repl: &str,
should_replace: bool,
writer: &mut W,
) -> Result<(), io::Error> {
if should_replace && !needle.is_empty() {
writer.write_all(&slice.replace(needle.as_bytes(), repl.as_bytes()))?;
} else {
writer.write_all(slice)?;
}
Ok(())
}
pub fn run(config: &Config, ioconfig: &IOConfig) -> Result<(), IOError> {
let stdout = io::stdout();
let mut writer: Box<dyn Write> = match ioconfig.get_output() {
Some(output) => {
let f = match OpenOptions::new()
.create(true)
.append(true)
.open(Path::new(output))
{
Ok(f) => f,
Err(e) => {
return Err(IOError {
message: format!("Can not open output '{}': {e}", output.display()),
})
}
};
Box::new(BufWriter::new(f)) as _
}
None => Box::new(BufWriter::new(stdout.lock())),
};
if let Some(input) = ioconfig.get_input() {
for arg in input {
match File::open(Path::new(arg)) {
Err(e) => {
return Err(IOError {
message: format!("Can not open input '{}': {e}", arg.display()),
})
}
Ok(f) => {
let reader: Box<dyn BufRead> = Box::new(BufReader::new(f));
if let Err(e) = replace_remote_address(config, reader, &mut writer) {
return Err(IOError {
message: e.to_string(),
});
}
}
}
}
} else {
let stdin = io::stdin();
let reader: Box<dyn BufRead> = Box::new(stdin.lock());
if let Err(e) = replace_remote_address(config, reader, &mut writer) {
return Err(IOError {
message: e.to_string(),
});
}
}
Ok(())
}
pub fn run_raw<R: BufRead, W: Write>(
config: &Config,
reader: R,
mut writer: W,
) -> Result<(), IOError> {
replace_remote_address(config, reader, &mut writer)?;
Ok(())
}