faucet_core/file_format/
json.rs1use crate::error::FaucetError;
7use serde_json::Value;
8
9pub const RAW_TEXT_FIELD: &str = "text";
11
12pub fn decode_lines(bytes: &[u8]) -> Result<Vec<Value>, FaucetError> {
18 let text = as_utf8(bytes, "json_lines")?;
19 let mut out = Vec::new();
20 for (i, line) in text.lines().enumerate() {
21 let line = line.trim();
22 if line.is_empty() {
23 continue;
24 }
25 let v = serde_json::from_str(line)
26 .map_err(|e| FaucetError::Source(format!("json_lines: line {}: {e}", i + 1)))?;
27 check_numbers_exact(line.as_bytes())
28 .map_err(|e| FaucetError::Source(format!("json_lines: line {}: {e}", i + 1)))?;
29 out.push(v);
30 }
31 Ok(out)
32}
33
34pub fn decode_array(bytes: &[u8]) -> Result<Vec<Value>, FaucetError> {
36 let v: Value = serde_json::from_slice(bytes)
37 .map_err(|e| FaucetError::Source(format!("json_array: {e}")))?;
38 check_numbers_exact(bytes).map_err(|e| FaucetError::Source(format!("json_array: {e}")))?;
39 Ok(match v {
40 Value::Array(a) => a,
41 other => vec![other],
42 })
43}
44
45fn check_numbers_exact(bytes: &[u8]) -> Result<(), String> {
54 let mut i = 0;
55 let mut in_string = false;
56 while i < bytes.len() {
57 let b = bytes[i];
58 if in_string {
59 match b {
60 b'\\' => i += 1,
61 b'"' => in_string = false,
62 _ => {}
63 }
64 i += 1;
65 continue;
66 }
67 if b == b'"' {
68 in_string = true;
69 i += 1;
70 continue;
71 }
72 if b == b'-' || b.is_ascii_digit() {
73 let start = i;
74 while i < bytes.len()
75 && matches!(bytes[i], b'0'..=b'9' | b'-' | b'+' | b'.' | b'e' | b'E')
76 {
77 i += 1;
78 }
79 let token = std::str::from_utf8(&bytes[start..i]).unwrap_or_default();
80 if token.bytes().filter(u8::is_ascii_digit).count() > 15 {
81 check_literal(token)?;
82 }
83 continue;
84 }
85 i += 1;
86 }
87 Ok(())
88}
89
90fn check_literal(token: &str) -> Result<(), String> {
91 let Ok(parsed) = serde_json::from_str::<serde_json::Number>(token) else {
92 return Ok(());
93 };
94 let back = parsed.to_string();
95 if canonical_decimal(token) == canonical_decimal(&back) {
96 Ok(())
97 } else {
98 Err(format!(
99 "number {token} cannot be represented exactly (it would be read as {back}); \
100 quote it as a string in the source data to keep every digit"
101 ))
102 }
103}
104
105fn canonical_decimal(s: &str) -> Option<(bool, String, i64)> {
108 let (neg, body) = match s.strip_prefix('-') {
109 Some(rest) => (true, rest),
110 None => (false, s),
111 };
112 let (mantissa, exp) = match body.find(['e', 'E']) {
113 Some(k) => (&body[..k], body[k + 1..].parse::<i64>().ok()?),
114 None => (body, 0),
115 };
116 let (int, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
117 let digits: String = format!("{int}{frac}");
118 let mut point = (int.len() as i64).checked_add(exp)?;
119 let trimmed = digits.trim_start_matches('0');
120 point -= (digits.len() - trimmed.len()) as i64;
121 let trimmed = trimmed.trim_end_matches('0');
122 if trimmed.is_empty() {
123 return Some((false, String::new(), 0));
124 }
125 Some((neg, trimmed.to_string(), point))
126}
127
128pub fn decode_raw_text(bytes: &[u8]) -> Result<Vec<Value>, FaucetError> {
130 let text = as_utf8(bytes, "raw_text")?;
131 Ok(vec![serde_json::json!({ RAW_TEXT_FIELD: text })])
132}
133
134pub fn encode_lines(records: &[Value]) -> Result<Vec<u8>, FaucetError> {
136 let mut out = Vec::new();
137 for r in records {
138 serde_json::to_writer(&mut out, r)
139 .map_err(|e| FaucetError::Sink(format!("json_lines: {e}")))?;
140 out.push(b'\n');
141 }
142 Ok(out)
143}
144
145pub fn encode_array(records: &[Value]) -> Result<Vec<u8>, FaucetError> {
147 serde_json::to_vec(records).map_err(|e| FaucetError::Sink(format!("json_array: {e}")))
148}
149
150pub fn encode_raw_text(records: &[Value]) -> Result<Vec<u8>, FaucetError> {
156 let mut out = Vec::new();
157 for r in records {
158 let line = match r.get(RAW_TEXT_FIELD) {
159 Some(Value::String(s)) => s.clone(),
160 _ => super::cell_text(r),
161 };
162 out.extend_from_slice(line.as_bytes());
163 out.push(b'\n');
164 }
165 Ok(out)
166}
167
168fn as_utf8<'a>(bytes: &'a [u8], what: &str) -> Result<&'a str, FaucetError> {
169 std::str::from_utf8(bytes)
170 .map_err(|e| FaucetError::Source(format!("{what}: body is not UTF-8: {e}")))
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use serde_json::json;
177
178 #[test]
179 fn numbers_that_would_round_are_refused() {
180 let ok = br#"[{"a": 18446744073709551615, "b": -9223372036854775808, "c": 0.1,
182 "d": 1.50, "e": "123456789012345678901234567890", "f": 1.0000000000000000e-5,
183 "g": 0.0000000000000000}]"#;
184 assert_eq!(decode_array(ok).unwrap().len(), 1);
185 let err = decode_array(br#"[{"id": 18446744073709551616}]"#).unwrap_err();
186 assert!(err.to_string().contains("18446744073709551616"), "{err}");
187 let err = decode_lines(b"{\"x\":\"\\\"\"}\n{\"amt\": 0.12345678901234567890}\n")
188 .unwrap_err()
189 .to_string();
190 assert!(
191 err.contains("line 2") && err.contains("0.12345678901234567890"),
192 "{err}"
193 );
194 }
195
196 #[test]
197 fn canonical_decimal_normalizes_spellings() {
198 assert_eq!(canonical_decimal("1.50"), canonical_decimal("15e-1"));
199 assert_eq!(canonical_decimal("0.15e1"), canonical_decimal("1.5"));
200 assert_eq!(canonical_decimal("-0.0"), canonical_decimal("0"));
201 assert_ne!(canonical_decimal("-1"), canonical_decimal("1"));
202 assert_eq!(canonical_decimal("1e99999999999999999999"), None);
203 assert!(check_literal("1e99999").is_ok());
204 }
205
206 #[test]
207 fn json_lines_round_trips_and_tolerates_blank_lines() {
208 let recs = vec![json!({"a": 1}), json!({"a": 2})];
209 let bytes = encode_lines(&recs).expect("encode");
210 assert_eq!(bytes, b"{\"a\":1}\n{\"a\":2}\n");
211 assert_eq!(decode_lines(&bytes).expect("decode"), recs);
212 assert_eq!(
214 decode_lines(b"{\"a\":1}\r\n\n{\"a\":2}\n").expect("decode"),
215 recs
216 );
217 }
218
219 #[test]
220 fn a_bad_line_names_its_line_number() {
221 let err = decode_lines(b"{\"a\":1}\nnot json\n").expect_err("bad line");
222 assert!(err.to_string().contains("line 2"), "{err}");
223 }
224
225 #[test]
226 fn json_array_accepts_an_array_or_a_bare_object() {
227 assert_eq!(
228 decode_array(br#"[{"a":1},{"a":2}]"#).expect("array"),
229 vec![json!({"a": 1}), json!({"a": 2})]
230 );
231 assert_eq!(
232 decode_array(br#"{"a":1}"#).expect("object"),
233 vec![json!({"a": 1})]
234 );
235 assert_eq!(
236 encode_array(&[json!({"a": 1})]).expect("encode"),
237 br#"[{"a":1}]"#
238 );
239 }
240
241 #[test]
242 fn raw_text_round_trips_through_the_text_field() {
243 let recs = decode_raw_text(b"hello\nworld").expect("decode");
244 assert_eq!(recs, vec![json!({"text": "hello\nworld"})]);
245 assert_eq!(encode_raw_text(&recs).expect("encode"), b"hello\nworld\n");
246 }
247
248 #[test]
249 fn raw_text_encode_never_drops_a_record_of_the_wrong_shape() {
250 let out = encode_raw_text(&[json!({"other": 1})]).expect("encode");
251 assert_eq!(out, b"{\"other\":1}\n");
252 }
253
254 #[test]
255 fn non_utf8_is_reported_not_replaced() {
256 let err = decode_lines(&[0xff, 0xfe]).expect_err("invalid utf-8");
257 assert!(err.to_string().contains("not UTF-8"), "{err}");
258 }
259}