use crate::error::FaucetError;
use serde_json::Value;
pub const RAW_TEXT_FIELD: &str = "text";
pub fn decode_lines(bytes: &[u8]) -> Result<Vec<Value>, FaucetError> {
let text = as_utf8(bytes, "json_lines")?;
let mut out = Vec::new();
for (i, line) in text.lines().enumerate() {
let line = line.trim();
if line.is_empty() {
continue;
}
let v = serde_json::from_str(line)
.map_err(|e| FaucetError::Source(format!("json_lines: line {}: {e}", i + 1)))?;
check_numbers_exact(line.as_bytes())
.map_err(|e| FaucetError::Source(format!("json_lines: line {}: {e}", i + 1)))?;
out.push(v);
}
Ok(out)
}
pub fn decode_array(bytes: &[u8]) -> Result<Vec<Value>, FaucetError> {
let v: Value = serde_json::from_slice(bytes)
.map_err(|e| FaucetError::Source(format!("json_array: {e}")))?;
check_numbers_exact(bytes).map_err(|e| FaucetError::Source(format!("json_array: {e}")))?;
Ok(match v {
Value::Array(a) => a,
other => vec![other],
})
}
fn check_numbers_exact(bytes: &[u8]) -> Result<(), String> {
match unfaithful_literals(bytes).first() {
Some(r) => check_literal(std::str::from_utf8(&bytes[r.clone()]).unwrap_or_default()),
None => Ok(()),
}
}
pub(crate) fn unfaithful_literals(bytes: &[u8]) -> Vec<std::ops::Range<usize>> {
let mut out = Vec::new();
let mut i = 0;
let mut in_string = false;
while i < bytes.len() {
let b = bytes[i];
if in_string {
match b {
b'\\' => i += 1,
b'"' => in_string = false,
_ => {}
}
i += 1;
continue;
}
if b == b'"' {
in_string = true;
i += 1;
continue;
}
if b == b'-' || b.is_ascii_digit() {
let start = i;
while i < bytes.len()
&& matches!(bytes[i], b'0'..=b'9' | b'-' | b'+' | b'.' | b'e' | b'E')
{
i += 1;
}
let token = std::str::from_utf8(&bytes[start..i]).unwrap_or_default();
if token.bytes().filter(u8::is_ascii_digit).count() > 15
&& let Ok(parsed) = serde_json::from_str::<serde_json::Number>(token)
&& !literal_is_faithful(token, &parsed)
{
out.push(start..i);
}
continue;
}
i += 1;
}
out
}
fn check_literal(token: &str) -> Result<(), String> {
let Ok(parsed) = serde_json::from_str::<serde_json::Number>(token) else {
return Ok(());
};
let back = parsed.to_string();
if literal_is_faithful(token, &parsed) {
Ok(())
} else {
Err(format!(
"number {token} cannot be represented exactly (it would be read as {back}); \
quote it as a string in the source data to keep every digit"
))
}
}
fn literal_is_faithful(token: &str, parsed: &serde_json::Number) -> bool {
let literal = canonical_decimal(token);
let is_integer = !token.contains(['.', 'e', 'E']);
match parsed.as_f64() {
Some(f) if parsed.is_f64() => {
if is_integer {
return false;
}
let digits = literal.as_ref().map_or(1, |(_, d, _)| d.len().max(1));
canonical_decimal(&format!("{:.*e}", digits - 1, f)) == literal
}
_ => canonical_decimal(&parsed.to_string()) == literal,
}
}
fn canonical_decimal(s: &str) -> Option<(bool, String, i64)> {
let (neg, body) = match s.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, s),
};
let (mantissa, exp) = match body.find(['e', 'E']) {
Some(k) => (&body[..k], body[k + 1..].parse::<i64>().ok()?),
None => (body, 0),
};
let (int, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
let digits: String = format!("{int}{frac}");
let mut point = (int.len() as i64).checked_add(exp)?;
let trimmed = digits.trim_start_matches('0');
point -= (digits.len() - trimmed.len()) as i64;
let trimmed = trimmed.trim_end_matches('0');
if trimmed.is_empty() {
return Some((false, String::new(), 0));
}
Some((neg, trimmed.to_string(), point))
}
pub fn decode_raw_text(bytes: &[u8]) -> Result<Vec<Value>, FaucetError> {
let text = as_utf8(bytes, "raw_text")?;
Ok(vec![serde_json::json!({ RAW_TEXT_FIELD: text })])
}
pub fn encode_lines(records: &[Value]) -> Result<Vec<u8>, FaucetError> {
let mut out = Vec::new();
for r in records {
serde_json::to_writer(&mut out, r)
.map_err(|e| FaucetError::Sink(format!("json_lines: {e}")))?;
out.push(b'\n');
}
Ok(out)
}
pub fn encode_array(records: &[Value]) -> Result<Vec<u8>, FaucetError> {
serde_json::to_vec(records).map_err(|e| FaucetError::Sink(format!("json_array: {e}")))
}
pub fn encode_raw_text(records: &[Value]) -> Result<Vec<u8>, FaucetError> {
let mut out = Vec::new();
for r in records {
let line = match r.get(RAW_TEXT_FIELD) {
Some(Value::String(s)) => s.clone(),
_ => super::cell_text(r),
};
out.extend_from_slice(line.as_bytes());
out.push(b'\n');
}
Ok(out)
}
fn as_utf8<'a>(bytes: &'a [u8], what: &str) -> Result<&'a str, FaucetError> {
std::str::from_utf8(bytes)
.map_err(|e| FaucetError::Source(format!("{what}: body is not UTF-8: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn numbers_that_would_round_are_refused() {
let ok = br#"[{"a": 18446744073709551615, "b": -9223372036854775808, "c": 0.1,
"d": 1.50, "e": "123456789012345678901234567890", "f": 1.0000000000000000e-5,
"g": 0.0000000000000000}]"#;
assert_eq!(decode_array(ok).unwrap().len(), 1);
let err = decode_array(br#"[{"id": 18446744073709551616}]"#).unwrap_err();
assert!(err.to_string().contains("18446744073709551616"), "{err}");
let err = decode_lines(b"{\"x\":\"\\\"\"}\n{\"amt\": 0.12345678901234567890}\n")
.unwrap_err()
.to_string();
assert!(
err.contains("line 2") && err.contains("0.12345678901234567890"),
"{err}"
);
}
#[test]
fn seventeen_digit_float_renderings_are_accepted() {
let ok = br#"[{"a": 0.10000000000000001, "b": 3.1415926535897931,
"c": 2.2250738585072014e-308, "d": -0.30000000000000004,
"e": 0.1000000000000000055511151231257827}]"#;
let rec = &decode_array(ok).unwrap()[0];
assert_eq!(rec["a"], json!(0.1));
assert_eq!(rec["b"], json!(std::f64::consts::PI));
let err = decode_array(br#"[{"amt": 1234567890123456.7}]"#).unwrap_err();
assert!(err.to_string().contains("1234567890123456.7"), "{err}");
assert!(!literal_is_faithful(
"18446744073709551616",
&serde_json::from_str("18446744073709551616").unwrap()
));
}
#[test]
fn canonical_decimal_normalizes_spellings() {
assert_eq!(canonical_decimal("1.50"), canonical_decimal("15e-1"));
assert_eq!(canonical_decimal("0.15e1"), canonical_decimal("1.5"));
assert_eq!(canonical_decimal("-0.0"), canonical_decimal("0"));
assert_ne!(canonical_decimal("-1"), canonical_decimal("1"));
assert_eq!(canonical_decimal("1e99999999999999999999"), None);
assert!(check_literal("1e99999").is_ok());
}
#[test]
fn json_lines_round_trips_and_tolerates_blank_lines() {
let recs = vec![json!({"a": 1}), json!({"a": 2})];
let bytes = encode_lines(&recs).expect("encode");
assert_eq!(bytes, b"{\"a\":1}\n{\"a\":2}\n");
assert_eq!(decode_lines(&bytes).expect("decode"), recs);
assert_eq!(
decode_lines(b"{\"a\":1}\r\n\n{\"a\":2}\n").expect("decode"),
recs
);
}
#[test]
fn a_bad_line_names_its_line_number() {
let err = decode_lines(b"{\"a\":1}\nnot json\n").expect_err("bad line");
assert!(err.to_string().contains("line 2"), "{err}");
}
#[test]
fn json_array_accepts_an_array_or_a_bare_object() {
assert_eq!(
decode_array(br#"[{"a":1},{"a":2}]"#).expect("array"),
vec![json!({"a": 1}), json!({"a": 2})]
);
assert_eq!(
decode_array(br#"{"a":1}"#).expect("object"),
vec![json!({"a": 1})]
);
assert_eq!(
encode_array(&[json!({"a": 1})]).expect("encode"),
br#"[{"a":1}]"#
);
}
#[test]
fn raw_text_round_trips_through_the_text_field() {
let recs = decode_raw_text(b"hello\nworld").expect("decode");
assert_eq!(recs, vec![json!({"text": "hello\nworld"})]);
assert_eq!(encode_raw_text(&recs).expect("encode"), b"hello\nworld\n");
}
#[test]
fn raw_text_encode_never_drops_a_record_of_the_wrong_shape() {
let out = encode_raw_text(&[json!({"other": 1})]).expect("encode");
assert_eq!(out, b"{\"other\":1}\n");
}
#[test]
fn non_utf8_is_reported_not_replaced() {
let err = decode_lines(&[0xff, 0xfe]).expect_err("invalid utf-8");
assert!(err.to_string().contains("not UTF-8"), "{err}");
}
}