rust_expect/auto_config/
line_ending.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum LineEnding {
6 #[default]
8 Lf,
9 CrLf,
11 Cr,
13 Unknown,
15}
16
17impl LineEnding {
18 #[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 #[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 #[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#[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 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 LineEnding::default()
87 }
88}
89
90#[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 result.extend_from_slice(target_bytes);
101 i += 2;
102 } else if data[i] == b'\n' {
103 result.extend_from_slice(target_bytes);
105 i += 1;
106 } else if data[i] == b'\r' {
107 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#[must_use]
121pub fn to_lf(data: &[u8]) -> Vec<u8> {
122 normalize_line_endings(data, LineEnding::Lf)
123}
124
125#[must_use]
127pub fn to_crlf(data: &[u8]) -> Vec<u8> {
128 normalize_line_endings(data, LineEnding::CrLf)
129}
130
131#[derive(Debug, Clone)]
133pub struct LineEndingConfig {
134 pub input: LineEnding,
136 pub output: Option<LineEnding>,
138 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 #[must_use]
155 pub fn new() -> Self {
156 Self::default()
157 }
158
159 #[must_use]
161 pub const fn with_input(mut self, ending: LineEnding) -> Self {
162 self.input = ending;
163 self
164 }
165
166 #[must_use]
168 pub const fn with_output(mut self, ending: LineEnding) -> Self {
169 self.output = Some(ending);
170 self
171 }
172
173 #[must_use]
175 pub const fn with_auto_detect(mut self, auto: bool) -> Self {
176 self.auto_detect = auto;
177 self
178 }
179
180 #[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 #[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}