#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineEnding {
#[default]
Lf,
CrLf,
Cr,
Unknown,
}
impl LineEnding {
#[must_use]
pub const fn as_bytes(&self) -> &'static [u8] {
match self {
Self::Lf => b"\n",
Self::CrLf => b"\r\n",
Self::Cr => b"\r",
Self::Unknown => b"\n",
}
}
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Lf => "\n",
Self::CrLf => "\r\n",
Self::Cr => "\r",
Self::Unknown => "\n",
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
match self {
Self::Lf => "LF",
Self::CrLf => "CRLF",
Self::Cr => "CR",
Self::Unknown => "Unknown",
}
}
}
#[must_use]
pub fn detect_line_ending(data: &[u8]) -> LineEnding {
let mut lf_count = 0;
let mut crlf_count = 0;
let mut cr_count = 0;
let mut i = 0;
while i < data.len() {
if i + 1 < data.len() && data[i] == b'\r' && data[i + 1] == b'\n' {
crlf_count += 1;
i += 2;
} else if data[i] == b'\n' {
lf_count += 1;
i += 1;
} else if data[i] == b'\r' {
cr_count += 1;
i += 1;
} else {
i += 1;
}
}
if crlf_count > lf_count && crlf_count > cr_count {
LineEnding::CrLf
} else if lf_count > crlf_count && lf_count > cr_count {
LineEnding::Lf
} else if cr_count > 0 && lf_count == 0 && crlf_count == 0 {
LineEnding::Cr
} else if lf_count == 0 && crlf_count == 0 && cr_count == 0 {
LineEnding::Unknown
} else {
LineEnding::default()
}
}
#[must_use]
pub fn normalize_line_endings(data: &[u8], target: LineEnding) -> Vec<u8> {
let target_bytes = target.as_bytes();
let mut result = Vec::with_capacity(data.len());
let mut i = 0;
while i < data.len() {
if i + 1 < data.len() && data[i] == b'\r' && data[i + 1] == b'\n' {
result.extend_from_slice(target_bytes);
i += 2;
} else if data[i] == b'\n' {
result.extend_from_slice(target_bytes);
i += 1;
} else if data[i] == b'\r' {
result.extend_from_slice(target_bytes);
i += 1;
} else {
result.push(data[i]);
i += 1;
}
}
result
}
#[must_use]
pub fn to_lf(data: &[u8]) -> Vec<u8> {
normalize_line_endings(data, LineEnding::Lf)
}
#[must_use]
pub fn to_crlf(data: &[u8]) -> Vec<u8> {
normalize_line_endings(data, LineEnding::CrLf)
}
#[derive(Debug, Clone)]
pub struct LineEndingConfig {
pub input: LineEnding,
pub output: Option<LineEnding>,
pub auto_detect: bool,
}
impl Default for LineEndingConfig {
fn default() -> Self {
Self {
input: LineEnding::default(),
output: None,
auto_detect: true,
}
}
}
impl LineEndingConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub const fn with_input(mut self, ending: LineEnding) -> Self {
self.input = ending;
self
}
#[must_use]
pub const fn with_output(mut self, ending: LineEnding) -> Self {
self.output = Some(ending);
self
}
#[must_use]
pub const fn with_auto_detect(mut self, auto: bool) -> Self {
self.auto_detect = auto;
self
}
#[must_use]
pub fn process_input(&self, line: &str) -> Vec<u8> {
let mut result = line.as_bytes().to_vec();
if !line.ends_with('\n') && !line.ends_with('\r') {
result.extend_from_slice(self.input.as_bytes());
}
result
}
#[must_use]
pub fn process_output(&self, data: &[u8]) -> Vec<u8> {
if let Some(target) = self.output {
normalize_line_endings(data, target)
} else {
data.to_vec()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detect_lf() {
let data = b"line1\nline2\nline3\n";
assert_eq!(detect_line_ending(data), LineEnding::Lf);
}
#[test]
fn detect_crlf() {
let data = b"line1\r\nline2\r\nline3\r\n";
assert_eq!(detect_line_ending(data), LineEnding::CrLf);
}
#[test]
fn normalize_to_lf() {
let data = b"line1\r\nline2\r\n";
let result = to_lf(data);
assert_eq!(result, b"line1\nline2\n");
}
#[test]
fn normalize_to_crlf() {
let data = b"line1\nline2\n";
let result = to_crlf(data);
assert_eq!(result, b"line1\r\nline2\r\n");
}
#[test]
fn line_ending_bytes() {
assert_eq!(LineEnding::Lf.as_bytes(), b"\n");
assert_eq!(LineEnding::CrLf.as_bytes(), b"\r\n");
}
}