rust_expect/auto_config/
line_ending.rs

1//! Line ending detection and handling.
2
3/// Line ending style.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum LineEnding {
6    /// Unix-style LF (\n).
7    #[default]
8    Lf,
9    /// Windows-style CRLF (\r\n).
10    CrLf,
11    /// Old Mac-style CR (\r).
12    Cr,
13    /// Unknown or mixed.
14    Unknown,
15}
16
17impl LineEnding {
18    /// Get the line ending bytes.
19    #[must_use]
20    pub const fn as_bytes(&self) -> &'static [u8] {
21        match self {
22            Self::Lf => b"\n",
23            Self::CrLf => b"\r\n",
24            Self::Cr => b"\r",
25            Self::Unknown => b"\n",
26        }
27    }
28
29    /// Get the line ending string.
30    #[must_use]
31    pub const fn as_str(&self) -> &'static str {
32        match self {
33            Self::Lf => "\n",
34            Self::CrLf => "\r\n",
35            Self::Cr => "\r",
36            Self::Unknown => "\n",
37        }
38    }
39
40    /// Get name of line ending.
41    #[must_use]
42    pub const fn name(&self) -> &'static str {
43        match self {
44            Self::Lf => "LF",
45            Self::CrLf => "CRLF",
46            Self::Cr => "CR",
47            Self::Unknown => "Unknown",
48        }
49    }
50}
51
52/// Detect line ending style in data.
53#[must_use]
54pub fn detect_line_ending(data: &[u8]) -> LineEnding {
55    let mut lf_count = 0;
56    let mut crlf_count = 0;
57    let mut cr_count = 0;
58
59    let mut i = 0;
60    while i < data.len() {
61        if i + 1 < data.len() && data[i] == b'\r' && data[i + 1] == b'\n' {
62            crlf_count += 1;
63            i += 2;
64        } else if data[i] == b'\n' {
65            lf_count += 1;
66            i += 1;
67        } else if data[i] == b'\r' {
68            cr_count += 1;
69            i += 1;
70        } else {
71            i += 1;
72        }
73    }
74
75    // Determine dominant style
76    if crlf_count > lf_count && crlf_count > cr_count {
77        LineEnding::CrLf
78    } else if lf_count > crlf_count && lf_count > cr_count {
79        LineEnding::Lf
80    } else if cr_count > 0 && lf_count == 0 && crlf_count == 0 {
81        LineEnding::Cr
82    } else if lf_count == 0 && crlf_count == 0 && cr_count == 0 {
83        LineEnding::Unknown
84    } else {
85        // Mixed - return platform default
86        LineEnding::default()
87    }
88}
89
90/// Normalize line endings in data.
91#[must_use]
92pub fn normalize_line_endings(data: &[u8], target: LineEnding) -> Vec<u8> {
93    let target_bytes = target.as_bytes();
94    let mut result = Vec::with_capacity(data.len());
95    let mut i = 0;
96
97    while i < data.len() {
98        if i + 1 < data.len() && data[i] == b'\r' && data[i + 1] == b'\n' {
99            // CRLF
100            result.extend_from_slice(target_bytes);
101            i += 2;
102        } else if data[i] == b'\n' {
103            // LF
104            result.extend_from_slice(target_bytes);
105            i += 1;
106        } else if data[i] == b'\r' {
107            // CR (not followed by LF)
108            result.extend_from_slice(target_bytes);
109            i += 1;
110        } else {
111            result.push(data[i]);
112            i += 1;
113        }
114    }
115
116    result
117}
118
119/// Convert line endings to LF.
120#[must_use]
121pub fn to_lf(data: &[u8]) -> Vec<u8> {
122    normalize_line_endings(data, LineEnding::Lf)
123}
124
125/// Convert line endings to CRLF.
126#[must_use]
127pub fn to_crlf(data: &[u8]) -> Vec<u8> {
128    normalize_line_endings(data, LineEnding::CrLf)
129}
130
131/// Line ending configuration.
132#[derive(Debug, Clone)]
133pub struct LineEndingConfig {
134    /// Input line ending (what to send).
135    pub input: LineEnding,
136    /// Output line ending (normalize received output).
137    pub output: Option<LineEnding>,
138    /// Auto-detect from first output.
139    pub auto_detect: bool,
140}
141
142impl Default for LineEndingConfig {
143    fn default() -> Self {
144        Self {
145            input: LineEnding::default(),
146            output: None,
147            auto_detect: true,
148        }
149    }
150}
151
152impl LineEndingConfig {
153    /// Create new config.
154    #[must_use]
155    pub fn new() -> Self {
156        Self::default()
157    }
158
159    /// Set input line ending.
160    #[must_use]
161    pub const fn with_input(mut self, ending: LineEnding) -> Self {
162        self.input = ending;
163        self
164    }
165
166    /// Set output normalization.
167    #[must_use]
168    pub const fn with_output(mut self, ending: LineEnding) -> Self {
169        self.output = Some(ending);
170        self
171    }
172
173    /// Enable auto-detection.
174    #[must_use]
175    pub const fn with_auto_detect(mut self, auto: bool) -> Self {
176        self.auto_detect = auto;
177        self
178    }
179
180    /// Process input (add line endings to send).
181    #[must_use]
182    pub fn process_input(&self, line: &str) -> Vec<u8> {
183        let mut result = line.as_bytes().to_vec();
184        if !line.ends_with('\n') && !line.ends_with('\r') {
185            result.extend_from_slice(self.input.as_bytes());
186        }
187        result
188    }
189
190    /// Process output (normalize received data).
191    #[must_use]
192    pub fn process_output(&self, data: &[u8]) -> Vec<u8> {
193        if let Some(target) = self.output {
194            normalize_line_endings(data, target)
195        } else {
196            data.to_vec()
197        }
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn detect_lf() {
207        let data = b"line1\nline2\nline3\n";
208        assert_eq!(detect_line_ending(data), LineEnding::Lf);
209    }
210
211    #[test]
212    fn detect_crlf() {
213        let data = b"line1\r\nline2\r\nline3\r\n";
214        assert_eq!(detect_line_ending(data), LineEnding::CrLf);
215    }
216
217    #[test]
218    fn normalize_to_lf() {
219        let data = b"line1\r\nline2\r\n";
220        let result = to_lf(data);
221        assert_eq!(result, b"line1\nline2\n");
222    }
223
224    #[test]
225    fn normalize_to_crlf() {
226        let data = b"line1\nline2\n";
227        let result = to_crlf(data);
228        assert_eq!(result, b"line1\r\nline2\r\n");
229    }
230
231    #[test]
232    fn line_ending_bytes() {
233        assert_eq!(LineEnding::Lf.as_bytes(), b"\n");
234        assert_eq!(LineEnding::CrLf.as_bytes(), b"\r\n");
235    }
236}