Repository: ergoplatform/sigma-rust (language: rust)

Issue:
[100 SigmaUSD] Serialization for ergo-p2p
In to sigma-ser crate add `ScorexSerializable` trait with `scorex_serialize` and `scorex_parse` methods similar to `SigmaSerializable` trait and implement helper `scorex_serialize_bytes` and `scorex_parse_bytes`. Parametrize with `ReadSigmaVlqExt` and `WriteSigmaVlqExt` as reader/writer.
Add `put_short_string` and `get_short_string` methods for `*SigmaVlqExt` for `String` shorter than 256 bytes. In the implementation, first store length in bytes as u8 and then string as bytes. See Scala implementation - https://github.com/ScorexFoundation/scorex-util/blob/00827a0a56a84f1ccb44b78aa92b00e72c8c8efb/src/main/scala/scorex/util/serialization/VLQWriter.scala#L150-L162



Discussion / hints:
I'd like to take this one.

Fix patch:
diff --git a/sigma-ser/Cargo.toml b/sigma-ser/Cargo.toml
index ae305a477..4b3fb99f8 100644
--- a/sigma-ser/Cargo.toml
+++ b/sigma-ser/Cargo.toml
@@ -17,7 +17,7 @@ crate-type = ["cdylib", "rlib"]
 # wasm-bindgen = "0.2"
 thiserror = "1"
 bitvec = "0.22.3"
-
+bounded-vec = { version = "^0.5.0" }
 # The `console_error_panic_hook` crate provides better debugging of panics by
 # logging them with `console.error`. This is great for development, but requires
 # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
diff --git a/sigma-ser/src/lib.rs b/sigma-ser/src/lib.rs
index 690a0780e..520f85d9a 100644
--- a/sigma-ser/src/lib.rs
+++ b/sigma-ser/src/lib.rs
@@ -17,6 +17,11 @@
 #![deny(clippy::panic)]
 #![deny(clippy::wildcard_enum_match_arm)]
 
+/// ScoreX Serialization
+mod scorex_serialize;
+pub use scorex_serialize::{
+    scorex_serialize_roundtrip, ScorexParsingError, ScorexSerializable, ScorexSerializationError,
+};
 /// VLQ encoder
 pub mod vlq_encode;
 /// ZigZag encoder
