fix: parse Cub AUR responses with serde
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,27 +1,61 @@
|
|||||||
use std::collections::BTreeMap;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::error::CubError;
|
use crate::error::CubError;
|
||||||
|
|
||||||
const DEFAULT_AUR_BASE_URL: &str = "https://aur.archlinux.org";
|
const DEFAULT_AUR_BASE_URL: &str = "https://aur.archlinux.org";
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
pub struct AurPackage {
|
pub struct AurPackage {
|
||||||
|
#[serde(rename = "Name")]
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
#[serde(rename = "Version")]
|
||||||
pub version: String,
|
pub version: String,
|
||||||
|
#[serde(rename = "Description")]
|
||||||
|
#[serde(default)]
|
||||||
pub description: String,
|
pub description: String,
|
||||||
|
#[serde(rename = "URL")]
|
||||||
|
#[serde(default)]
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
#[serde(rename = "License")]
|
||||||
|
#[serde(default)]
|
||||||
pub license: Vec<String>,
|
pub license: Vec<String>,
|
||||||
|
#[serde(rename = "Depends")]
|
||||||
|
#[serde(default)]
|
||||||
pub depends: Vec<String>,
|
pub depends: Vec<String>,
|
||||||
|
#[serde(rename = "MakeDepends")]
|
||||||
|
#[serde(default)]
|
||||||
pub makedepends: Vec<String>,
|
pub makedepends: Vec<String>,
|
||||||
|
#[serde(rename = "OptDepends")]
|
||||||
|
#[serde(default)]
|
||||||
pub optdepends: Vec<String>,
|
pub optdepends: Vec<String>,
|
||||||
|
#[serde(rename = "Provides")]
|
||||||
|
#[serde(default)]
|
||||||
pub provides: Vec<String>,
|
pub provides: Vec<String>,
|
||||||
|
#[serde(rename = "Conflicts")]
|
||||||
|
#[serde(default)]
|
||||||
pub conflicts: Vec<String>,
|
pub conflicts: Vec<String>,
|
||||||
|
#[serde(rename = "NumVotes")]
|
||||||
pub num_votes: u64,
|
pub num_votes: u64,
|
||||||
|
#[serde(rename = "Popularity")]
|
||||||
pub popularity: f64,
|
pub popularity: f64,
|
||||||
|
#[serde(rename = "LastModified")]
|
||||||
pub last_modified: i64,
|
pub last_modified: i64,
|
||||||
|
#[serde(rename = "OutOfDate")]
|
||||||
|
#[serde(default)]
|
||||||
pub out_of_date: Option<bool>,
|
pub out_of_date: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct AurRpcResponse {
|
||||||
|
version: i64,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
response_type: String,
|
||||||
|
resultcount: i64,
|
||||||
|
results: Vec<AurPackage>,
|
||||||
|
#[serde(default)]
|
||||||
|
error: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
pub struct AurClient {
|
pub struct AurClient {
|
||||||
pub base_url: String,
|
pub base_url: String,
|
||||||
@@ -153,526 +187,39 @@ fn non_empty_trimmed(value: &str) -> Option<&str> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn aur_error(message: impl Into<String>) -> CubError {
|
fn aur_error(message: impl Into<String>) -> CubError {
|
||||||
CubError::Conversion(format!("AUR: {}", message.into()))
|
CubError::Aur(message.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "full"))]
|
#[cfg(not(feature = "full"))]
|
||||||
fn feature_not_enabled_error() -> CubError {
|
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<Vec<AurPackage>, CubError> {
|
fn parse_rpc_response(body: &str, empty_message: &str) -> Result<Vec<AurPackage>, CubError> {
|
||||||
let root = JsonParser::new(body)
|
let rpc: AurRpcResponse = serde_json::from_str(body)
|
||||||
.parse()
|
|
||||||
.map_err(|err| aur_error(format!("failed to parse JSON response: {err}")))?;
|
.map_err(|err| aur_error(format!("failed to parse JSON response: {err}")))?;
|
||||||
|
|
||||||
let object = match root {
|
if rpc.version != 5 {
|
||||||
JsonValue::Object(object) => object,
|
return Err(aur_error(format!("unexpected RPC version: {}", rpc.version)));
|
||||||
_ => {
|
|
||||||
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}")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let response_type = require_string(&object, "type")?;
|
if rpc.response_type == "error" {
|
||||||
if response_type == "error" {
|
let message = rpc.error.unwrap_or_else(|| "unknown AUR error".to_string());
|
||||||
let message =
|
|
||||||
optional_string(&object, "error")?.unwrap_or_else(|| "unknown AUR error".to_string());
|
|
||||||
return Err(aur_error(message));
|
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!(
|
return Err(aur_error(format!(
|
||||||
"unexpected RPC response type: {response_type}"
|
"unexpected RPC response type: {}",
|
||||||
|
rpc.response_type
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let resultcount = require_i64(&object, "resultcount")?;
|
if rpc.resultcount <= 0 || rpc.results.is_empty() {
|
||||||
let results = require_array(&object, "results")?;
|
|
||||||
|
|
||||||
if resultcount <= 0 || results.is_empty() {
|
|
||||||
return Err(aur_error(empty_message.to_string()));
|
return Err(aur_error(empty_message.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut packages = Vec::with_capacity(results.len());
|
Ok(rpc.results)
|
||||||
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<Self, CubError> {
|
|
||||||
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<String, JsonValue>, key: &str) -> Result<String, CubError> {
|
|
||||||
optional_string(object, key)?.ok_or_else(|| aur_error(format!("missing field '{key}'")))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn optional_string(
|
|
||||||
object: &BTreeMap<String, JsonValue>,
|
|
||||||
key: &str,
|
|
||||||
) -> Result<Option<String>, 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<String, JsonValue>,
|
|
||||||
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<String, JsonValue>,
|
|
||||||
key: &str,
|
|
||||||
) -> Result<Vec<String>, 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<String, JsonValue>, key: &str) -> Result<i64, CubError> {
|
|
||||||
optional_i64(object, key)?.ok_or_else(|| aur_error(format!("missing field '{key}'")))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn optional_i64(object: &BTreeMap<String, JsonValue>, key: &str) -> Result<Option<i64>, 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<String, JsonValue>, key: &str) -> Result<Option<u64>, 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<String, JsonValue>, key: &str) -> Result<Option<f64>, 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<String, JsonValue>,
|
|
||||||
key: &str,
|
|
||||||
) -> Result<Option<bool>, 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<JsonValue>),
|
|
||||||
Object(BTreeMap<String, JsonValue>),
|
|
||||||
}
|
|
||||||
|
|
||||||
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<JsonValue, String> {
|
|
||||||
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<JsonValue, String> {
|
|
||||||
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<JsonValue, String> {
|
|
||||||
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<JsonValue, String> {
|
|
||||||
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<String, String> {
|
|
||||||
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<String, String> {
|
|
||||||
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<char, String> {
|
|
||||||
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<char, String> {
|
|
||||||
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<JsonValue, String> {
|
|
||||||
self.expect_keyword(b"true")?;
|
|
||||||
Ok(JsonValue::Bool(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_false(&mut self) -> Result<JsonValue, String> {
|
|
||||||
self.expect_keyword(b"false")?;
|
|
||||||
Ok(JsonValue::Bool(false))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_null(&mut self) -> Result<JsonValue, String> {
|
|
||||||
self.expect_keyword(b"null")?;
|
|
||||||
Ok(JsonValue::Null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_number(&mut self) -> Result<JsonValue, String> {
|
|
||||||
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::<f64>()
|
|
||||||
.map(JsonValue::Float)
|
|
||||||
.map_err(|err| format!("invalid number '{slice}': {err}"))
|
|
||||||
} else {
|
|
||||||
slice
|
|
||||||
.parse::<i64>()
|
|
||||||
.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<u8> {
|
|
||||||
self.input.get(self.position).copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_byte(&mut self) -> Option<u8> {
|
|
||||||
let byte = self.peek_byte()?;
|
|
||||||
self.position += 1;
|
|
||||||
Some(byte)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
Reference in New Issue
Block a user