use crate::{ContentEncoding, Error, Meta, Result, Stat};
#[derive(serde::Deserialize)]
struct ObjMeta {
name: String,
#[serde(default)]
size: Option<NumStr>,
generation: NumStr,
#[serde(rename = "contentEncoding", default)]
content_encoding: Option<String>,
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum NumStr {
S(String),
N(u64),
}
impl NumStr {
fn get(&self) -> Option<u64> {
match self {
NumStr::S(s) => s.parse().ok(),
NumStr::N(n) => Some(*n),
}
}
}
#[derive(serde::Deserialize)]
struct ListPage {
#[serde(default)]
items: Vec<ObjMeta>,
#[serde(rename = "nextPageToken", default)]
next_page_token: Option<String>,
}
fn to_meta(m: ObjMeta) -> Result<Meta> {
let generation =
m.generation.get().filter(|g| *g != 0).ok_or_else(|| Error::protocol("bad generation"))?;
let size = match m.size {
Some(s) => s.get().ok_or_else(|| Error::protocol("bad size"))?,
None => 0,
};
Ok(Meta { key: m.name, size, generation })
}
fn to_stat(mut m: ObjMeta) -> Result<Stat> {
let content_encoding = match m.content_encoding.take() {
Some(t) => ContentEncoding::parse(&t)?,
None => None,
};
Ok(Stat { meta: to_meta(m)?, content_encoding })
}
pub(crate) fn parse_meta(body: &[u8]) -> Result<Meta> {
let m: ObjMeta =
serde_json::from_slice(body).map_err(|_| Error::protocol("malformed object resource"))?;
to_meta(m)
}
pub(crate) fn parse_stat(body: &[u8]) -> Result<Stat> {
let m: ObjMeta =
serde_json::from_slice(body).map_err(|_| Error::protocol("malformed object resource"))?;
to_stat(m)
}
pub(crate) fn parse_list(body: &[u8]) -> Result<(Vec<Meta>, Option<String>)> {
let p: ListPage =
serde_json::from_slice(body).map_err(|_| Error::protocol("malformed listing"))?;
let items = p.items.into_iter().map(to_meta).collect::<Result<Vec<_>>>()?;
Ok((items, p.next_page_token))
}
pub(crate) fn parse_list_stat(body: &[u8]) -> Result<(Vec<Stat>, Option<String>)> {
let p: ListPage =
serde_json::from_slice(body).map_err(|_| Error::protocol("malformed listing"))?;
let items = p.items.into_iter().map(to_stat).collect::<Result<Vec<_>>>()?;
Ok((items, p.next_page_token))
}
#[doc(hidden)]
pub fn fuzz(body: &[u8]) {
let _ = parse_meta(body);
let _ = parse_list(body);
let _ = parse_stat(body);
let _ = parse_list_stat(body);
let _ = super::crc32c::from_goog_hash(std::str::from_utf8(body).ok());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_real_shapes() {
let m =
parse_meta(br#"{"name":"a/b","size":"12","generation":"1695000000123456"}"#).unwrap();
assert_eq!((m.key.as_str(), m.size, m.generation), ("a/b", 12, 1_695_000_000_123_456));
let m = parse_meta(br#"{"name":"x","size":3,"generation":7}"#).unwrap();
assert_eq!((m.size, m.generation), (3, 7));
let (items, next) = parse_list(
br#"{"items":[{"name":"a","size":"1","generation":"2"}],"nextPageToken":"t"}"#,
)
.unwrap();
assert_eq!((items.len(), next.as_deref()), (1, Some("t")));
let (items, next) = parse_list(b"{}").unwrap();
assert!(items.is_empty() && next.is_none());
let s = parse_stat(br#"{"name":"a","size":"9","generation":"3","contentEncoding":"gzip"}"#)
.unwrap();
assert_eq!(s.content_encoding, Some(ContentEncoding::Gzip));
let s = parse_stat(br#"{"name":"a","generation":"3"}"#).unwrap();
assert_eq!((s.content_encoding, s.meta.size), (None, 0));
let (items, _) = parse_list_stat(
br#"{"items":[{"name":"a","generation":"2","contentEncoding":"identity"},
{"name":"b","generation":"4","contentEncoding":"gzip"}]}"#,
)
.unwrap();
assert_eq!(items[0].content_encoding, None);
assert_eq!(items[1].content_encoding, Some(ContentEncoding::Gzip));
assert!(matches!(
parse_stat(br#"{"name":"a","generation":"3","contentEncoding":"br"}"#),
Err(Error::UnsupportedEncoding { .. })
));
for bad in [
&b""[..],
b"null",
b"[]",
br#"{"name":"a","generation":"0"}"#,
br#"{"name":"a","generation":"x"}"#,
] {
assert!(parse_meta(bad).is_err());
}
}
}