use crate::relative::RelativeRewriter;
use crate::time::clock::Clock;
use crate::time::format;
use crate::time::tz::TimezoneSource;
use chrono::{DateTime, Utc};
use std::io::{BufRead, ErrorKind, Write};
#[derive(Debug, Clone)]
pub enum PrefixSource {
Absolute,
SincePreviousLine,
SinceProgramStart,
}
pub struct PrefixConfig<'a> {
pub format: &'a str,
pub tz: &'a TimezoneSource,
pub clock: &'a dyn Clock,
pub source: PrefixSource,
}
pub struct RelativeConfig<'a> {
pub rewriter: &'a RelativeRewriter,
pub reference: DateTime<Utc>,
}
pub fn run_prefix<R: BufRead, W: Write>(
mut reader: R,
mut writer: W,
cfg: &PrefixConfig<'_>,
) -> std::io::Result<()> {
let program_start = cfg.clock.now();
let mut previous_line_at = program_start;
let mut line = Vec::with_capacity(256);
loop {
line.clear();
match reader.read_until(b'\n', &mut line) {
Ok(0) => return Ok(()), Ok(_) => {
let now = cfg.clock.now();
let prefix = match cfg.source {
PrefixSource::Absolute => format::format_with(cfg.format, now, cfg.tz),
PrefixSource::SincePreviousLine => {
let elapsed = (now - previous_line_at).to_std().unwrap_or_default();
previous_line_at = now;
render_elapsed(cfg.format, elapsed)
}
PrefixSource::SinceProgramStart => {
let elapsed = (now - program_start).to_std().unwrap_or_default();
render_elapsed(cfg.format, elapsed)
}
};
if let Err(err) = writer
.write_all(prefix.as_bytes())
.and_then(|_| writer.write_all(b" "))
.and_then(|_| writer.write_all(&line))
.and_then(|_| writer.flush())
{
if err.kind() == ErrorKind::BrokenPipe {
return Ok(());
}
return Err(err);
}
}
Err(err) => {
if err.kind() == ErrorKind::BrokenPipe {
return Ok(());
}
return Err(err);
}
}
}
}
pub fn run_relative<R: BufRead, W: Write>(
mut reader: R,
mut writer: W,
cfg: &RelativeConfig<'_>,
) -> std::io::Result<()> {
let mut line = Vec::with_capacity(256);
loop {
line.clear();
match reader.read_until(b'\n', &mut line) {
Ok(0) => return Ok(()),
Ok(_) => {
let text = String::from_utf8_lossy(&line);
let rewritten = cfg.rewriter.rewrite(&text, cfg.reference);
if let Err(err) = writer
.write_all(rewritten.as_bytes())
.and_then(|_| writer.flush())
{
if err.kind() == ErrorKind::BrokenPipe {
return Ok(());
}
return Err(err);
}
}
Err(err) => {
if err.kind() == ErrorKind::BrokenPipe {
return Ok(());
}
return Err(err);
}
}
}
}
fn render_elapsed(spec: &str, elapsed: std::time::Duration) -> String {
let secs = elapsed.as_secs() as i64;
let nsecs = elapsed.subsec_nanos();
let synthetic = chrono::DateTime::<Utc>::from_timestamp(secs, nsecs).unwrap_or_else(|| {
chrono::DateTime::<Utc>::from_timestamp(0, 0).expect("epoch is in range")
});
format::format_with(spec, synthetic, &TimezoneSource::Utc)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::time::clock::Fixed;
use chrono::TimeZone;
use std::io::Cursor;
fn fixed_clock() -> Fixed {
Fixed::new(Utc.with_ymd_and_hms(2026, 5, 22, 14, 30, 45).unwrap())
}
#[test]
fn absolute_default_format() {
let clock = fixed_clock();
let tz = TimezoneSource::Utc;
let cfg = PrefixConfig {
format: format::DEFAULT_FORMAT,
tz: &tz,
clock: &clock,
source: PrefixSource::Absolute,
};
let mut out = Vec::new();
run_prefix(Cursor::new("hello\nworld\n"), &mut out, &cfg).expect("ok");
let s = String::from_utf8(out).expect("utf-8");
assert_eq!(s, "May 22 14:30:45 hello\nMay 22 14:30:45 world\n");
}
#[test]
fn since_program_start_renders_zero_on_first_line() {
let clock = fixed_clock();
let tz = TimezoneSource::Utc;
let cfg = PrefixConfig {
format: "%H:%M:%S",
tz: &tz,
clock: &clock,
source: PrefixSource::SinceProgramStart,
};
let mut out = Vec::new();
run_prefix(Cursor::new("a\n"), &mut out, &cfg).expect("ok");
let s = String::from_utf8(out).expect("utf-8");
assert!(
s.starts_with("00:00:00 "),
"expected elapsed-zero prefix; got {s:?}",
);
}
#[test]
fn empty_stdin_produces_no_output() {
let clock = fixed_clock();
let tz = TimezoneSource::Utc;
let cfg = PrefixConfig {
format: format::DEFAULT_FORMAT,
tz: &tz,
clock: &clock,
source: PrefixSource::Absolute,
};
let mut out = Vec::new();
run_prefix(Cursor::new(""), &mut out, &cfg).expect("ok");
assert!(out.is_empty(), "expected no output; got {:?}", out);
}
#[test]
fn partial_final_line_is_emitted_without_added_newline() {
let clock = fixed_clock();
let tz = TimezoneSource::Utc;
let cfg = PrefixConfig {
format: format::DEFAULT_FORMAT,
tz: &tz,
clock: &clock,
source: PrefixSource::Absolute,
};
let mut out = Vec::new();
run_prefix(Cursor::new("incomplete"), &mut out, &cfg).expect("ok");
let s = String::from_utf8(out).expect("utf-8");
assert_eq!(s, "May 22 14:30:45 incomplete");
assert!(!s.ends_with('\n'));
}
#[test]
fn binary_payload_passes_through() {
let input: &[u8] = b"hello\xff\nworld\n";
let clock = fixed_clock();
let tz = TimezoneSource::Utc;
let cfg = PrefixConfig {
format: format::DEFAULT_FORMAT,
tz: &tz,
clock: &clock,
source: PrefixSource::Absolute,
};
let mut out = Vec::new();
run_prefix(Cursor::new(input), &mut out, &cfg).expect("ok");
assert!(
out.contains(&0xFF),
"expected 0xFF byte to pass through; got {:?}",
out,
);
}
}