diff --git a/proxmox-schema/src/de/cow3.rs b/proxmox-schema/src/de/cow3.rs new file mode 100644 index 00000000..30446818 --- /dev/null +++ b/proxmox-schema/src/de/cow3.rs @@ -0,0 +1,171 @@ +use std::borrow::{Borrow, Cow}; +use std::fmt; +use std::ops::Range; + +/// Manage 2 lifetimes for deserializing. +/// +/// When deserializing from a value it is considered to have lifetime `'de`. Any value that doesn't +/// need to live longer than the deserialized *input* can *borrow* from that lifetime. +/// +/// For example, from the `String` `{ "hello": "you" }` you can deserialize a `HashMap<&'de str, +/// &'de str>`, as long as that map only exists as long as the original string. +/// +/// However, if the data is `{ "hello": "\"hello\"" }`, then the value string needs to be +/// unescaped, and can only be owned. However, if you only need it *temporarily*, eg. to parse a +/// property string of numbers, you may want to avoid cloning individual parts from that. +/// +/// Due to implementation details (particularly not wanting to provide a `Cow` version of +/// `PropertyIterator`), we may need to be able to hold references to such intermediate values. +/// +/// For the above scenario, `'o` would be the original `'de` lifetime, and `'i` the intermediate +/// lifetime for the unescaped string. +/// +/// Finally we also have an "Owned" value as a 3rd option. +pub enum Cow3<'o, 'i, B> +where + B: 'o + 'i + ToOwned + ?Sized, +{ + /// Original lifetime from the deserialization entry point. + Original(&'o B), + + /// Borrowed from an intermediate value. + Intermediate(&'i B), + + /// Owned data. + Owned(::Owned), +} + +impl<'o, 'i, B> Cow3<'o, 'i, B> +where + B: 'o + 'i + ToOwned + ?Sized, +{ + /// From a `Cow` with the original lifetime. + pub fn from_original(value: T) -> Self + where + T: Into>, + { + match value.into() { + Cow::Borrowed(v) => Self::Original(v), + Cow::Owned(v) => Self::Owned(v), + } + } + + /// From a `Cow` with the intermediate lifetime. + pub fn from_intermediate(value: T) -> Self + where + T: Into>, + { + match value.into() { + Cow::Borrowed(v) => Self::Intermediate(v), + Cow::Owned(v) => Self::Owned(v), + } + } + + /// Turn into a `Cow`, forcing intermediate values to become owned. + pub fn into_original_or_owned(self) -> Cow<'o, B> { + match self { + Self::Original(v) => Cow::Borrowed(v), + Self::Intermediate(v) => Cow::Owned(v.to_owned()), + Self::Owned(v) => Cow::Owned(v), + } + } +} + +impl<'o, 'i, B> std::ops::Deref for Cow3<'o, 'i, B> +where + B: 'o + 'i + ToOwned + ?Sized, + ::Owned: Borrow, +{ + type Target = B; + + fn deref(&self) -> &B { + match self { + Self::Original(v) => v, + Self::Intermediate(v) => v, + Self::Owned(v) => v.borrow(), + } + } +} + +impl<'o, 'i, B> AsRef for Cow3<'o, 'i, B> +where + B: 'o + 'i + ToOwned + ?Sized, + ::Owned: Borrow, +{ + fn as_ref(&self) -> &B { + &self + } +} + +/// Build a `Cow3` with a value surviving the `'o` lifetime. +impl<'x, 'o, 'i, B> From<&'x B> for Cow3<'o, 'i, B> +where + B: 'o + 'i + ToOwned + ?Sized, + ::Owned: Borrow, + 'x: 'o, +{ + fn from(value: &'x B) -> Self { + Self::Original(value) + } +} + +impl fmt::Display for Cow3<'_, '_, B> +where + B: fmt::Display + ToOwned, + B::Owned: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::Original(ref b) => fmt::Display::fmt(b, f), + Self::Intermediate(ref b) => fmt::Display::fmt(b, f), + Self::Owned(ref o) => fmt::Display::fmt(o, f), + } + } +} + +impl fmt::Debug for Cow3<'_, '_, B> +where + B: fmt::Debug + ToOwned, + B::Owned: fmt::Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + Self::Original(ref b) => fmt::Debug::fmt(b, f), + Self::Intermediate(ref b) => fmt::Debug::fmt(b, f), + Self::Owned(ref o) => fmt::Debug::fmt(o, f), + } + } +} + +impl<'o, 'i> Cow3<'o, 'i, str> { + /// Index value as a borrowed value. + pub fn slice<'ni, I>(&'ni self, index: I) -> Cow3<'o, 'ni, str> + where + I: std::slice::SliceIndex, + 'i: 'ni, + { + match self { + Self::Original(value) => Cow3::Original(&value[index]), + Self::Intermediate(value) => Cow3::Intermediate(&value[index]), + Self::Owned(value) => Cow3::Intermediate(&value.as_str()[index]), + } + } +} + +pub fn str_slice_to_range(original: &str, slice: &str) -> Option> { + let bytes = original.as_bytes(); + + let orig_addr = bytes.as_ptr() as usize; + let slice_addr = slice.as_bytes().as_ptr() as usize; + let offset = slice_addr.checked_sub(orig_addr)?; + if offset > orig_addr + bytes.len() { + return None; + } + + let end = offset + slice.as_bytes().len(); + if end > orig_addr + bytes.len() { + return None; + } + + Some(offset..end) +} diff --git a/proxmox-schema/src/de.rs b/proxmox-schema/src/de/extract.rs similarity index 100% rename from proxmox-schema/src/de.rs rename to proxmox-schema/src/de/extract.rs diff --git a/proxmox-schema/src/de/mod.rs b/proxmox-schema/src/de/mod.rs new file mode 100644 index 00000000..25efb420 --- /dev/null +++ b/proxmox-schema/src/de/mod.rs @@ -0,0 +1,624 @@ +//! Property string deserialization. + +use std::borrow::Cow; +use std::fmt; +use std::ops::Range; + +use serde::de::{self, IntoDeserializer}; + +use crate::schema::{self, ArraySchema, Schema}; + +mod cow3; +mod extract; +mod no_schema; + +pub mod verify; + +pub use extract::ExtractValueDeserializer; + +use cow3::{str_slice_to_range, Cow3}; + +#[derive(Debug)] +pub struct Error(Cow<'static, str>); + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl Error { + pub(crate) fn msg>>(msg: T) -> Self { + Self(msg.into()) + } + + fn invalid(msg: T) -> Self { + Self::msg(format!("schema validation failed: {}", msg)) + } +} + +impl serde::de::Error for Error { + fn custom(msg: T) -> Self { + Self(msg.to_string().into()) + } +} + +impl From for Error { + fn from(error: serde_json::Error) -> Self { + Self(error.to_string().into()) + } +} + +impl From for Error { + fn from(err: fmt::Error) -> Self { + Self::msg(err.to_string()) + } +} + +/// Deserializer for parts a part of a property string given a schema. +pub struct SchemaDeserializer<'de, 'i> { + input: Cow3<'de, 'i, str>, + schema: &'static Schema, +} + +impl<'de, 'i> SchemaDeserializer<'de, 'i> { + pub fn new_cow(input: Cow3<'de, 'i, str>, schema: &'static Schema) -> Self { + Self { input, schema } + } + + pub fn new(input: T, schema: &'static Schema) -> Self + where + T: Into>, + { + Self { + input: Cow3::from_original(input.into()), + schema, + } + } + + fn deserialize_str( + self, + visitor: V, + schema: &'static schema::StringSchema, + ) -> Result + where + V: de::Visitor<'de>, + { + schema + .check_constraints(&self.input) + .map_err(|err| Error::invalid(err))?; + match self.input { + Cow3::Original(input) => visitor.visit_borrowed_str(input), + Cow3::Intermediate(input) => visitor.visit_str(input), + Cow3::Owned(input) => visitor.visit_string(input), + } + } + + fn deserialize_property_string( + self, + visitor: V, + schema: &'static Schema, + ) -> Result + where + V: de::Visitor<'de>, + { + match schema { + Schema::Object(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)), + Schema::AllOf(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)), + _ => Err(Error::msg( + "non-object-like schema in ApiStringFormat::PropertyString while deserializing a property string", + )), + } + } + + fn deserialize_array_string( + self, + visitor: V, + schema: &'static Schema, + ) -> Result + where + V: de::Visitor<'de>, + { + match schema { + Schema::Array(schema) => visitor.visit_seq(SeqAccess::new(self.input, schema)), + _ => Err(Error::msg( + "non-array schema in ApiStringFormat::PropertyString while deserializing an array", + )), + } + } +} + +impl<'de, 'i> de::Deserializer<'de> for SchemaDeserializer<'de, 'i> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.schema { + Schema::Array(schema) => visitor.visit_seq(SeqAccess::new(self.input, schema)), + Schema::AllOf(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)), + Schema::Object(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)), + Schema::Null => Err(Error::msg("null")), + Schema::Boolean(_) => visitor.visit_bool( + schema::parse_boolean(&self.input) + .map_err(|_| Error::msg(format!("not a boolean: {:?}", self.input)))?, + ), + Schema::Integer(schema) => { + // FIXME: isize vs explicit i64, needs fixing in schema check_constraints api + let value: isize = self + .input + .parse() + .map_err(|_| Error::msg(format!("not an integer: {:?}", self.input)))?; + + schema + .check_constraints(value) + .map_err(|err| Error::invalid(err))?; + + let value: i64 = i64::try_from(value) + .map_err(|_| Error::invalid("isize did not fit into i64"))?; + + if let Ok(value) = u64::try_from(value) { + visitor.visit_u64(value) + } else { + visitor.visit_i64(value) + } + } + Schema::Number(schema) => { + let value: f64 = self + .input + .parse() + .map_err(|_| Error::msg(format!("not a valid number: {:?}", self.input)))?; + + schema + .check_constraints(value) + .map_err(|err| Error::invalid(err))?; + + visitor.visit_f64(value) + } + Schema::String(schema) => { + // If not requested differently, strings stay strings, otherwise deserializing to a + // `Value` will get objects here instead of strings, which we do not expect + // anywhere. + self.deserialize_str(visitor, schema) + } + } + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if self.input.is_empty() { + visitor.visit_none() + } else { + visitor.visit_some(self) + } + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.schema { + Schema::String(schema) => self.deserialize_str(visitor, schema), + _ => Err(Error::msg( + "tried to deserialize a string with a non-string-schema", + )), + } + } + + fn deserialize_string(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.schema { + Schema::String(schema) => self.deserialize_str(visitor, schema), + _ => Err(Error::msg( + "tried to deserialize a string with a non-string-schema", + )), + } + } + + fn deserialize_newtype_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_struct( + self, + name: &'static str, + _fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + match self.schema { + Schema::Object(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)), + Schema::AllOf(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)), + Schema::String(schema) => match schema.format { + Some(schema::ApiStringFormat::PropertyString(schema)) => { + self.deserialize_property_string(visitor, schema) + } + _ => Err(Error::msg(format!( + "cannot deserialize struct '{}' with a string schema", + name + ))), + }, + _ => Err(Error::msg(format!( + "cannot deserialize struct '{}' with non-object schema", + name, + ))), + } + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.schema { + Schema::Object(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)), + Schema::AllOf(schema) => visitor.visit_map(MapAccess::new_cow(self.input, schema)), + Schema::String(schema) => match schema.format { + Some(schema::ApiStringFormat::PropertyString(schema)) => { + self.deserialize_property_string(visitor, schema) + } + _ => Err(Error::msg(format!( + "cannot deserialize map with a string schema", + ))), + }, + _ => Err(Error::msg(format!( + "cannot deserialize map with non-object schema", + ))), + } + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.schema { + Schema::Array(schema) => visitor.visit_seq(SeqAccess::new(self.input, schema)), + Schema::String(schema) => match schema.format { + Some(schema::ApiStringFormat::PropertyString(schema)) => { + self.deserialize_array_string(visitor, schema) + } + _ => Err(Error::msg("cannot deserialize array with a string schema")), + }, + _ => Err(Error::msg( + "cannot deserialize array with non-object schema", + )), + } + } + + fn deserialize_enum( + self, + name: &'static str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + match self.schema { + Schema::String(_) => visitor.visit_enum(self.input.into_deserializer()), + _ => Err(Error::msg(format!( + "cannot deserialize enum '{}' with non-string schema", + name, + ))), + } + } + + serde::forward_to_deserialize_any! { + i8 i16 i32 i64 + u8 u16 u32 u64 + f32 f64 + bool + char + bytes byte_buf + unit unit_struct + tuple tuple_struct + identifier + ignored_any + } +} + +fn next_str_entry(input: &str, at: &mut usize, has_null: bool) -> Option> { + while *at != input.len() { + let begin = *at; + + let part = &input[*at..]; + + let part_end = if has_null { + part.find('\0') + } else { + part.find(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c)) + }; + + let end = match part_end { + None => { + *at = input.len(); + input.len() + } + Some(rel_end) => { + *at += rel_end + 1; + begin + rel_end + } + }; + + if input[..end].is_empty() { + continue; + } + + return Some(begin..end); + } + + None +} + +/// Parse an array with a schema. +/// +/// Provides both `SeqAccess` and `Deserializer` implementations. +pub struct SeqAccess<'o, 'i, 's> { + schema: &'s ArraySchema, + was_empty: bool, + input: Cow3<'o, 'i, str>, + has_null: bool, + at: usize, + count: usize, +} + +impl<'o, 'i, 's> SeqAccess<'o, 'i, 's> { + pub fn new(input: Cow3<'o, 'i, str>, schema: &'s ArraySchema) -> Self { + Self { + schema, + was_empty: input.is_empty(), + has_null: input.contains('\0'), + input, + at: 0, + count: 0, + } + } +} + +impl<'de, 'i, 's> de::SeqAccess<'de> for SeqAccess<'de, 'i, 's> { + type Error = Error; + + fn next_element_seed(&mut self, seed: T) -> Result, Error> + where + T: de::DeserializeSeed<'de>, + { + if self.was_empty { + return Ok(None); + } + + while let Some(el_range) = next_str_entry(&self.input, &mut self.at, self.has_null) { + if el_range.is_empty() { + continue; + } + + if let Some(max) = self.schema.max_length { + if self.count == max { + return Err(Error::msg("too many elements")); + } + } + + self.count += 1; + + return seed + .deserialize(SchemaDeserializer::new_cow( + self.input.slice(el_range), + self.schema.items, + )) + .map(Some); + } + + if let Some(min) = self.schema.min_length { + if self.count < min { + return Err(Error::msg("not enough elements")); + } + } + + Ok(None) + } +} + +impl<'de, 'i, 's> de::Deserializer<'de> for SeqAccess<'de, 'i, 's> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_seq(self) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if self.was_empty { + visitor.visit_none() + } else { + visitor.visit_some(self) + } + } + + serde::forward_to_deserialize_any! { + i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 + bool char str string + bytes byte_buf + unit unit_struct + newtype_struct + tuple tuple_struct + enum map seq + struct + identifier ignored_any + } +} + +/// Provides serde's `MapAccess` for parsing a property string. +pub struct MapAccess<'de, 'i> { + // The property string iterator and quoted string handler. + input: Cow3<'de, 'i, str>, + input_at: usize, // for when using `Cow3::Owned`. + + /// As a `Deserializer` we want to be able to handle `deserialize_option` and need to know + /// whether this was an empty string. + was_empty: bool, + + /// The schema used to verify the contents and distinguish between structs and property + /// strings. + schema: &'static dyn schema::ObjectSchemaType, + + /// The current next value's key, value and schema (if available). + value: Option<(Cow<'de, str>, Cow<'de, str>, Option<&'static Schema>)>, +} + +impl<'de, 'i> MapAccess<'de, 'i> { + pub fn new(input: &'de str, schema: &'static S) -> Self { + Self { + was_empty: input.is_empty(), + input: Cow3::Original(input), + schema, + input_at: 0, + value: None, + } + } + + pub fn new_cow( + input: Cow3<'de, 'i, str>, + schema: &'static S, + ) -> Self { + Self { + was_empty: input.is_empty(), + input, + schema, + input_at: 0, + value: None, + } + } + + pub fn new_intermediate( + input: &'i str, + schema: &'static S, + ) -> Self { + Self { + was_empty: input.is_empty(), + input: Cow3::Intermediate(input), + schema, + input_at: 0, + value: None, + } + } +} + +impl<'de, 'i> de::MapAccess<'de> for MapAccess<'de, 'i> { + type Error = Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Error> + where + K: de::DeserializeSeed<'de>, + { + use crate::property_string::next_property; + + if self.was_empty { + // shortcut + return Ok(None); + } + + let (key, value, rem) = match next_property(&self.input[self.input_at..]) { + None => return Ok(None), + Some(entry) => entry?, + }; + + if rem.is_empty() { + self.input_at = self.input.len(); + } else { + let ofs = unsafe { rem.as_ptr().offset_from(self.input.as_ptr()) }; + if ofs < 0 || (ofs as usize) > self.input.len() { + // 'rem' is either an empty string (rem.is_empty() is true), or a valid offset into + // the input string... + panic!("unexpected remainder in next_property"); + } + self.input_at = ofs as usize; + } + + let value = match value { + Cow::Owned(value) => Cow::Owned(value), + Cow::Borrowed(value) => match str_slice_to_range(&self.input, value) { + None => Cow::Owned(value.to_string()), + Some(range) => match &self.input { + Cow3::Original(orig) => Cow::Borrowed(&orig[range]), + _ => Cow::Owned(value.to_string()), + }, + }, + }; + + let (key, schema) = match key { + Some(key) => { + let schema = self.schema.lookup(&key); + let key = match str_slice_to_range(&self.input, key) { + None => Cow::Owned(key.to_string()), + Some(range) => match &self.input { + Cow3::Original(orig) => Cow::Borrowed(&orig[range]), + _ => Cow::Owned(key.to_string()), + }, + }; + (key, schema) + } + None => match self.schema.default_key() { + Some(key) => { + let schema = self + .schema + .lookup(key) + .ok_or(Error::msg("bad default key"))?; + (Cow::Borrowed(key), Some(schema)) + } + None => return Err(Error::msg("missing key")), + }, + }; + let schema = schema.map(|(_optional, schema)| schema); + + let out = match &key { + Cow::Borrowed(key) => { + seed.deserialize(de::value::BorrowedStrDeserializer::<'de, Error>::new(key))? + } + Cow::Owned(key) => { + seed.deserialize(IntoDeserializer::::into_deserializer(key.as_str()))? + } + }; + + self.value = Some((key, value, schema)); + + Ok(Some(out)) + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + let (key, input, schema) = self.value.take().ok_or(Error::msg("bad map access"))?; + + if let Some(schema) = schema { + seed.deserialize(SchemaDeserializer::new(input, schema)) + } else { + if !verify::is_verifying() && !self.schema.additional_properties() { + return Err(Error::msg(format!("unknown key {:?}", key.as_ref()))); + } + + // additional properties are treated as strings... + let deserializer = no_schema::NoSchemaDeserializer::new(input); + seed.deserialize(deserializer) + } + } +} diff --git a/proxmox-schema/src/de/no_schema.rs b/proxmox-schema/src/de/no_schema.rs new file mode 100644 index 00000000..254ebd96 --- /dev/null +++ b/proxmox-schema/src/de/no_schema.rs @@ -0,0 +1,311 @@ +//! When we have no schema we allow simple values and arrays. + +use std::borrow::Cow; + +use serde::de; + +use super::cow3::Cow3; +use super::Error; + +/// This can only deserialize strings and lists of strings and has no schema. +pub struct NoSchemaDeserializer<'de, 'i> { + input: Cow3<'de, 'i, str>, +} + +impl<'de, 'i> NoSchemaDeserializer<'de, 'i> { + pub fn new(input: T) -> Self + where + T: Into>, + { + Self { + input: Cow3::from_original(input), + } + } +} + +macro_rules! deserialize_num { + ($( $name:ident : $visit:ident : $ty:ty : $error:literal, )*) => {$( + fn $name>(self, visitor: V) -> Result { + let value: $ty = self + .input + .parse() + .map_err(|_| Error::msg(format!($error, self.input)))?; + visitor.$visit(value) + } + )*} +} + +impl<'de, 'i> de::Deserializer<'de> for NoSchemaDeserializer<'de, 'i> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.input { + Cow3::Original(input) => visitor.visit_borrowed_str(input), + Cow3::Intermediate(input) => visitor.visit_str(input), + Cow3::Owned(input) => visitor.visit_string(input), + } + } + + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + self.deserialize_any(visitor) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if self.input.is_empty() { + visitor.visit_none() + } else { + visitor.visit_some(self) + } + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + self.deserialize_any(visitor) + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_seq(SimpleSeqAccess::new(self.input)) + } + + fn deserialize_tuple(self, _len: usize, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_seq(SimpleSeqAccess::new(self.input)) + } + + fn deserialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_seq(SimpleSeqAccess::new(self.input)) + } + + deserialize_num! { + deserialize_i8 : visit_i8 : i8 : "not an integer: {:?}", + deserialize_u8 : visit_u8 : u8 : "not an integer: {:?}", + deserialize_i16 : visit_i16 : i16 : "not an integer: {:?}", + deserialize_u16 : visit_u16 : u16 : "not an integer: {:?}", + deserialize_i32 : visit_i32 : i32 : "not an integer: {:?}", + deserialize_u32 : visit_u32 : u32 : "not an integer: {:?}", + deserialize_i64 : visit_i64 : i64 : "not an integer: {:?}", + deserialize_u64 : visit_u64 : u64 : "not an integer: {:?}", + deserialize_f32 : visit_f32 : f32 : "not a number: {:?}", + deserialize_f64 : visit_f64 : f64 : "not a number: {:?}", + deserialize_bool : visit_bool : bool : "not a boolean: {:?}", + } + + fn deserialize_char(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + let mut chars = self.input.chars(); + let ch = chars + .next() + .ok_or_else(|| Error::msg(format!("not a single character: {:?}", self.input)))?; + if chars.next().is_some() { + return Err(Error::msg(format!( + "not a single character: {:?}", + self.input + ))); + } + visitor.visit_char(ch) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.input { + Cow3::Original(input) => visitor.visit_borrowed_str(input), + Cow3::Intermediate(input) => visitor.visit_str(input), + Cow3::Owned(input) => visitor.visit_string(input), + } + } + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.input { + Cow3::Original(input) => visitor.visit_borrowed_str(input), + Cow3::Intermediate(input) => visitor.visit_str(input), + Cow3::Owned(input) => visitor.visit_string(input), + } + } + + fn deserialize_string(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.input { + Cow3::Original(input) => visitor.visit_borrowed_str(input), + Cow3::Intermediate(input) => visitor.visit_str(input), + Cow3::Owned(input) => visitor.visit_string(input), + } + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.input { + Cow3::Original(input) => visitor.visit_borrowed_bytes(input.as_bytes()), + Cow3::Intermediate(input) => visitor.visit_bytes(input.as_bytes()), + Cow3::Owned(input) => visitor.visit_byte_buf(input.into_bytes()), + } + } + + fn deserialize_byte_buf(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + match self.input { + Cow3::Original(input) => visitor.visit_borrowed_bytes(input.as_bytes()), + Cow3::Intermediate(input) => visitor.visit_bytes(input.as_bytes()), + Cow3::Owned(input) => visitor.visit_byte_buf(input.into_bytes()), + } + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if self.input.is_empty() { + visitor.visit_unit() + } else { + self.deserialize_string(visitor) + } + } + + fn deserialize_unit_struct(self, _name: &'static str, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + if self.input.is_empty() { + visitor.visit_unit() + } else { + self.deserialize_string(visitor) + } + } + + fn deserialize_newtype_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: de::Visitor<'de>, + { + use serde::de::IntoDeserializer; + visitor.visit_enum(self.input.into_deserializer()) + } + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: de::Visitor<'de>, + { + self.deserialize_string(visitor) + } +} + +/// Parse an array without a schema. +/// +/// It may only contain simple values. +struct SimpleSeqAccess<'de, 'i> { + input: Cow3<'de, 'i, str>, + has_null: bool, + at: usize, +} + +impl<'de, 'i> SimpleSeqAccess<'de, 'i> { + fn new(input: Cow3<'de, 'i, str>) -> Self { + Self { + has_null: input.contains('\0'), + input, + at: 0, + } + } +} + +impl<'de, 'i> de::SeqAccess<'de> for SimpleSeqAccess<'de, 'i> { + type Error = Error; + + fn next_element_seed(&mut self, seed: T) -> Result, Error> + where + T: de::DeserializeSeed<'de>, + { + while self.at != self.input.len() { + let begin = self.at; + + let input = &self.input[self.at..]; + + let end = if self.has_null { + input.find('\0') + } else { + input.find(|c: char| c == ',' || c == ';' || char::is_ascii_whitespace(&c)) + }; + + let end = match end { + None => { + self.at = self.input.len(); + input.len() + } + Some(pos) => { + self.at += pos + 1; + pos + } + }; + + if input[..end].is_empty() { + continue; + } + + return seed + .deserialize(NoSchemaDeserializer::new(match &self.input { + Cow3::Original(input) => Cow::Borrowed(&input[begin..end]), + Cow3::Intermediate(input) => Cow::Owned(input[begin..end].to_string()), + Cow3::Owned(input) => Cow::Owned(input[begin..end].to_string()), + })) + .map(Some); + } + + Ok(None) + } +} diff --git a/proxmox-schema/src/de/verify.rs b/proxmox-schema/src/de/verify.rs new file mode 100644 index 00000000..a626d284 --- /dev/null +++ b/proxmox-schema/src/de/verify.rs @@ -0,0 +1,298 @@ +use std::borrow::Cow; +use std::cell::UnsafeCell; +use std::collections::HashSet; +use std::fmt; +use std::mem; + +use anyhow::format_err; +use serde::de::{self, Deserialize, Unexpected}; + +use super::Schema; +use crate::schema::ParameterError; + +struct VerifyState { + schema: Option<&'static Schema>, + path: String, +} + +thread_local! { + static VERIFY_SCHEMA: UnsafeCell> = UnsafeCell::new(None); + static ERRORS: UnsafeCell> = UnsafeCell::new(Vec::new()); +} + +pub(crate) struct SchemaGuard(Option); + +impl Drop for SchemaGuard { + fn drop(&mut self) { + VERIFY_SCHEMA.with(|schema| unsafe { + if self.0.is_none() { + ERRORS.with(|errors| (*errors.get()).clear()) + } + *schema.get() = self.0.take(); + }); + } +} + +impl SchemaGuard { + /// If this is the "final" guard, take out the errors: + fn errors(self) -> Option> { + if self.0.is_none() { + Some(ERRORS.with(|e| mem::take(unsafe { &mut *e.get() }))) + } else { + None + } + } +} + +pub(crate) fn push_schema(schema: Option<&'static Schema>, path: Option<&str>) -> SchemaGuard { + SchemaGuard(VERIFY_SCHEMA.with(|s| { + let prev = unsafe { (*s.get()).take() }; + let path = match (path, &prev) { + (Some(path), Some(prev)) => join_path(&prev.path, path), + (Some(path), None) => path.to_owned(), + (None, Some(prev)) => prev.path.clone(), + (None, None) => String::new(), + }; + + unsafe { + (*s.get()) = Some(VerifyState { schema, path }); + } + + prev + })) +} + +fn get_path() -> Option { + VERIFY_SCHEMA.with(|s| unsafe { (*s.get()).as_ref().map(|state| state.path.clone()) }) +} + +fn get_schema() -> Option<&'static Schema> { + VERIFY_SCHEMA.with(|s| unsafe { (*s.get()).as_ref().and_then(|state| state.schema) }) +} + +pub(crate) fn is_verifying() -> bool { + VERIFY_SCHEMA.with(|s| unsafe { (*s.get()).as_ref().is_some() }) +} + +fn join_path(a: &str, b: &str) -> String { + if a.is_empty() { + b.to_string() + } else { + format!("{}/{}", a, b) + } +} + +fn push_errstr_path(err_path: &str, err: &str) { + if let Some(path) = get_path() { + push_err_do(join_path(&path, err_path), format_err!("{}", err)); + } +} + +fn push_err(err: impl fmt::Display) { + if let Some(path) = get_path() { + push_err_do(path, format_err!("{}", err)); + } +} + +fn push_err_do(path: String, err: anyhow::Error) { + ERRORS.with(move |errors| unsafe { (*errors.get()).push((path, err)) }) +} + +/// Helper to collect multiple deserialization errors for better reporting. +/// +/// This is similar to [`IgnoredAny`] in that it implements [`Deserialize`] +/// but does not actually deserialize to anything, however, when a deserialization error occurs, +/// it'll try to continue and collect further errors. +/// +/// This only makes sense with the [`SchemaDeserializer`](super::SchemaDeserializer). +pub struct Verifier; + +impl<'de> Deserialize<'de> for Verifier { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + if let Some(schema) = get_schema() { + let visitor = Visitor(schema); + match schema { + Schema::Boolean(_) => deserializer.deserialize_bool(visitor), + Schema::Integer(_) => deserializer.deserialize_i64(visitor), + Schema::Number(_) => deserializer.deserialize_f64(visitor), + Schema::String(_) => deserializer.deserialize_str(visitor), + Schema::Object(_) => deserializer.deserialize_map(visitor), + Schema::AllOf(_) => deserializer.deserialize_map(visitor), + Schema::Array(_) => deserializer.deserialize_seq(visitor), + Schema::Null => deserializer.deserialize_unit(visitor), + } + } else { + Ok(Verifier) + } + } +} + +pub fn verify(schema: &'static Schema, value: &str) -> Result<(), anyhow::Error> { + let guard = push_schema(Some(schema), None); + Verifier::deserialize(super::SchemaDeserializer::new(value, schema))?; + + if let Some(errors) = guard.errors() { + Err(ParameterError::from_list(errors).into()) + } else { + Ok(()) + } +} + +struct Visitor(&'static Schema); + +impl<'de> de::Visitor<'de> for Visitor { + type Value = Verifier; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.0 { + Schema::Boolean(_) => f.write_str("boolean"), + Schema::Integer(_) => f.write_str("integer"), + Schema::Number(_) => f.write_str("number"), + Schema::String(_) => f.write_str("string"), + Schema::Object(_) => f.write_str("object"), + Schema::AllOf(_) => f.write_str("allOf"), + Schema::Array(_) => f.write_str("Array"), + Schema::Null => f.write_str("null"), + } + } + + fn visit_bool(self, v: bool) -> Result { + match self.0 { + Schema::Boolean(_) => (), + _ => return Err(E::invalid_type(Unexpected::Bool(v), &self)), + } + Ok(Verifier) + } + + fn visit_i64(self, v: i64) -> Result { + match self.0 { + Schema::Integer(schema) => match schema.check_constraints(v as isize) { + Ok(()) => Ok(Verifier), + Err(err) => Err(E::custom(err)), + }, + _ => Err(E::invalid_type(Unexpected::Signed(v), &self)), + } + } + + fn visit_u64(self, v: u64) -> Result { + match self.0 { + Schema::Integer(schema) => match schema.check_constraints(v as isize) { + Ok(()) => Ok(Verifier), + Err(err) => Err(E::custom(err)), + }, + _ => Err(E::invalid_type(Unexpected::Unsigned(v), &self)), + } + } + + fn visit_f64(self, v: f64) -> Result { + match self.0 { + Schema::Number(schema) => match schema.check_constraints(v) { + Ok(()) => Ok(Verifier), + Err(err) => Err(E::custom(err)), + }, + _ => Err(E::invalid_type(Unexpected::Float(v), &self)), + } + } + + fn visit_seq>(self, mut seq: A) -> Result { + use de::Error; + + let schema = match self.0 { + Schema::Array(schema) => schema, + _ => return Err(A::Error::invalid_type(Unexpected::Seq, &self)), + }; + + let _guard = push_schema(Some(schema.items), None); + + let mut count = 0; + loop { + match seq.next_element::() { + Ok(Some(_)) => count += 1, + Ok(None) => break, + Err(err) => push_err(err), + } + } + + schema.check_length(count).map_err(de::Error::custom)?; + + Ok(Verifier) + } + + fn visit_map>(self, mut map: A) -> Result { + use de::Error; + + let schema: &'static dyn crate::schema::ObjectSchemaType = match self.0 { + Schema::Object(schema) => schema, + Schema::AllOf(schema) => schema, + _ => return Err(A::Error::invalid_type(Unexpected::Map, &self)), + }; + + let mut required_keys = HashSet::<&'static str>::new(); + for (key, optional, _schema) in schema.properties() { + if !optional { + required_keys.insert(key); + } + } + + let mut other_keys = HashSet::::new(); + loop { + let key: Cow<'de, str> = match map.next_key()? { + Some(key) => key, + None => break, + }; + + let _guard = match schema.lookup(&key) { + Some((optional, schema)) => { + if !optional { + // required keys are only tracked in the required_keys hashset + if !required_keys.remove(key.as_ref()) { + // duplicate key + push_errstr_path(&key, "duplicate key"); + } + } else { + // optional keys + if !other_keys.insert(key.clone().into_owned()) { + push_errstr_path(&key, "duplicate key"); + } + } + + push_schema(Some(schema), Some(&key)) + } + None => { + if !schema.additional_properties() { + push_errstr_path(&key, "schema does not allow additional properties"); + } else if !other_keys.insert(key.clone().into_owned()) { + push_errstr_path(&key, "duplicate key"); + } + + push_schema(None, Some(&key)) + } + }; + + match map.next_value::() { + Ok(Verifier) => (), + Err(err) => push_err(err), + } + } + + for key in required_keys { + push_errstr_path(key, "property is missing and it is not optional"); + } + + Ok(Verifier) + } + + fn visit_str(self, value: &str) -> Result { + let schema = match self.0 { + Schema::String(schema) => schema, + _ => return Err(E::invalid_type(Unexpected::Str(value), &self)), + }; + + let _: () = schema.check_constraints(value).map_err(E::custom)?; + + Ok(Verifier) + } +} diff --git a/proxmox-schema/src/lib.rs b/proxmox-schema/src/lib.rs index cfebed7e..09c271bf 100644 --- a/proxmox-schema/src/lib.rs +++ b/proxmox-schema/src/lib.rs @@ -19,6 +19,7 @@ pub use const_regex::ConstRegexPattern; pub mod de; pub mod format; +pub mod ser; pub mod property_string; diff --git a/proxmox-schema/src/property_string.rs b/proxmox-schema/src/property_string.rs index b4269448..bda6ed87 100644 --- a/proxmox-schema/src/property_string.rs +++ b/proxmox-schema/src/property_string.rs @@ -3,9 +3,13 @@ //! strings. use std::borrow::Cow; +use std::fmt; use std::mem; -use anyhow::{bail, format_err, Error}; +use serde::{Deserialize, Serialize}; + +use crate::de::Error; +use crate::schema::ApiType; /// Iterate over the `key=value` pairs of a property string. /// @@ -26,48 +30,61 @@ impl<'a> Iterator for PropertyIterator<'a> { type Item = Result<(Option<&'a str>, Cow<'a, str>), Error>; fn next(&mut self) -> Option { - if self.data.is_empty() { - return None; - } - - let key = if self.data.starts_with('"') { - // value without key and quoted - None - } else { - let key = match self.data.find([',', '=']) { - Some(pos) if self.data.as_bytes()[pos] == b',' => None, - Some(pos) => Some(ascii_split_off(&mut self.data, pos)), - None => None, - }; - - if !self.data.starts_with('"') { - let value = match self.data.find(',') { - Some(pos) => ascii_split_off(&mut self.data, pos), - None => mem::take(&mut self.data), - }; - return Some(Ok((key, Cow::Borrowed(value)))); + Some(match next_property(self.data)? { + Ok((key, value, data)) => { + self.data = data; + Ok((key, value)) } - - key - }; - - let value = match parse_quoted_string(&mut self.data) { - Ok(value) => value, - Err(err) => return Some(Err(err)), - }; - - if !self.data.is_empty() { - if self.data.starts_with(',') { - self.data = &self.data[1..]; - } else { - return Some(Err(format_err!("garbage after quoted string"))); - } - } - - Some(Ok((key, value))) + Err(err) => Err(err), + }) } } +/// Returns an optional key, its value, and the remainder of `data`. +pub(crate) fn next_property( + mut data: &str, +) -> Option, Cow, &str), Error>> { + if data.is_empty() { + return None; + } + + let key = if data.starts_with('"') { + // value without key and quoted + None + } else { + let key = match data.find([',', '=']) { + Some(pos) if data.as_bytes()[pos] == b',' => None, + Some(pos) => Some(ascii_split_off(&mut data, pos)), + None => None, + }; + + if !data.starts_with('"') { + let value = match data.find(',') { + Some(pos) => ascii_split_off(&mut data, pos), + None => mem::take(&mut data), + }; + return Some(Ok((key, Cow::Borrowed(value), data))); + } + + key + }; + + let value = match parse_quoted_string(&mut data) { + Ok(value) => value, + Err(err) => return Some(Err(err)), + }; + + if !data.is_empty() { + if data.starts_with(',') { + data = &data[1..]; + } else { + return Some(Err(Error::msg("garbage after quoted string"))); + } + } + + Some(Ok((key, value, data))) +} + impl<'a> std::iter::FusedIterator for PropertyIterator<'a> {} /// Parse a quoted string and move `data` to after the closing quote. @@ -83,7 +100,7 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result, Error> let data = data_out.as_bytes(); if data[0] != b'"' { - bail!("not a quoted string"); + return Err(Error::msg("not a quoted string")); } let mut i = 1; @@ -101,7 +118,7 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result, Error> } if i == data.len() { // reached the end before reaching a quote - bail!("unexpected end of string"); + return Err(Error::msg("unexpected end of string")); } // we're now at the first backslash, don't include it in the output: @@ -111,7 +128,7 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result, Error> let mut was_backslash = true; loop { if i == data.len() { - bail!("unexpected end of string"); + return Err(Error::msg("unexpected end of string")); } match (data[i], mem::replace(&mut was_backslash, false)) { @@ -122,7 +139,7 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result, Error> (b'"', true) => out.push(b'"'), (b'\\', true) => out.push(b'\\'), (b'n', true) => out.push(b'\n'), - (_, true) => bail!("unsupported escape sequence"), + (_, true) => return Err(Error::msg("unsupported escape sequence")), (b'\\', false) => was_backslash = true, (ch, false) => out.push(ch), } @@ -134,6 +151,20 @@ fn parse_quoted_string<'s>(data: &'_ mut &'s str) -> Result, Error> Ok(Cow::Owned(unsafe { String::from_utf8_unchecked(out) })) } +/// Counterpart to `parse_quoted_string`, only supporting the above-supported escape sequences. +/// Returns `true` +pub(crate) fn quote(s: &str, out: &mut T) -> fmt::Result { + for b in s.chars() { + match b { + '"' => out.write_str(r#"\""#)?, + '\\' => out.write_str(r#"\\"#)?, + '\n' => out.write_str(r#"\n"#)?, + b => out.write_char(b)?, + } + } + Ok(()) +} + /// Like `str::split_at` but with assumes `mid` points to an ASCII character and the 2nd slice /// *excludes* `mid`. fn ascii_split_around(s: &str, mid: usize) -> (&str, &str) { @@ -174,3 +205,258 @@ fn iterate_over_property_string() { .unwrap() .is_err()); } + +/// A wrapper for a de/serializable type which is stored as a property string. +#[derive(Clone, Copy, Debug, Default, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[repr(transparent)] +pub struct PropertyString(T); + +impl PropertyString { + pub fn new(inner: T) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> T { + self.0 + } +} + +impl PropertyString { + pub fn to_property_string(&self) -> Result { + print(&self.0) + } +} + +impl From for PropertyString { + fn from(inner: T) -> Self { + Self(inner) + } +} + +impl std::ops::Deref for PropertyString { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl std::ops::DerefMut for PropertyString { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl AsRef for PropertyString { + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl AsMut for PropertyString { + fn as_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl<'de, T> Deserialize<'de> for PropertyString +where + T: Deserialize<'de> + ApiType, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use std::marker::PhantomData; + + struct V(PhantomData); + + impl<'de, T> serde::de::Visitor<'de> for V + where + T: Deserialize<'de> + ApiType, + { + type Value = T; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("a property string") + } + + fn visit_str(self, s: &str) -> Result + where + E: serde::de::Error, + { + self.visit_string(s.to_string()) + } + + fn visit_string(self, s: String) -> Result + where + E: serde::de::Error, + { + T::deserialize(crate::de::SchemaDeserializer::new(s, &T::API_SCHEMA)) + .map_err(|err| E::custom(err.to_string())) + } + + fn visit_borrowed_str(self, s: &'de str) -> Result + where + E: serde::de::Error, + { + T::deserialize(crate::de::SchemaDeserializer::new(s, &T::API_SCHEMA)) + .map_err(|err| E::custom(err.to_string())) + } + } + + deserializer.deserialize_string(V(PhantomData)).map(Self) + } +} + +impl std::str::FromStr for PropertyString +where + T: ApiType + for<'de> Deserialize<'de>, +{ + type Err = Error; + + fn from_str(s: &str) -> Result { + T::deserialize(crate::de::SchemaDeserializer::new(s, &T::API_SCHEMA)).map(Self) + } +} + +impl Serialize for PropertyString +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::Error; + + serializer.serialize_str(&print(&self.0).map_err(S::Error::custom)?) + } +} + +/// Serialize a value as a property string. +pub fn print(value: &T) -> Result { + value.serialize(crate::ser::PropertyStringSerializer::new(String::new())) +} + +/// Deserialize a value from a property string. +pub fn parse(value: &str) -> Result +where + T: for<'de> Deserialize<'de>, +{ + parse_with_schema(value, &T::API_SCHEMA) +} + +/// Deserialize a value from a property string. +pub fn parse_with_schema(value: &str, schema: &'static crate::Schema) -> Result +where + T: for<'de> Deserialize<'de>, +{ + T::deserialize(crate::de::SchemaDeserializer::new(value, schema)) +} + +#[cfg(test)] +mod test { + use serde::{Deserialize, Serialize}; + + use crate::schema::*; + + impl ApiType for Object { + const API_SCHEMA: Schema = ObjectSchema::new( + "An object", + &[ + // MUST BE SORTED + ("count", false, &IntegerSchema::new("name").schema()), + ("name", false, &StringSchema::new("name").schema()), + ("nested", true, &Nested::API_SCHEMA), + ( + "optional", + true, + &BooleanSchema::new("an optional boolean").schema(), + ), + ], + ) + .schema(); + } + + #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] + pub struct Object { + name: String, + count: u32, + #[serde(skip_serializing_if = "Option::is_none")] + optional: Option, + #[serde(skip_serializing_if = "Option::is_none")] + nested: Option, + } + + impl ApiType for Nested { + const API_SCHEMA: Schema = ObjectSchema::new( + "An object", + &[ + // MUST BE SORTED + ( + "count", + true, + &ArraySchema::new("count", &IntegerSchema::new("a value").schema()).schema(), + ), + ("name", false, &StringSchema::new("name").schema()), + ("third", true, &Third::API_SCHEMA), + ], + ) + .schema(); + } + + #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] + pub struct Nested { + name: String, + + #[serde(default, skip_serializing_if = "Vec::is_empty")] + count: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + third: Option, + } + + impl ApiType for Third { + const API_SCHEMA: Schema = ObjectSchema::new( + "An object", + &[ + // MUST BE SORTED + ("count", false, &IntegerSchema::new("name").schema()), + ("name", false, &StringSchema::new("name").schema()), + ], + ) + .schema(); + } + + #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] + pub struct Third { + name: String, + count: u32, + } + + #[test] + fn test() -> Result<(), super::Error> { + let obj = Object { + name: "One \"Mo\\re\" Name".to_string(), + count: 12, + optional: Some(true), + nested: Some(Nested { + name: "a \"bobby\"".to_string(), + count: vec![22, 23, 24], + third: Some(Third { + name: "oh\\backslash".to_string(), + count: 37, + }), + }), + }; + + let s = super::print(&obj)?; + + let deserialized: Object = super::parse(&s).expect("failed to parse property string"); + + assert_eq!(obj, deserialized, "deserialized does not equal original"); + + Ok(()) + } +} diff --git a/proxmox-schema/src/schema.rs b/proxmox-schema/src/schema.rs index a2b165c8..2ae58c0f 100644 --- a/proxmox-schema/src/schema.rs +++ b/proxmox-schema/src/schema.rs @@ -88,6 +88,10 @@ impl ParameterError { Err(err) => self.push(prefix.to_string(), err), } } + + pub(crate) fn from_list(error_list: Vec<(String, Error)>) -> Self { + Self { error_list } + } } impl fmt::Display for ParameterError { @@ -248,7 +252,7 @@ impl IntegerSchema { Schema::Integer(self) } - fn check_constraints(&self, value: isize) -> Result<(), Error> { + pub fn check_constraints(&self, value: isize) -> Result<(), Error> { if let Some(minimum) = self.minimum { if value < minimum { bail!( @@ -323,7 +327,7 @@ impl NumberSchema { Schema::Number(self) } - fn check_constraints(&self, value: f64) -> Result<(), Error> { + pub fn check_constraints(&self, value: f64) -> Result<(), Error> { if let Some(minimum) = self.minimum { if value < minimum { bail!( @@ -436,7 +440,7 @@ impl StringSchema { Schema::String(self) } - fn check_length(&self, length: usize) -> Result<(), Error> { + pub(crate) fn check_length(&self, length: usize) -> Result<(), Error> { if let Some(min_length) = self.min_length { if length < min_length { bail!("value must be at least {} characters long", min_length); @@ -537,7 +541,7 @@ impl ArraySchema { Schema::Array(self) } - fn check_length(&self, length: usize) -> Result<(), Error> { + pub(crate) fn check_length(&self, length: usize) -> Result<(), Error> { if let Some(min_length) = self.min_length { if length < min_length { bail!("array must contain at least {} elements", min_length); @@ -722,6 +726,7 @@ pub trait ObjectSchemaType { fn lookup(&self, key: &str) -> Option<(bool, &Schema)>; fn properties(&self) -> ObjectPropertyIterator; fn additional_properties(&self) -> bool; + fn default_key(&self) -> Option<&'static str>; /// Verify JSON value using an object schema. fn verify_json(&self, data: &Value) -> Result<(), Error> { @@ -785,6 +790,10 @@ impl ObjectSchemaType for ObjectSchema { fn additional_properties(&self) -> bool { self.additional_properties } + + fn default_key(&self) -> Option<&'static str> { + self.default_key + } } impl ObjectSchemaType for AllOfSchema { @@ -807,6 +816,22 @@ impl ObjectSchemaType for AllOfSchema { fn additional_properties(&self) -> bool { true } + + fn default_key(&self) -> Option<&'static str> { + for schema in self.list { + let default_key = match schema { + Schema::Object(schema) => schema.default_key(), + Schema::AllOf(schema) => schema.default_key(), + _ => panic!("non-object-schema in `AllOfSchema`"), + }; + + if default_key.is_some() { + return default_key; + } + } + + None + } } #[doc(hidden)] @@ -1246,6 +1271,13 @@ impl ObjectSchemaType for ParameterSchema { ParameterSchema::AllOf(o) => o.additional_properties(), } } + + fn default_key(&self) -> Option<&'static str> { + match self { + ParameterSchema::Object(o) => o.default_key(), + ParameterSchema::AllOf(o) => o.default_key(), + } + } } impl From<&'static ObjectSchema> for ParameterSchema { diff --git a/proxmox-schema/src/ser/mod.rs b/proxmox-schema/src/ser/mod.rs new file mode 100644 index 00000000..ab81c4bd --- /dev/null +++ b/proxmox-schema/src/ser/mod.rs @@ -0,0 +1,636 @@ +//! Property string serialization. + +use std::fmt; +use std::mem; + +use serde::ser::{self, Serialize, Serializer}; + +use crate::de::Error; + +impl serde::ser::Error for Error { + fn custom(msg: T) -> Self { + Self::msg(msg.to_string()) + } +} + +pub struct PropertyStringSerializer { + inner: T, +} + +impl PropertyStringSerializer { + pub fn new(inner: T) -> Self { + Self { inner } + } +} + +macro_rules! not_an_object { + () => {}; + ($name:ident($ty:ty) $($rest:tt)*) => { + fn $name(self, _v: $ty) -> Result { + Err(Error::msg("property string serializer used with a non-object type")) + } + + not_an_object! { $($rest)* } + }; + ($name:ident($($args:tt)*) $($rest:tt)*) => { + fn $name(self, $($args)*) -> Result { + Err(Error::msg("property string serializer used with a non-object type")) + } + + not_an_object! { $($rest)* } + }; + ($name:ident<($($gen:tt)*)>($($args:tt)*) $($rest:tt)*) => { + fn $name<$($gen)*>(self, $($args)*) -> Result { + Err(Error::msg("property string serializer used with a non-object type")) + } + + not_an_object! { $($rest)* } + }; +} + +macro_rules! same_impl { + (as impl _ for $struct:ident { $($code:tt)* }) => {}; + ( + ser::$trait:ident + $(ser::$more_traits:ident)* + as impl _ for $struct:ident { $($code:tt)* } + ) => { + impl ser::$trait for $struct { $($code)* } + same_impl! { + $(ser::$more_traits)* + as impl _ for $struct { $($code)* } + } + } +} + +impl Serializer for PropertyStringSerializer { + type Ok = T; + type Error = Error; + + type SerializeSeq = SerializeSeq; + type SerializeTuple = SerializeSeq; + type SerializeTupleStruct = SerializeSeq; + type SerializeTupleVariant = SerializeSeq; + type SerializeMap = SerializeStruct; + type SerializeStruct = SerializeStruct; + type SerializeStructVariant = SerializeStruct; + + fn is_human_readable(&self) -> bool { + true + } + + not_an_object! { + serialize_bool(bool) + serialize_i8(i8) + serialize_i16(i16) + serialize_i32(i32) + serialize_i64(i64) + serialize_u8(u8) + serialize_u16(u16) + serialize_u32(u32) + serialize_u64(u64) + serialize_f32(f32) + serialize_f64(f64) + serialize_char(char) + serialize_str(&str) + serialize_bytes(&[u8]) + serialize_none() + serialize_some<(V: Serialize + ?Sized)>(_value: &V) + serialize_unit() + serialize_unit_struct(&'static str) + serialize_unit_variant(_name: &'static str, _index: u32, _var: &'static str) + } + + fn serialize_newtype_struct(self, _name: &'static str, value: &V) -> Result + where + V: Serialize + ?Sized, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &V, + ) -> Result + where + V: Serialize + ?Sized, + { + Err(Error::msg(format!( + "cannot serialize enum {name:?} with newtype variants" + ))) + } + + fn serialize_seq(self, _len: Option) -> Result { + Ok(SerializeSeq::new(self.inner)) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Ok(SerializeSeq::new(self.inner)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(SerializeSeq::new(self.inner)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Ok(SerializeSeq::new(self.inner)) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result, Error> { + Ok(SerializeStruct::new(self.inner)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result, Error> { + Ok(SerializeStruct::new(self.inner)) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(SerializeStruct::new(self.inner)) + } +} + +pub struct SerializeStruct { + inner: Option, + comma: bool, +} + +impl SerializeStruct { + fn new(inner: T) -> Self { + Self { + inner: Some(inner), + comma: false, + } + } + + fn field(&mut self, key: &'static str, value: &V) -> Result<(), Error> + where + V: Serialize + ?Sized, + { + let mut inner = self.inner.take().unwrap(); + + if mem::replace(&mut self.comma, true) { + inner.write_char(',')?; + } + write!(inner, "{key}=")?; + self.inner = Some(value.serialize(ElementSerializer::new(inner))?); + Ok(()) + } + + fn finish(mut self) -> Result { + Ok(self.inner.take().unwrap()) + } +} + +same_impl! { + ser::SerializeStruct + ser::SerializeStructVariant + as impl _ for SerializeStruct { + type Ok = T; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &V) -> Result<(), Self::Error> + where + V: Serialize + ?Sized, + { + self.field(key, value) + } + + fn end(self) -> Result { + self.finish() + } + } +} + +impl ser::SerializeMap for SerializeStruct { + type Ok = T; + type Error = Error; + + fn serialize_key(&mut self, key: &K) -> Result<(), Self::Error> + where + K: Serialize + ?Sized, + { + let mut inner = self.inner.take().unwrap(); + if mem::replace(&mut self.comma, true) { + inner.write_char(',')?; + } + inner = key.serialize(ElementSerializer::new(inner))?; + inner.write_char('=')?; + self.inner = Some(inner); + Ok(()) + } + + fn serialize_value(&mut self, value: &V) -> Result<(), Self::Error> + where + V: Serialize + ?Sized, + { + let mut inner = self.inner.take().unwrap(); + inner = value.serialize(ElementSerializer::new(inner))?; + self.inner = Some(inner); + Ok(()) + } + + fn end(self) -> Result { + self.finish() + } +} + +pub struct SerializeSeq { + inner: Option, + comma: bool, +} + +impl SerializeSeq { + fn new(inner: T) -> Self { + Self { + inner: Some(inner), + comma: false, + } + } + + fn element(&mut self, value: &V) -> Result<(), Error> + where + V: Serialize + ?Sized, + { + let mut inner = self.inner.take().unwrap(); + if mem::replace(&mut self.comma, true) { + inner.write_char(',')?; + } + + inner = value.serialize(ElementSerializer::new(inner))?; + self.inner = Some(inner); + Ok(()) + } + + fn finish(mut self) -> Result { + Ok(self.inner.take().unwrap()) + } +} + +same_impl! { + ser::SerializeSeq + ser::SerializeTuple + as impl _ for SerializeSeq { + type Ok = T; + type Error = Error; + + fn serialize_element(&mut self, value: &V) -> Result<(), Error> + where + V: Serialize + ?Sized, + { + self.element(value) + } + + fn end(self) -> Result { + self.finish() + } + } +} + +same_impl! { + ser::SerializeTupleStruct + ser::SerializeTupleVariant + as impl _ for SerializeSeq { + type Ok = T; + type Error = Error; + + fn serialize_field(&mut self, value: &V) -> Result<(), Error> + where + V: Serialize + ?Sized, + { + self.element(value) + } + + fn end(self) -> Result { + self.finish() + } + } +} + +pub struct ElementSerializer { + inner: T, +} + +impl ElementSerializer { + fn new(inner: T) -> Self { + Self { inner } + } +} + +impl ElementSerializer { + fn serialize_with_display(mut self, v: V) -> Result { + write!(self.inner, "{v}") + .map_err(|err| Error::msg(format!("failed to write string: {err}")))?; + Ok(self.inner) + } +} + +macro_rules! forward_to_display { + () => {}; + ($name:ident($ty:ty) $($rest:tt)*) => { + fn $name(self, v: $ty) -> Result { + self.serialize_with_display(v) + } + + forward_to_display! { $($rest)* } + }; +} + +impl Serializer for ElementSerializer { + type Ok = T; + type Error = Error; + + type SerializeSeq = ElementSerializeSeq; + type SerializeTuple = ElementSerializeSeq; + type SerializeTupleStruct = ElementSerializeSeq; + type SerializeTupleVariant = ElementSerializeSeq; + type SerializeMap = ElementSerializeStruct; + type SerializeStruct = ElementSerializeStruct; + type SerializeStructVariant = ElementSerializeStruct; + + fn is_human_readable(&self) -> bool { + true + } + + forward_to_display! { + serialize_bool(bool) + serialize_i8(i8) + serialize_i16(i16) + serialize_i32(i32) + serialize_i64(i64) + serialize_u8(u8) + serialize_u16(u16) + serialize_u32(u32) + serialize_u64(u64) + serialize_f32(f32) + serialize_f64(f64) + serialize_char(char) + } + + fn serialize_str(mut self, v: &str) -> Result { + if v.contains(&['"', '\\', '\n']) { + self.inner.write_char('"')?; + crate::property_string::quote(v, &mut self.inner)?; + self.inner.write_char('"')?; + } else { + self.inner.write_str(v)?; + } + Ok(self.inner) + } + + fn serialize_bytes(self, _: &[u8]) -> Result { + Err(Error::msg( + "raw byte value not supported in property string", + )) + } + + fn serialize_none(self) -> Result { + Err(Error::msg("tried to serialize 'None' value")) + } + + fn serialize_some(self, v: &V) -> Result { + v.serialize(self) + } + + fn serialize_unit(self) -> Result { + Err(Error::msg("tried to serialize a unit value")) + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + Err(Error::msg(format!( + "tried to serialize a unit value (struct {name})" + ))) + } + + fn serialize_unit_variant( + self, + name: &'static str, + _index: u32, + variant: &'static str, + ) -> Result { + Err(Error::msg(format!( + "tried to serialize a unit variant ({name}::{variant})" + ))) + } + + fn serialize_newtype_struct(self, _name: &'static str, value: &V) -> Result + where + V: Serialize + ?Sized, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + value: &V, + ) -> Result + where + V: Serialize + ?Sized, + { + value.serialize(self) + } + + fn serialize_seq(self, _len: Option) -> Result { + Ok(ElementSerializeSeq::new(self.inner)) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Ok(ElementSerializeSeq::new(self.inner)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(ElementSerializeSeq::new(self.inner)) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Ok(ElementSerializeSeq::new(self.inner)) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(ElementSerializeStruct::new(self.inner)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Ok(ElementSerializeStruct::new(self.inner)) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(ElementSerializeStruct::new(self.inner)) + } +} + +pub struct ElementSerializeStruct { + output: T, + inner: SerializeStruct, +} + +impl ElementSerializeStruct { + fn new(inner: T) -> Self { + Self { + output: inner, + inner: SerializeStruct::new(String::new()), + } + } + + fn finish(mut self) -> Result { + let value = self.inner.finish()?; + self.output.write_char('"')?; + crate::property_string::quote(&value, &mut self.output)?; + self.output.write_char('"')?; + Ok(self.output) + } +} + +same_impl! { + ser::SerializeStruct + ser::SerializeStructVariant + as impl _ for ElementSerializeStruct { + type Ok = T; + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &V) -> Result<(), Self::Error> + where + V: Serialize + ?Sized, + { + self.inner.field(key, value) + } + + fn end(self) -> Result { + self.finish() + } + } +} + +impl ser::SerializeMap for ElementSerializeStruct { + type Ok = T; + type Error = Error; + + fn serialize_key(&mut self, key: &K) -> Result<(), Self::Error> + where + K: Serialize + ?Sized, + { + self.inner.serialize_key(key) + } + + fn serialize_value(&mut self, value: &V) -> Result<(), Self::Error> + where + V: Serialize + ?Sized, + { + self.inner.serialize_value(value) + } + + fn end(self) -> Result { + self.finish() + } +} + +pub struct ElementSerializeSeq { + output: T, + inner: SerializeSeq, +} + +impl ElementSerializeSeq { + fn new(inner: T) -> Self { + Self { + output: inner, + inner: SerializeSeq::new(String::new()), + } + } + + fn finish(mut self) -> Result { + let value = self.inner.finish()?; + if value.contains(&[',', ';', ' ', '"', '\\', '\n']) { + self.output.write_char('"')?; + crate::property_string::quote(&value, &mut self.output)?; + self.output.write_char('"')?; + } else { + self.output.write_str(&value)?; + } + Ok(self.output) + } +} + +same_impl! { + ser::SerializeSeq + ser::SerializeTuple + as impl _ for ElementSerializeSeq { + type Ok = T; + type Error = Error; + + fn serialize_element(&mut self, value: &V) -> Result<(), Error> + where + V: Serialize + ?Sized, + { + self.inner.serialize_element(value) + } + + fn end(self) -> Result { + self.finish() + } + } +} + +same_impl! { + ser::SerializeTupleStruct + ser::SerializeTupleVariant + as impl _ for ElementSerializeSeq { + type Ok = T; + type Error = Error; + + fn serialize_field(&mut self, value: &V) -> Result<(), Error> + where + V: Serialize + ?Sized, + { + self.inner.element(value) + } + + fn end(self) -> Result { + self.finish() + } + } +}