diff options
Diffstat (limited to 'src/diagnostics')
| -rw-r--r-- | src/diagnostics/error_document.rs | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/src/diagnostics/error_document.rs b/src/diagnostics/error_document.rs new file mode 100644 index 0000000..d3aab03 --- /dev/null +++ b/src/diagnostics/error_document.rs @@ -0,0 +1,170 @@ +//! Recognize top-level JSON error markers without buffering response values. +//! This classifies provider error documents; it does not validate arbitrary JSON. + +#[derive(Default)] +pub(super) struct ErrorDocument { + started: bool, + finished: bool, + depth: usize, + string: bool, + escaped: bool, + expect_key: bool, + key: Option<Vec<u8>>, + error_key: bool, + value: Option<ErrorValue>, + detected: bool, +} + +enum ErrorValue { + Start, + String, + Container(u8), + Literal(Vec<u8>), +} + +impl ErrorDocument { + pub fn feed(&mut self, bytes: &[u8]) { + for &byte in bytes { + if self.finished || self.detected { + break; + } + if !self.started { + // JSON producers sometimes include a UTF-8 BOM. + if byte.is_ascii_whitespace() || matches!(byte, 0xef | 0xbb | 0xbf) { + continue; + } + self.started = true; + if byte != b'{' { + self.finished = true; + continue; + } + self.depth = 1; + self.expect_key = true; + continue; + } + self.inspect_value(byte); + if self.detected { + break; + } + if self.string { + if let Some(key) = &mut self.key { + if key.len() < 64 { + key.push(byte); + } else { + self.key = None; + } + } + if self.escaped { + self.escaped = false; + continue; + } + if byte == b'\\' { + self.escaped = true; + continue; + } + if byte == b'"' { + self.string = false; + if let Some(key) = self.key.take() { + self.error_key = + serde_json::from_slice::<String>(&key) + .ok() + .is_some_and(|key| { + key.eq_ignore_ascii_case("error") + || key.eq_ignore_ascii_case("errors") + }); + self.expect_key = false; + } + } + continue; + } + match byte { + b'"' => { + self.string = true; + if self.depth == 1 && self.expect_key { + self.key = Some(vec![b'"']); + } + } + b'{' | b'[' => self.depth = self.depth.saturating_add(1), + b'}' | b']' => { + self.depth = self.depth.saturating_sub(1); + if self.depth == 0 { + self.finished = true; + } + } + b':' if self.depth == 1 && self.error_key => { + self.value = Some(ErrorValue::Start); + self.error_key = false; + } + b',' if self.depth == 1 => { + self.expect_key = true; + self.error_key = false; + } + _ => {} + } + } + } + + fn inspect_value(&mut self, byte: u8) { + let Some(value) = self.value.take() else { + return; + }; + self.value = match value { + ErrorValue::Start if byte.is_ascii_whitespace() => Some(ErrorValue::Start), + ErrorValue::Start => match byte { + b'"' => Some(ErrorValue::String), + b'[' => Some(ErrorValue::Container(b']')), + b'{' => Some(ErrorValue::Container(b'}')), + b'n' | b'f' | b'0' | b'-' => Some(ErrorValue::Literal(vec![byte])), + _ => { + self.detected = true; + None + } + }, + ErrorValue::String => { + self.detected = byte != b'"'; + None + } + ErrorValue::Container(end) if byte.is_ascii_whitespace() => { + Some(ErrorValue::Container(end)) + } + ErrorValue::Container(end) => { + self.detected = byte != end; + None + } + ErrorValue::Literal(mut bytes) => { + if byte.is_ascii_whitespace() || matches!(byte, b',' | b'}') { + self.detected = literal_is_error(&bytes); + None + } else if bytes.len() < 64 { + bytes.push(byte); + Some(ErrorValue::Literal(bytes)) + } else { + self.detected = true; + None + } + } + }; + } + + pub fn is_error(&self) -> bool { + self.detected + || match &self.value { + Some(ErrorValue::Literal(bytes)) => literal_is_error(bytes), + // An incomplete value of a declared error field is not a successful download. + Some(_) => true, + None => false, + } + } + + pub fn confirmed_error(&self) -> bool { + self.detected + } +} + +fn literal_is_error(bytes: &[u8]) -> bool { + match serde_json::from_slice::<serde_json::Value>(bytes) { + Ok(serde_json::Value::Null | serde_json::Value::Bool(false)) => false, + Ok(serde_json::Value::Number(number)) => number.as_f64() != Some(0.0), + _ => true, + } +} |