diff --git a/local/recipes/system/cub/source/cub-lib/src/aur.rs b/local/recipes/system/cub/source/cub-lib/src/aur.rs index 370c227e7..885d25806 100644 --- a/local/recipes/system/cub/source/cub-lib/src/aur.rs +++ b/local/recipes/system/cub/source/cub-lib/src/aur.rs @@ -1,27 +1,61 @@ -use std::collections::BTreeMap; +use serde::Deserialize; use crate::error::CubError; const DEFAULT_AUR_BASE_URL: &str = "https://aur.archlinux.org"; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Deserialize)] pub struct AurPackage { + #[serde(rename = "Name")] pub name: String, + #[serde(rename = "Version")] pub version: String, + #[serde(rename = "Description")] + #[serde(default)] pub description: String, + #[serde(rename = "URL")] + #[serde(default)] pub url: String, + #[serde(rename = "License")] + #[serde(default)] pub license: Vec, + #[serde(rename = "Depends")] + #[serde(default)] pub depends: Vec, + #[serde(rename = "MakeDepends")] + #[serde(default)] pub makedepends: Vec, + #[serde(rename = "OptDepends")] + #[serde(default)] pub optdepends: Vec, + #[serde(rename = "Provides")] + #[serde(default)] pub provides: Vec, + #[serde(rename = "Conflicts")] + #[serde(default)] pub conflicts: Vec, + #[serde(rename = "NumVotes")] pub num_votes: u64, + #[serde(rename = "Popularity")] pub popularity: f64, + #[serde(rename = "LastModified")] pub last_modified: i64, + #[serde(rename = "OutOfDate")] + #[serde(default)] pub out_of_date: Option, } +#[derive(Debug, Deserialize)] +struct AurRpcResponse { + version: i64, + #[serde(rename = "type")] + response_type: String, + resultcount: i64, + results: Vec, + #[serde(default)] + error: Option, +} + #[cfg(feature = "full")] pub struct AurClient { pub base_url: String, @@ -153,526 +187,39 @@ fn non_empty_trimmed(value: &str) -> Option<&str> { } fn aur_error(message: impl Into) -> CubError { - CubError::Conversion(format!("AUR: {}", message.into())) + CubError::Aur(message.into()) } #[cfg(not(feature = "full"))] fn feature_not_enabled_error() -> CubError { - CubError::Conversion("reqwest feature not enabled".into()) + CubError::Aur("reqwest feature not enabled".into()) } fn parse_rpc_response(body: &str, empty_message: &str) -> Result, CubError> { - let root = JsonParser::new(body) - .parse() + let rpc: AurRpcResponse = serde_json::from_str(body) .map_err(|err| aur_error(format!("failed to parse JSON response: {err}")))?; - let object = match root { - JsonValue::Object(object) => object, - _ => { - return Err(aur_error( - "invalid JSON response: expected top-level object", - )) - } - }; - - let version = require_i64(&object, "version")?; - if version != 5 { - return Err(aur_error(format!("unexpected RPC version: {version}"))); + if rpc.version != 5 { + return Err(aur_error(format!("unexpected RPC version: {}", rpc.version))); } - let response_type = require_string(&object, "type")?; - if response_type == "error" { - let message = - optional_string(&object, "error")?.unwrap_or_else(|| "unknown AUR error".to_string()); + if rpc.response_type == "error" { + let message = rpc.error.unwrap_or_else(|| "unknown AUR error".to_string()); return Err(aur_error(message)); } - if response_type != "search" && response_type != "multiinfo" && response_type != "info" { + if rpc.response_type != "search" && rpc.response_type != "multiinfo" && rpc.response_type != "info" { return Err(aur_error(format!( - "unexpected RPC response type: {response_type}" + "unexpected RPC response type: {}", + rpc.response_type ))); } - let resultcount = require_i64(&object, "resultcount")?; - let results = require_array(&object, "results")?; - - if resultcount <= 0 || results.is_empty() { + if rpc.resultcount <= 0 || rpc.results.is_empty() { return Err(aur_error(empty_message.to_string())); } - let mut packages = Vec::with_capacity(results.len()); - for value in results { - packages.push(AurPackage::from_json_value(value)?); - } - - if packages.is_empty() { - return Err(aur_error(empty_message.to_string())); - } - - Ok(packages) -} - -impl AurPackage { - fn from_json_value(value: &JsonValue) -> Result { - let object = match value { - JsonValue::Object(object) => object, - _ => return Err(aur_error("invalid package entry: expected object")), - }; - - Ok(Self { - name: require_string(object, "Name")?, - version: require_string(object, "Version")?, - description: optional_string(object, "Description")?.unwrap_or_default(), - url: optional_string(object, "URL")?.unwrap_or_default(), - license: optional_string_list(object, "License")?, - depends: optional_string_list(object, "Depends")?, - makedepends: optional_string_list(object, "MakeDepends")?, - optdepends: optional_string_list(object, "OptDepends")?, - provides: optional_string_list(object, "Provides")?, - conflicts: optional_string_list(object, "Conflicts")?, - num_votes: optional_u64(object, "NumVotes")?.unwrap_or(0), - popularity: optional_f64(object, "Popularity")?.unwrap_or(0.0), - last_modified: optional_i64(object, "LastModified")?.unwrap_or(0), - out_of_date: optional_out_of_date(object, "OutOfDate")?, - }) - } -} - -fn require_string(object: &BTreeMap, key: &str) -> Result { - optional_string(object, key)?.ok_or_else(|| aur_error(format!("missing field '{key}'"))) -} - -fn optional_string( - object: &BTreeMap, - key: &str, -) -> Result, CubError> { - match object.get(key) { - None | Some(JsonValue::Null) => Ok(None), - Some(JsonValue::String(value)) => Ok(Some(value.clone())), - Some(_) => Err(aur_error(format!("invalid field '{key}': expected string"))), - } -} - -fn require_array<'a>( - object: &'a BTreeMap, - key: &str, -) -> Result<&'a [JsonValue], CubError> { - match object.get(key) { - Some(JsonValue::Array(values)) => Ok(values.as_slice()), - Some(_) => Err(aur_error(format!("invalid field '{key}': expected array"))), - None => Err(aur_error(format!("missing field '{key}'"))), - } -} - -fn optional_string_list( - object: &BTreeMap, - key: &str, -) -> Result, CubError> { - match object.get(key) { - None | Some(JsonValue::Null) => Ok(Vec::new()), - Some(JsonValue::String(value)) => Ok(vec![value.clone()]), - Some(JsonValue::Array(values)) => { - let mut items = Vec::with_capacity(values.len()); - for value in values { - match value { - JsonValue::String(item) => items.push(item.clone()), - JsonValue::Null => {} - _ => { - return Err(aur_error(format!( - "invalid field '{key}': expected string array" - ))) - } - } - } - Ok(items) - } - Some(_) => Err(aur_error(format!( - "invalid field '{key}': expected string array" - ))), - } -} - -fn require_i64(object: &BTreeMap, key: &str) -> Result { - optional_i64(object, key)?.ok_or_else(|| aur_error(format!("missing field '{key}'"))) -} - -fn optional_i64(object: &BTreeMap, key: &str) -> Result, CubError> { - match object.get(key) { - None | Some(JsonValue::Null) => Ok(None), - Some(JsonValue::Integer(value)) => Ok(Some(*value)), - Some(_) => Err(aur_error(format!( - "invalid field '{key}': expected integer" - ))), - } -} - -fn optional_u64(object: &BTreeMap, key: &str) -> Result, CubError> { - match object.get(key) { - None | Some(JsonValue::Null) => Ok(None), - Some(JsonValue::Integer(value)) if *value >= 0 => Ok(Some(*value as u64)), - Some(_) => Err(aur_error(format!( - "invalid field '{key}': expected unsigned integer" - ))), - } -} - -fn optional_f64(object: &BTreeMap, key: &str) -> Result, CubError> { - match object.get(key) { - None | Some(JsonValue::Null) => Ok(None), - Some(JsonValue::Integer(value)) => Ok(Some(*value as f64)), - Some(JsonValue::Float(value)) => Ok(Some(*value)), - Some(_) => Err(aur_error(format!("invalid field '{key}': expected number"))), - } -} - -fn optional_out_of_date( - object: &BTreeMap, - key: &str, -) -> Result, CubError> { - match object.get(key) { - None | Some(JsonValue::Null) => Ok(None), - Some(JsonValue::Bool(value)) => Ok(Some(*value)), - Some(JsonValue::Integer(value)) => Ok(Some(*value != 0)), - Some(JsonValue::Float(value)) => Ok(Some(*value != 0.0)), - Some(_) => Err(aur_error(format!( - "invalid field '{key}': expected bool or number" - ))), - } -} - -#[derive(Debug, Clone, PartialEq)] -enum JsonValue { - Null, - Bool(bool), - Integer(i64), - Float(f64), - String(String), - Array(Vec), - Object(BTreeMap), -} - -struct JsonParser<'a> { - input: &'a [u8], - position: usize, -} - -impl<'a> JsonParser<'a> { - fn new(input: &'a str) -> Self { - Self { - input: input.as_bytes(), - position: 0, - } - } - - fn parse(mut self) -> Result { - let value = self.parse_value()?; - self.skip_whitespace(); - if self.position != self.input.len() { - return Err(format!( - "unexpected trailing content at byte {}", - self.position - )); - } - Ok(value) - } - - fn parse_value(&mut self) -> Result { - self.skip_whitespace(); - match self.peek_byte() { - Some(b'{') => self.parse_object(), - Some(b'[') => self.parse_array(), - Some(b'"') => self.parse_string().map(JsonValue::String), - Some(b't') => self.parse_true(), - Some(b'f') => self.parse_false(), - Some(b'n') => self.parse_null(), - Some(b'-' | b'0'..=b'9') => self.parse_number(), - Some(byte) => Err(format!( - "unexpected byte '{}' at {}", - byte as char, self.position - )), - None => Err("unexpected end of input".to_string()), - } - } - - fn parse_object(&mut self) -> Result { - self.expect_byte(b'{')?; - self.skip_whitespace(); - - let mut object = BTreeMap::new(); - if self.peek_byte() == Some(b'}') { - self.position += 1; - return Ok(JsonValue::Object(object)); - } - - loop { - self.skip_whitespace(); - let key = self.parse_string()?; - self.skip_whitespace(); - self.expect_byte(b':')?; - let value = self.parse_value()?; - object.insert(key, value); - self.skip_whitespace(); - - match self.peek_byte() { - Some(b',') => { - self.position += 1; - } - Some(b'}') => { - self.position += 1; - break; - } - Some(byte) => { - return Err(format!( - "unexpected byte '{}' in object at {}", - byte as char, self.position - )) - } - None => return Err("unexpected end of input while parsing object".to_string()), - } - } - - Ok(JsonValue::Object(object)) - } - - fn parse_array(&mut self) -> Result { - self.expect_byte(b'[')?; - self.skip_whitespace(); - - let mut values = Vec::new(); - if self.peek_byte() == Some(b']') { - self.position += 1; - return Ok(JsonValue::Array(values)); - } - - loop { - values.push(self.parse_value()?); - self.skip_whitespace(); - match self.peek_byte() { - Some(b',') => { - self.position += 1; - } - Some(b']') => { - self.position += 1; - break; - } - Some(byte) => { - return Err(format!( - "unexpected byte '{}' in array at {}", - byte as char, self.position - )) - } - None => return Err("unexpected end of input while parsing array".to_string()), - } - } - - Ok(JsonValue::Array(values)) - } - - fn parse_string(&mut self) -> Result { - self.expect_byte(b'"')?; - let mut value = String::new(); - - loop { - match self.peek_byte() { - Some(b'"') => { - self.position += 1; - return Ok(value); - } - Some(b'\\') => { - self.position += 1; - value.push(self.parse_escape_sequence()?); - } - Some(0x00..=0x1F) => { - return Err(format!( - "invalid control character in string at {}", - self.position - )) - } - Some(_) => value.push_str(&self.parse_raw_string_segment()?), - None => break, - } - } - - Err("unexpected end of input while parsing string".to_string()) - } - - fn parse_raw_string_segment(&mut self) -> Result { - let start = self.position; - while let Some(byte) = self.peek_byte() { - if matches!(byte, b'"' | b'\\' | 0x00..=0x1F) { - break; - } - self.position += 1; - } - - let segment = std::str::from_utf8(&self.input[start..self.position]) - .map_err(|err| format!("invalid UTF-8 in string: {err}"))?; - Ok(segment.to_string()) - } - - fn parse_escape_sequence(&mut self) -> Result { - match self.next_byte() { - Some(b'"') => Ok('"'), - Some(b'\\') => Ok('\\'), - Some(b'/') => Ok('/'), - Some(b'b') => Ok('\u{0008}'), - Some(b'f') => Ok('\u{000C}'), - Some(b'n') => Ok('\n'), - Some(b'r') => Ok('\r'), - Some(b't') => Ok('\t'), - Some(b'u') => self.parse_unicode_escape(), - Some(byte) => Err(format!( - "invalid escape byte '{}' at {}", - byte as char, - self.position.saturating_sub(1) - )), - None => Err("unexpected end of input while parsing escape sequence".to_string()), - } - } - - fn parse_unicode_escape(&mut self) -> Result { - let start = self.position; - let mut value = 0u32; - - for _ in 0..4 { - let byte = self.next_byte().ok_or_else(|| { - "unexpected end of input while parsing unicode escape".to_string() - })?; - value = (value << 4) - | match byte { - b'0'..=b'9' => (byte - b'0') as u32, - b'a'..=b'f' => (byte - b'a' + 10) as u32, - b'A'..=b'F' => (byte - b'A' + 10) as u32, - _ => { - return Err(format!( - "invalid unicode escape at byte {}", - start.saturating_sub(2) - )) - } - }; - } - - char::from_u32(value).ok_or_else(|| format!("invalid unicode scalar value: {value:#X}")) - } - - fn parse_true(&mut self) -> Result { - self.expect_keyword(b"true")?; - Ok(JsonValue::Bool(true)) - } - - fn parse_false(&mut self) -> Result { - self.expect_keyword(b"false")?; - Ok(JsonValue::Bool(false)) - } - - fn parse_null(&mut self) -> Result { - self.expect_keyword(b"null")?; - Ok(JsonValue::Null) - } - - fn parse_number(&mut self) -> Result { - let start = self.position; - - if self.peek_byte() == Some(b'-') { - self.position += 1; - } - - self.parse_digits()?; - - if self.peek_byte() == Some(b'.') { - self.position += 1; - self.parse_digits()?; - } - - if matches!(self.peek_byte(), Some(b'e' | b'E')) { - self.position += 1; - if matches!(self.peek_byte(), Some(b'+' | b'-')) { - self.position += 1; - } - self.parse_digits()?; - } - - let slice = std::str::from_utf8(&self.input[start..self.position]) - .map_err(|err| format!("invalid UTF-8 in number: {err}"))?; - - if slice - .as_bytes() - .iter() - .any(|byte| matches!(byte, b'.' | b'e' | b'E')) - { - slice - .parse::() - .map(JsonValue::Float) - .map_err(|err| format!("invalid number '{slice}': {err}")) - } else { - slice - .parse::() - .map(JsonValue::Integer) - .map_err(|err| format!("invalid number '{slice}': {err}")) - } - } - - fn parse_digits(&mut self) -> Result<(), String> { - let start = self.position; - while matches!(self.peek_byte(), Some(b'0'..=b'9')) { - self.position += 1; - } - - if self.position == start { - Err(format!("expected digit at byte {}", self.position)) - } else { - Ok(()) - } - } - - fn expect_keyword(&mut self, keyword: &[u8]) -> Result<(), String> { - for expected in keyword { - match self.next_byte() { - Some(byte) if byte == *expected => {} - Some(byte) => { - return Err(format!( - "unexpected byte '{}' while parsing keyword at {}", - byte as char, - self.position.saturating_sub(1) - )) - } - None => return Err("unexpected end of input while parsing keyword".to_string()), - } - } - Ok(()) - } - - fn skip_whitespace(&mut self) { - while matches!(self.peek_byte(), Some(b' ' | b'\n' | b'\r' | b'\t')) { - self.position += 1; - } - } - - fn expect_byte(&mut self, expected: u8) -> Result<(), String> { - match self.next_byte() { - Some(byte) if byte == expected => Ok(()), - Some(byte) => Err(format!( - "expected '{}' but found '{}' at {}", - expected as char, - byte as char, - self.position.saturating_sub(1) - )), - None => Err(format!( - "expected '{}' but reached end of input", - expected as char - )), - } - } - - fn peek_byte(&self) -> Option { - self.input.get(self.position).copied() - } - - fn next_byte(&mut self) -> Option { - let byte = self.peek_byte()?; - self.position += 1; - Some(byte) - } + Ok(rpc.results) } #[cfg(test)]