use bytes::Bytes;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum LineEnding {
#[default]
None,
AddCrToLf,
AddLfToCr,
DropCr,
DropLf,
}
pub trait Mapper: Send {
fn map(&mut self, bytes: &[u8]) -> Bytes;
}
#[derive(Clone, Copy, Debug, Default)]
pub struct LineEndingMapper {
rule: LineEnding,
}
impl LineEndingMapper {
#[must_use]
pub const fn new(rule: LineEnding) -> Self {
Self { rule }
}
#[must_use]
pub const fn rule(&self) -> LineEnding {
self.rule
}
}
impl Mapper for LineEndingMapper {
fn map(&mut self, bytes: &[u8]) -> Bytes {
if matches!(self.rule, LineEnding::None) {
return Bytes::copy_from_slice(bytes);
}
let mut out = Vec::with_capacity(bytes.len() + 4);
for &byte in bytes {
match (self.rule, byte) {
(LineEnding::AddCrToLf, b'\n') | (LineEnding::AddLfToCr, b'\r') => {
out.push(b'\r');
out.push(b'\n');
}
(LineEnding::DropCr, b'\r') | (LineEnding::DropLf, b'\n') => {
}
_ => out.push(byte),
}
}
Bytes::from(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(rule: LineEnding, input: &[u8]) -> Vec<u8> {
let mut m = LineEndingMapper::new(rule);
m.map(input).to_vec()
}
#[test]
fn none_passes_bytes_through_verbatim() {
assert_eq!(run(LineEnding::None, b""), b"");
assert_eq!(
run(LineEnding::None, b"hello\r\nworld\n"),
b"hello\r\nworld\n"
);
}
#[test]
fn default_rule_is_none() {
let mut m = LineEndingMapper::default();
assert_eq!(m.rule(), LineEnding::None);
assert_eq!(m.map(b"abc").to_vec(), b"abc");
}
#[test]
fn add_cr_to_lf_converts_lf_to_crlf() {
assert_eq!(run(LineEnding::AddCrToLf, b"hi\nyo\n"), b"hi\r\nyo\r\n");
}
#[test]
fn add_cr_to_lf_does_not_touch_existing_crlf() {
assert_eq!(run(LineEnding::AddCrToLf, b"a\r\nb"), b"a\r\r\nb");
}
#[test]
fn add_cr_to_lf_handles_consecutive_lfs() {
assert_eq!(run(LineEnding::AddCrToLf, b"\n\n"), b"\r\n\r\n");
}
#[test]
fn add_lf_to_cr_converts_cr_to_crlf() {
assert_eq!(run(LineEnding::AddLfToCr, b"hi\ryo\r"), b"hi\r\nyo\r\n");
}
#[test]
fn add_lf_to_cr_does_not_touch_existing_crlf() {
assert_eq!(run(LineEnding::AddLfToCr, b"a\r\nb"), b"a\r\n\nb");
}
#[test]
fn drop_cr_removes_carriage_returns_and_keeps_other_bytes() {
assert_eq!(run(LineEnding::DropCr, b"a\r\nb\rc"), b"a\nbc");
}
#[test]
fn drop_lf_removes_line_feeds_and_keeps_other_bytes() {
assert_eq!(run(LineEnding::DropLf, b"a\r\nb\nc"), b"a\rbc");
}
#[test]
fn empty_input_yields_empty_output_for_every_rule() {
for rule in [
LineEnding::None,
LineEnding::AddCrToLf,
LineEnding::AddLfToCr,
LineEnding::DropCr,
LineEnding::DropLf,
] {
assert!(run(rule, b"").is_empty(), "{rule:?} on empty input");
}
}
#[test]
fn add_cr_to_lf_leaves_non_lf_bytes_alone() {
assert_eq!(run(LineEnding::AddCrToLf, b"\rabc\x1bxyz"), b"\rabc\x1bxyz");
}
}