summaryrefslogtreecommitdiff
path: root/src/diagnostics
diff options
context:
space:
mode:
authorAndrew Guschin <guschin@altlinux.org>2026-10-30 05:15:56 +0400
committerAndrew Guschin <guschin@altlinux.org>2026-10-30 05:15:56 +0400
commit3c8b130be89fbb0e58e2ee23f07ad56a3b9de2e7 (patch)
treed0e5d447f7324a2793bc6975ed57f172624d6311 /src/diagnostics
parent7999fab25c5172207ee41e38732f4923565ff6b6 (diff)
Reject streamed diagnostic error documents and record VM qualification
Diffstat (limited to 'src/diagnostics')
-rw-r--r--src/diagnostics/error_document.rs170
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,
+ }
+}