diff --git a/sigma-ser/src/scorex_serialize.rs b/sigma-ser/src/scorex_serialize.rs
new file mode 100644
index 000000000..187141650
--- /dev/null
+++ b/sigma-ser/src/scorex_serialize.rs
@@ -0,0 +1,198 @@
+use std::convert::TryInto;
+use std::io;
+
+use crate::vlq_encode;
+use crate::vlq_encode::*;
+use bounded_vec::{BoundedVec, BoundedVecOutOfBounds};
+use thiserror::Error;
+
+/// Ways serialization might fail
+#[derive(Error, Eq, PartialEq, Debug, Clone)]
+pub enum ScorexSerializationError {
+    /// IO fail (EOF, etc.)
+    #[error("IO error: {0}")]
+    Io(String),
+    /// Serialization not yet implemented
+    #[error("serialization not yet implemented: {0}")]
+    NotImplementedYet(&'static str),
+    /// Serialization not supported
+    #[error("serialization not supported: {0}")]
+    NotSupported(&'static str),
+}
+
+impl From<io::Error> for ScorexSerializationError {
+    fn from(error: io::Error) -> Self {
+        ScorexSerializationError::Io(error.to_string())
+    }
+}
+
+/// Ways parsing might fail
+#[derive(Error, Eq, PartialEq, Debug, Clone)]
+pub enum ScorexParsingError {
+    /// Invalid op code
+    #[error("invalid op code: {0}")]
+    InvalidOpCode(u8),
+    /// Lacking support for the op
+    #[error("not implemented op error")]
+    NotImplementedOpCode(String),
+    /// Failed to parse type
+    #[error("type parsing error")]
+    InvalidTypeCode(u8),
+    /// Failed to decode VLQ
+    #[error("vlq encode error: {0}")]
+    VlqEncode(#[from] vlq_encode::VlqEncodingError),
+    /// IO fail (EOF, etc.)
+    #[error("IO error: {0}")]
+    Io(String),
+    /// Misc fail
+    #[error("misc error")]
+    Misc(String),
+    /// Feature not yet implemented
+    #[error("parsing not yet implemented: {0}")]
+    NotImplementedYet(String),
+    /// Value out of bounds
+    #[error("Value out of bounds: {0}")]
+    ValueOutOfBounds(String),
+    /// Tuple items out of bounds
+    #[error("Tuple items out of bounds: {0}")]
+    TupleItemsOutOfBounds(usize),
+    /// Feature not supported
+    #[error("parsing not supported: {0}")]
+    NotSupported(&'static str),
+    /// Serialization error
+    #[error("serialization error: {0}")]
+    SerializationError(#[from] ScorexSerializationError),
+    /// Invalid item quantity for BoundedVec
+    #[error("Invalid item quantity for BoundedVec: {0}")]
+    BoundedVecOutOfBounds(#[from] BoundedVecOutOfBounds),
+}
+
+impl From<io::Error> for ScorexParsingError {
+    fn from(error: io::Error) -> Self {
+        ScorexParsingError::Io(error.to_string())
+    }
+}
+
+impl From<&io::Error> for ScorexParsingError {
+    fn from(error: &io::Error) -> Self {
+        ScorexParsingError::Io(error.to_string())
+    }
+}
+
+/// Result type for [`ScorexSerializable::scorex_serialize`]
+pub type ScorexSerializeResult = Result<(), ScorexSerializationError>;
+
+/// Scorex Serializable Trait.
+pub trait ScorexSerializable: Sized {
+    /// Write `self` to the `writer`
+    fn scorex_serialize<W: WriteSigmaVlqExt>(&self, w: &mut W) -> ScorexSerializeResult;
+    /// parse `self` from `reader`
+    fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError>;
+
+    /// Serialize a ScorexSerializable value into bytes
+    fn scorex_serialize_bytes(&self) -> Result<Vec<u8>, ScorexParsingError> {
+        let mut w = vec![];
+        self.scorex_serialize(&mut w)?;
+        Ok(w)
+    }
+    /// Parse `self` from the bytes
+    fn scorex_parse_bytes(mut bytes: &[u8]) -> Result<Self, ScorexParsingError> {
+        Self::scorex_parse(&mut bytes)
+    }
+}
+
+impl<T: ScorexSerializable> ScorexSerializable for Vec<T> {
+    fn scorex_serialize<W: WriteSigmaVlqExt>(&self, w: &mut W) -> ScorexSerializeResult {
+        w.put_u32(self.len() as u32)?;
+        self.iter().try_for_each(|i| i.scorex_serialize(w))
+    }
+
+    fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError> {
+        let items_count = r.get_u32()?;
+        let mut items = Vec::with_capacity(items_count as usize);
+        for _ in 0..items_count {
+            items.push(T::scorex_parse(r)?);
+        }
+        Ok(items)
+    }
+}
+
+impl<T: ScorexSerializable, const L: usize, const U: usize> ScorexSerializable
+    for BoundedVec<T, L, U>
+{
+    fn scorex_serialize<W: WriteSigmaVlqExt>(&self, w: &mut W) -> ScorexSerializeResult {
+        self.as_vec().scorex_serialize(w)
+    }
+
+    fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError> {
+        Ok(Vec::<T>::scorex_parse(r)?.try_into()?)
+    }
+}
+
+/// Corresponds to `VLQ(UInt)` format from `ErgoTree` spec.
+impl ScorexSerializable for u32 {
+    fn scorex_serialize<W: WriteSigmaVlqExt>(&self, w: &mut W) -> ScorexSerializeResult {
+        w.put_u32(*self)?;
+        Ok(())
+    }
+    fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError> {
+        let v = r.get_u32()?;
+        Ok(v)
+    }
+}
+
+impl<T: ScorexSerializable> ScorexSerializable for Option<Box<T>> {
+    fn scorex_serialize<W: WriteSigmaVlqExt>(&self, w: &mut W) -> ScorexSerializeResult {
+        match self {
+            Some(v) => {
+                w.put_u8(1)?;
+                v.scorex_serialize(w)
+            }
+            None => Ok(w.put_u8(0)?),
+        }
+    }
+
+    fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError> {
+        let tag = r.get_u8()?;
+        Ok(if tag != 0 {
+            Some(T::scorex_parse(r)?.into())
+        } else {
+            None
+        })
+    }
+}
+
+/// serialization roundtrip
+#[allow(clippy::expect_used)]
+pub fn scorex_serialize_roundtrip<T: ScorexSerializable>(v: &T) -> T {
+    let mut data = Vec::new();
+    v.scorex_serialize(&mut data).expect("serialization failed");
+    let reader = &mut &data[..];
+    T::scorex_parse(reader).expect("parse failed")
+}
+
+#[allow(clippy::unwrap_used)]
+#[cfg(test)]
+#[allow(clippy::panic)]
+mod test {
+    use super::*;
+    use proptest::collection::vec;
+    use proptest::prelude::*;
+
+    proptest! {
+        #[test]
+        fn u32_roundtrip(val in any::<u32>()) {
+            assert_eq!(scorex_serialize_roundtrip(&val), val);
+        }
+
+        #[test]
+        fn vec_roundtrip(val in vec(any::<u32>(), 0..255)) {
+            assert_eq!(scorex_serialize_roundtrip(&val), val);
+        }
+
+        #[test]
+        fn box_roundtrip(val in any::<Option<Box<u32>>>()) {
+            assert_eq!(scorex_serialize_roundtrip(&val), val);
+        }
+    }
+}
diff --git a/sigma-ser/src/vlq_encode.rs b/sigma-ser/src/vlq_encode.rs
index 2241c575b..c18d7dba0 100644
--- a/sigma-ser/src/vlq_encode.rs
+++ b/sigma-ser/src/vlq_encode.rs
@@ -116,6 +116,19 @@ pub trait WriteSigmaVlqExt: io::Write {
     fn put_i16_be_bytes(&mut self, v: i16) -> io::Result<()> {
         self.write_all(v.to_be_bytes().as_ref())
     }
+
+    /// Put a short string (< 256 bytes) into the writer. Writes length (as u8) and string bytes to the writer
+    fn put_short_string(&mut self, s: &str) -> io::Result<()> {
+        if s.len() > 255 {
+            return Err(io::Error::new(
+                io::ErrorKind::Unsupported,
+                "Serializing strings with more than 255 bytes is not allowed",
+            ));
+        }
+        self.put_u8(s.len() as u8)?;
+        self.write_all(s.as_bytes())?;
+        Ok(())
+    }
 }
 
 /// Mark all types implementing `Write` as implementing the extension.
@@ -196,6 +209,15 @@ pub trait ReadSigmaVlqExt: io::Read {
         bits.truncate(size);
         Ok(bits.iter().map(|x| *x).collect::<Vec<bool>>())
     }
+
+    /// Reads a string from the reader. Reads a byte (size), and the string
+    fn get_short_string(&mut self) -> Result<String, VlqEncodingError> {
+        let size_bytes = self.get_u8()?;
+        let mut bytes = vec![0u8; size_bytes as usize];
+        self.read_exact(&mut bytes)?;
+        let string = String::from_utf8(bytes).map_err(|_| VlqEncodingError::VlqDecodingFailed)?;
+        Ok(string)
+    }
 }
 
 /// Mark all types implementing `Read` as implementing the extension.
@@ -881,6 +903,15 @@ mod tests {
             prop_assert_eq![bits.clone(), r.get_bits(bits.len()).unwrap()];
         }
 
+        #[test]
+        fn prop_short_string_roundtrip(s in ".{1,255}".prop_filter("Filter strings that are too large", |s| s.len() < 256)) {
+            let mut w = Cursor::new(vec![]);
+            w.put_short_string(&s).unwrap();
+            let inner = w.into_inner();
+            prop_assert_eq!(inner[0] as usize, s.len());
+            prop_assert_eq!(std::str::from_utf8(&inner[1..]), Ok(&*s));
+        }
+
         #[test]
         fn arbitrary_values_list(vals in collection::vec(any::<Val>(), 0..100)) {
             let mut w = Cursor::new(vec![]);


Test patch: