diff --git a/src/curr.rs b/src/curr.rs index 5f51b0a5..79c74694 100644 --- a/src/curr.rs +++ b/src/curr.rs @@ -76,6 +76,9 @@ use std::{ io::{BufRead, BufReader, Cursor, Read, Write}, }; +/// Error contains all errors returned by functions in this crate. It can be +/// compared via `PartialEq`, however any contained IO errors will only be +/// compared on their `ErrorKind`. #[derive(Debug)] pub enum Error { Invalid, @@ -87,6 +90,23 @@ pub enum Error { Io(io::Error), } +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Utf8Error(l), Self::Utf8Error(r)) => l == r, + // IO errors cannot be compared, but in the absence of any more + // meaningful way to compare the errors we compare the kind of error + // and ignore the embedded source error or OS error. The main use + // case for comparing errors outputted by the XDR library is for + // error case testing, and a lack of the ability to compare has a + // detrimental affect on failure testing, so this is a tradeoff. + #[cfg(feature = "std")] + (Self::Io(l), Self::Io(r)) => l.kind() == r.kind(), + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + #[cfg(feature = "std")] impl error::Error for Error { #[must_use] diff --git a/src/lib.rs b/src/lib.rs index b293c51c..9e41bd8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,3 +20,8 @@ pub use scval_conversions::*; mod scval_validations; #[cfg(feature = "next")] pub use scval_validations::*; + +#[cfg(all(feature = "alloc", feature = "next"))] +mod scmap; +#[cfg(all(feature = "alloc", feature = "next"))] +pub use scmap::*; diff --git a/src/next.rs b/src/next.rs index c48e1c8f..bfcabb75 100644 --- a/src/next.rs +++ b/src/next.rs @@ -91,6 +91,9 @@ use std::{ io::{BufRead, BufReader, Cursor, Read, Write}, }; +/// Error contains all errors returned by functions in this crate. It can be +/// compared via `PartialEq`, however any contained IO errors will only be +/// compared on their `ErrorKind`. #[derive(Debug)] pub enum Error { Invalid, @@ -102,6 +105,23 @@ pub enum Error { Io(io::Error), } +impl PartialEq for Error { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Utf8Error(l), Self::Utf8Error(r)) => l == r, + // IO errors cannot be compared, but in the absence of any more + // meaningful way to compare the errors we compare the kind of error + // and ignore the embedded source error or OS error. The main use + // case for comparing errors outputted by the XDR library is for + // error case testing, and a lack of the ability to compare has a + // detrimental affect on failure testing, so this is a tradeoff. + #[cfg(feature = "std")] + (Self::Io(l), Self::Io(r)) => l.kind() == r.kind(), + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + #[cfg(feature = "std")] impl error::Error for Error { #[must_use] diff --git a/src/scmap.rs b/src/scmap.rs new file mode 100644 index 00000000..904d5ef8 --- /dev/null +++ b/src/scmap.rs @@ -0,0 +1,75 @@ +#![allow(clippy::missing_errors_doc)] + +use crate::{Error, ScMap, ScMapEntry, ScVal, Validate}; +extern crate alloc; +use alloc::vec::Vec; + +impl ScMap { + pub fn sorted_from_entries(entries: I) -> Result + where + E: TryInto, + I: Iterator, + { + let mut v = entries + .map(TryInto::try_into) + .collect::, _>>() + .map_err(|_| Error::Invalid)?; + // TODO: Add tests that prove order consistency of ScVal with RawVal. https://github.com/stellar/rs-stellar-xdr/issues/117 + v.sort_by(|a, b| a.key.cmp(&b.key)); + let m = ScMap(v.try_into()?); + // `validate` will further check that there are no duplicates. + m.validate()?; + Ok(m) + } + + pub fn sorted_from_pairs(pairs: I) -> Result + where + K: TryInto, + V: TryInto, + I: Iterator, + { + Self::sorted_from_entries(pairs) + } + + pub fn sorted_from(src: I) -> Result + where + E: TryInto, + I: IntoIterator, + { + Self::sorted_from_entries(src.into_iter()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use alloc::{collections::BTreeMap, vec}; + + #[test] + fn scmap_from_map() -> Result<(), ()> { + let mut m: BTreeMap = BTreeMap::new(); + m.insert(1, 2); + m.insert(5, 6); + m.insert(3, 4); + let scm = ScMap::sorted_from(m)?; + assert_eq!(scm.0.first().unwrap().key, 1u32.into()); + assert_eq!(scm.0.last().unwrap().key, 5u32.into()); + Ok(()) + } + + #[test] + fn scmap_from_pairs() -> Result<(), ()> { + let pairs: Vec<(u32, u32)> = vec![(3, 4), (5, 6), (1, 2)]; + let scm = ScMap::sorted_from(pairs)?; + assert_eq!(scm.0.first().unwrap().key, 1u32.into()); + assert_eq!(scm.0.last().unwrap().key, 5u32.into()); + Ok(()) + } + + #[test] + fn scmap_from_pairs_containing_duplicate_keys() { + let pairs: Vec<(u32, u32)> = vec![(3, 4), (3, 5), (5, 6), (1, 2)]; + let scm = ScMap::sorted_from(pairs); + assert!(scm.is_err()); + } +} diff --git a/src/scval_conversions.rs b/src/scval_conversions.rs index 9f405a72..012489e1 100644 --- a/src/scval_conversions.rs +++ b/src/scval_conversions.rs @@ -1,5 +1,6 @@ use crate::{ - Hash, PublicKey, ScBigInt, ScHash, ScMap, ScObject, ScStatic, ScStatus, ScSymbol, ScVal, ScVec, + Hash, PublicKey, ScBigInt, ScHash, ScMap, ScMapEntry, ScObject, ScStatic, ScStatus, ScSymbol, + ScVal, ScVec, }; #[cfg(all(not(feature = "std"), feature = "alloc"))] @@ -10,6 +11,8 @@ use alloc::{string::String, vec, vec::Vec}; #[cfg(feature = "num-bigint")] use num_bigint::{BigInt, Sign}; +// TODO: Use the Error type for conversions in this file. + impl From for ScVal { fn from(v: ScStatic) -> Self { Self::Static(v) @@ -723,6 +726,21 @@ impl TryFrom for ScMap { } } +impl TryFrom<(K, V)> for ScMapEntry +where + K: TryInto, + V: TryInto, +{ + type Error = (); + + fn try_from(v: (K, V)) -> Result { + Ok(ScMapEntry { + key: v.0.try_into().map_err(|_| ())?, + val: v.1.try_into().map_err(|_| ())?, + }) + } +} + // TODO: Add conversions from std::collections::HashMap, im_rcOrdMap, and other // popular map types to ScMap. diff --git a/src/scval_validations.rs b/src/scval_validations.rs index dfb7621c..cecbffc4 100644 --- a/src/scval_validations.rs +++ b/src/scval_validations.rs @@ -1,6 +1,6 @@ #![allow(clippy::missing_errors_doc)] -use crate::{ScMap, ScObject, ScVal}; +use crate::{Error, ScMap, ScObject, ScVal}; pub trait Validate { type Error; @@ -8,7 +8,7 @@ pub trait Validate { } impl Validate for ScVal { - type Error = (); + type Error = Error; fn validate(&self) -> Result<(), Self::Error> { match self { @@ -17,7 +17,7 @@ impl Validate for ScVal { if *i >= 0 { Ok(()) } else { - Err(()) + Err(Error::Invalid) } } @@ -28,7 +28,7 @@ impl Validate for ScVal { { Ok(()) } else { - Err(()) + Err(Error::Invalid) } } @@ -37,11 +37,11 @@ impl Validate for ScVal { if b & 0x0fff_ffff_ffff_ffff == *b { Ok(()) } else { - Err(()) + Err(Error::Invalid) } } - ScVal::Object(None) => Err(()), + ScVal::Object(None) => Err(Error::Invalid), ScVal::Object(Some(o)) => match o { ScObject::Map(m) => m.validate(), @@ -63,39 +63,134 @@ impl Validate for ScVal { } impl Validate for ScMap { - type Error = (); + type Error = Error; fn validate(&self) -> Result<(), Self::Error> { - // TODO: Validate that the map is sorted and has no duplicates, or find - // a way to guarantee this to be the case. - todo!() + // Check the map is sorted by key, and there are no keys that are + // duplicates. + if self.windows(2).all(|w| w[0].key < w[1].key) { + Ok(()) + } else { + Err(Error::Invalid) + } } } #[cfg(test)] mod test { - use crate::{ScVal, Validate}; + use crate::{Error, ScVal, Validate}; #[test] fn u63() { assert_eq!(ScVal::U63(0).validate(), Ok(())); assert_eq!(ScVal::U63(1).validate(), Ok(())); assert_eq!(ScVal::U63(i64::MAX).validate(), Ok(())); - assert_eq!(ScVal::U63(-1).validate(), Err(())); + assert_eq!(ScVal::U63(-1).validate(), Err(Error::Invalid)); } #[test] fn symbol() { assert_eq!(ScVal::Symbol("".try_into().unwrap()).validate(), Ok(())); assert_eq!(ScVal::Symbol("a0A_".try_into().unwrap()).validate(), Ok(())); - assert_eq!(ScVal::Symbol("]".try_into().unwrap()).validate(), Err(())); + assert_eq!( + ScVal::Symbol("]".try_into().unwrap()).validate(), + Err(Error::Invalid) + ); } #[test] fn bitset() { assert_eq!(ScVal::Bitset(0x0000_0000_0000_0000).validate(), Ok(())); assert_eq!(ScVal::Bitset(0x0fff_ffff_ffff_ffff).validate(), Ok(())); - assert_eq!(ScVal::Bitset(0x1000_0000_0000_0000).validate(), Err(())); - assert_eq!(ScVal::Bitset(0x1fff_ffff_ffff_ffff).validate(), Err(())); + assert_eq!( + ScVal::Bitset(0x1000_0000_0000_0000).validate(), + Err(Error::Invalid) + ); + assert_eq!( + ScVal::Bitset(0x1fff_ffff_ffff_ffff).validate(), + Err(Error::Invalid) + ); + } + + #[test] + #[cfg(feature = "alloc")] + fn map() { + extern crate alloc; + use crate::{ScMap, ScMapEntry, ScObject}; + use alloc::vec; + // Maps should be sorted by key and have no duplicates. The sort order + // is just the "normal" sort order on ScVal emitted by derive(PartialOrd). + assert_eq!( + ScVal::Object(Some(ScObject::Map(ScMap( + vec![ + ScMapEntry { + key: ScVal::U63(0), + val: ScVal::U32(1), + }, + ScMapEntry { + key: ScVal::U63(1), + val: ScVal::U63(1), + } + ] + .try_into() + .unwrap() + )))) + .validate(), + Ok(()) + ); + assert_eq!( + ScVal::Object(Some(ScObject::Map(ScMap( + vec![ + ScMapEntry { + key: ScVal::U63(0), + val: ScVal::U63(1), + }, + ScMapEntry { + key: ScVal::U63(1), + val: ScVal::U63(1), + } + ] + .try_into() + .unwrap() + )))) + .validate(), + Ok(()) + ); + assert_eq!( + ScVal::Object(Some(ScObject::Map(ScMap( + vec![ + ScMapEntry { + key: ScVal::U63(2), + val: ScVal::U63(1), + }, + ScMapEntry { + key: ScVal::U63(1), + val: ScVal::U63(1), + } + ] + .try_into() + .unwrap() + )))) + .validate(), + Err(Error::Invalid) + ); + assert_eq!( + ScVal::Object(Some(ScObject::Map(ScMap( + vec![ + ScMapEntry { + key: ScVal::U32(1), + val: ScVal::U63(1), + }, + ScMapEntry { + key: ScVal::U63(2), + val: ScVal::U63(1), + }, + ] + .try_into() + .unwrap() + )))) + .validate(), + Err(Error::Invalid) + ); } }