1use std::io::BufRead;
9
10use crate::{Envelope, FaceError, InputFormat};
11
12pub const SNIFF_PROBE_BYTES: usize = 4096;
14
15const UTF8_BOM: [u8; 3] = [0xEF, 0xBB, 0xBF];
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20#[non_exhaustive]
21pub struct SniffOutcome {
22 pub format: InputFormat,
24 pub bom_bytes: usize,
28}
29
30pub fn sniff_format(reader: &mut impl BufRead) -> Result<SniffOutcome, FaceError> {
65 let bom_bytes = strip_bom(reader)?;
66 let probe = take_probe(reader)?;
67
68 if Envelope::looks_like_envelope(&probe) {
69 return Ok(SniffOutcome {
70 format: InputFormat::FaceEnvelope,
71 bom_bytes,
72 });
73 }
74
75 let first_non_ws = probe.iter().find(|b| !b.is_ascii_whitespace());
76 let format = match first_non_ws {
77 None => {
81 return Err(FaceError::InputParse {
82 format: InputFormat::Json,
83 message: "input is empty (no bytes to classify)".to_string(),
84 });
85 }
86 Some(b'{') | Some(b'[') => classify_brace_or_bracket(&probe)?,
87 Some(_) => {
88 if parses_as_json_value(&probe) {
90 InputFormat::Json
91 } else if parses_as_jsonl(&probe) {
92 InputFormat::Jsonl
93 } else if let Some(format) = sniff_delimited(&probe) {
94 format
95 } else {
96 return Err(FaceError::InputParse {
97 format: InputFormat::Json,
98 message: "could not classify input as JSON, JSONL, CSV, or TSV".to_string(),
99 });
100 }
101 }
102 };
103
104 Ok(SniffOutcome { format, bom_bytes })
105}
106
107fn classify_brace_or_bracket(probe: &[u8]) -> Result<InputFormat, FaceError> {
116 match json_value_probe_state(probe) {
117 JsonProbeState::Complete => Ok(InputFormat::Json),
118 JsonProbeState::Incomplete => Ok(InputFormat::Json),
119 JsonProbeState::Invalid => {
120 if parses_as_jsonl(probe) {
121 Ok(InputFormat::Jsonl)
122 } else {
123 Err(FaceError::InputParse {
126 format: InputFormat::Json,
127 message: "input begins with `{` or `[` but parses as neither \
128 JSON nor JSONL"
129 .to_string(),
130 })
131 }
132 }
133 }
134}
135
136fn strip_bom(reader: &mut impl BufRead) -> Result<usize, FaceError> {
138 let buf = reader.fill_buf()?;
139 if buf.starts_with(&UTF8_BOM) {
140 reader.consume(UTF8_BOM.len());
141 Ok(UTF8_BOM.len())
142 } else {
143 Ok(0)
144 }
145}
146
147fn take_probe(reader: &mut impl BufRead) -> Result<Vec<u8>, FaceError> {
156 let buf = reader.fill_buf()?;
157 let len = buf.len().min(SNIFF_PROBE_BYTES);
158 Ok(buf[..len].to_vec())
159}
160
161fn parses_as_json_value(probe: &[u8]) -> bool {
167 matches!(json_value_probe_state(probe), JsonProbeState::Complete)
168}
169
170enum JsonProbeState {
171 Complete,
172 Incomplete,
173 Invalid,
174}
175
176fn json_value_probe_state(probe: &[u8]) -> JsonProbeState {
177 match serde_json::from_slice::<serde_json::Value>(probe) {
178 Ok(_) => JsonProbeState::Complete,
179 Err(error) if error.is_eof() => JsonProbeState::Incomplete,
180 Err(_) => JsonProbeState::Invalid,
181 }
182}
183
184fn parses_as_jsonl(probe: &[u8]) -> bool {
193 let text = match std::str::from_utf8(probe) {
194 Ok(t) => t,
195 Err(_) => return false,
196 };
197
198 let lines: Vec<&str> = text
199 .split('\n')
200 .map(|l| l.trim_end_matches('\r'))
201 .filter(|l| !l.trim().is_empty())
202 .collect();
203
204 if lines.len() < 2 {
205 return false;
206 }
207
208 let ok_count = lines
209 .iter()
210 .filter(|line| serde_json::from_str::<serde_json::Value>(line).is_ok())
211 .count();
212 ok_count >= 2
213}
214
215fn sniff_delimited(probe: &[u8]) -> Option<InputFormat> {
216 const CANDIDATES: [u8; 4] = [b',', b';', b'\t', b'|'];
217 let lines = sample_nonempty_lines(probe);
218 let mut best = None;
219 for delimiter in CANDIDATES {
220 let counts = lines
221 .iter()
222 .map(|line| count_delimiter_outside_quotes(line, delimiter))
223 .collect::<Vec<_>>();
224 let Some((&first, rest)) = counts.split_first() else {
225 continue;
226 };
227 if first == 0 || rest.iter().any(|count| *count != first) {
228 continue;
229 }
230 if best.is_none_or(|(_, best_count)| first > best_count) {
231 best = Some((delimiter, first));
232 }
233 }
234 match best.map(|(delimiter, _)| delimiter) {
235 Some(b'\t') => Some(InputFormat::Tsv),
236 Some(_) => Some(InputFormat::Csv),
237 None => None,
238 }
239}
240
241fn sample_nonempty_lines(bytes: &[u8]) -> Vec<&[u8]> {
242 bytes
243 .split(|b| *b == b'\n')
244 .map(|line| line.strip_suffix(b"\r").unwrap_or(line))
245 .filter(|line| line.iter().any(|b| !b.is_ascii_whitespace()))
246 .take(8)
247 .collect()
248}
249
250fn count_delimiter_outside_quotes(line: &[u8], delimiter: u8) -> usize {
251 let mut count = 0usize;
252 let mut in_quotes = false;
253 let mut i = 0usize;
254 while let Some(&byte) = line.get(i) {
255 if byte == b'"' {
256 if in_quotes && line.get(i + 1) == Some(&b'"') {
257 i += 2;
258 continue;
259 }
260 in_quotes = !in_quotes;
261 } else if byte == delimiter && !in_quotes {
262 count += 1;
263 }
264 i += 1;
265 }
266 count
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272 use std::io::BufReader;
273
274 #[test]
275 fn sniffs_json_array() {
276 let mut r = BufReader::new(b"[1, 2, 3]" as &[u8]);
277 let out = sniff_format(&mut r).unwrap();
278 assert_eq!(out.format, InputFormat::Json);
279 assert_eq!(out.bom_bytes, 0);
280 let buf = r.fill_buf().unwrap();
282 assert_eq!(buf, b"[1, 2, 3]");
283 }
284
285 #[test]
286 fn sniffs_json_object() {
287 let mut r = BufReader::new(b"{\"a\": 1}" as &[u8]);
288 let out = sniff_format(&mut r).unwrap();
289 assert_eq!(out.format, InputFormat::Json);
290 }
291
292 #[test]
293 fn sniffs_jsonl() {
294 let mut r = BufReader::new(b"{\"a\":1}\n{\"a\":2}\n{\"a\":3}\n" as &[u8]);
295 let out = sniff_format(&mut r).unwrap();
296 assert_eq!(out.format, InputFormat::Jsonl);
297 }
298
299 #[test]
300 fn sniffs_face_envelope() {
301 let env = br#"{"result":{"input_total":0},"clusters":[]}"#;
302 let mut r = BufReader::new(&env[..]);
303 let out = sniff_format(&mut r).unwrap();
304 assert_eq!(out.format, InputFormat::FaceEnvelope);
305 }
306
307 #[test]
308 fn strips_utf8_bom() {
309 let mut bytes = vec![0xEF, 0xBB, 0xBF];
310 bytes.extend_from_slice(b"{\"a\": 1}");
311 let mut r = BufReader::new(&bytes[..]);
312 let out = sniff_format(&mut r).unwrap();
313 assert_eq!(out.format, InputFormat::Json);
314 assert_eq!(out.bom_bytes, 3);
315 let buf = r.fill_buf().unwrap();
317 assert_eq!(&buf[..8], b"{\"a\": 1}");
318 }
319
320 #[test]
321 fn sniffs_csv() {
322 let mut r = BufReader::new(b"a,b,c\n1,2,3\n4,5,6\n" as &[u8]);
323 let out = sniff_format(&mut r).unwrap();
324 assert_eq!(out.format, InputFormat::Csv);
325 }
326
327 #[test]
328 fn empty_input_errors() {
329 let mut r = BufReader::new(b"" as &[u8]);
330 let err = sniff_format(&mut r).unwrap_err();
331 match err {
332 FaceError::InputParse { .. } => {}
333 other => panic!("unexpected: {other:?}"),
334 }
335 }
336}