diff --git a/ergotree-ir/src/mir/constant.rs b/ergotree-ir/src/mir/constant.rs index f07107bbd..c6c705fe8 100644 --- a/ergotree-ir/src/mir/constant.rs +++ b/ergotree-ir/src/mir/constant.rs @@ -70,6 +70,8 @@ pub enum Literal { Int(i32), /// Long Long(i64), + /// String type + String(Arc), /// Big integer BigInt(BigInt256), /// Sigma property @@ -120,6 +122,7 @@ impl core::fmt::Debug for Literal { Literal::GroupElement(v) => v.fmt(f), Literal::AvlTree(v) => v.fmt(f), Literal::CBox(v) => v.fmt(f), + Literal::String(v) => v.fmt(f), } } } @@ -177,6 +180,7 @@ impl core::fmt::Display for Literal { Literal::GroupElement(v) => v.fmt(f), Literal::AvlTree(v) => write!(f, "AvlTree({:?})", v), Literal::CBox(v) => write!(f, "ErgoBox({:?})", v), + Literal::String(v) => write!(f, "String({v})"), } } } @@ -366,6 +370,7 @@ impl<'ctx> TryFrom> for Constant { } } Value::AvlTree(a) => Ok(Constant::from(*a)), + Value::String(s) => Ok(Constant::from(s)), Value::Context => Err("Cannot convert Value::Context into Constant".into()), Value::Header(_) => Err("Cannot convert Value::Header(_) into Constant".into()), Value::PreHeader(_) => Err("Cannot convert Value::PreHeader(_) into Constant".into()), @@ -597,6 +602,24 @@ impl From for Constant { } } +impl From> for Constant { + fn from(s: Arc) -> Self { + Constant { + tpe: SType::SString, + v: Literal::String(s), + } + } +} + +impl From for Constant { + fn from(s: String) -> Self { + Constant { + tpe: SType::SString, + v: Literal::String(s.into()), + } + } +} + #[allow(clippy::unwrap_used)] #[allow(clippy::from_over_into)] #[impl_for_tuples(2, 4)] @@ -835,6 +858,19 @@ impl TryExtractFrom for BigInt256 { } } +impl TryExtractFrom for String { + fn try_extract_from(v: Literal) -> Result { + match v { + Literal::String(s) => Ok(String::from(&*s)), + _ => Err(TryExtractFromError(format!( + "expected {:?}, found {:?}", + core::any::type_name::(), + v + ))), + } + } +} + impl TryExtractFrom for AvlTreeData { fn try_extract_from(v: Literal) -> Result { match v { @@ -1124,6 +1160,19 @@ pub mod tests { test_constant_roundtrip(()); } + // test that invalid strings don't error but are instead parsed lossily to match reference impl + #[test] + fn parse_invalid_string() { + let mut bytes = Constant::from(".".to_string()) + .sigma_serialize_bytes() + .unwrap(); + *bytes.last_mut().unwrap() = 0xf0; + assert_eq!( + Constant::sigma_parse_bytes(&bytes).unwrap().v, + Literal::String("�".into()) + ); + } + proptest! { #![proptest_config(ProptestConfig::with_cases(8))] @@ -1256,5 +1305,10 @@ pub mod tests { test_constant_roundtrip(v); } + #[test] + fn string_roundtrip(v in any::()) { + test_constant_roundtrip(v); + } + } } diff --git a/ergotree-ir/src/mir/value.rs b/ergotree-ir/src/mir/value.rs index 9991bb703..1783226ed 100644 --- a/ergotree-ir/src/mir/value.rs +++ b/ergotree-ir/src/mir/value.rs @@ -198,6 +198,8 @@ pub enum Value<'ctx> { Tup(TupleItems>), /// Transaction(and blockchain) context info Context, + /// String type + String(Arc), /// Block header Header(Box
), /// Header with predictable data @@ -242,6 +244,7 @@ impl<'ctx> Value<'ctx> { .unwrap(), ), Value::Context => Value::Context, + Value::String(s) => Value::String(s.clone()), Value::Header(h) => Value::Header(h.clone()), Value::PreHeader(h) => Value::PreHeader(h.clone()), Value::Global => Value::Global, @@ -312,6 +315,7 @@ impl From for Value<'static> { Literal::Int(i) => Value::Int(i), Literal::Long(l) => Value::Long(l), Literal::BigInt(b) => Value::BigInt(b), + Literal::String(s) => Value::String(s), Literal::Unit => Value::Unit, Literal::SigmaProp(s) => Value::SigmaProp(s), Literal::GroupElement(e) => Value::GroupElement(e.into()), @@ -382,6 +386,7 @@ impl core::fmt::Display for Value<'_> { Value::Int(v) => v.fmt(f), Value::Long(v) => write!(f, "{}L", v), Value::BigInt(v) => v.fmt(f), + Value::String(v) => v.fmt(f), Value::SigmaProp(v) => v.fmt(f), Value::GroupElement(v) => v.fmt(f), Value::AvlTree(v) => write!(f, "AvlTree({:?})", v), @@ -405,6 +410,7 @@ impl StoreWrapped for i64 {} impl StoreWrapped for BigInt256 {} impl StoreWrapped for Header {} impl StoreWrapped for ErgoBox {} +impl StoreWrapped for Arc {} impl StoreWrapped for Ref<'_, ErgoBox> {} impl StoreWrapped for EcPoint {} impl StoreWrapped for SigmaProp {} diff --git a/ergotree-ir/src/serialization/data.rs b/ergotree-ir/src/serialization/data.rs index 50943e19b..ee6279639 100644 --- a/ergotree-ir/src/serialization/data.rs +++ b/ergotree-ir/src/serialization/data.rs @@ -1,5 +1,6 @@ use alloc::boxed::Box; +use alloc::string::String; use alloc::string::ToString; use alloc::vec; use alloc::vec::Vec; @@ -44,6 +45,10 @@ impl DataSerializer { Literal::BigInt(v) => { v.sigma_serialize(w)?; } + Literal::String(s) => { + w.put_usize_as_u32_unwrapped(s.len())?; + w.write_all(s.as_bytes())?; + } Literal::GroupElement(ecp) => ecp.sigma_serialize(w)?, Literal::SigmaProp(s) => s.value().sigma_serialize(w)?, Literal::AvlTree(a) => a.sigma_serialize(w)?, @@ -101,6 +106,12 @@ impl DataSerializer { SShort => Literal::Short(r.get_i16()?), SInt => Literal::Int(r.get_i32()?), SLong => Literal::Long(r.get_i64()?), + SString => { + let len = r.get_u32()?; + let mut buf = vec![0; len as usize]; + r.read_exact(&mut buf)?; + Literal::String(String::from_utf8_lossy(&buf).into()) + } SBigInt => Literal::BigInt(BigInt256::sigma_parse(r)?), SUnit => Literal::Unit, SGroupElement => Literal::GroupElement(Arc::new(EcPoint::sigma_parse(r)?)), diff --git a/ergotree-ir/src/serialization/types.rs b/ergotree-ir/src/serialization/types.rs index 850a2ff5f..6e5ba5ad7 100644 --- a/ergotree-ir/src/serialization/types.rs +++ b/ergotree-ir/src/serialization/types.rs @@ -106,7 +106,7 @@ pub enum TypeCode { SBOX = 99, SAVL_TREE = 100, SCONTEXT = 101, - // SSTRING = 102, + SSTRING = 102, STYPE_VAR = 103, SHEADER = 104, SPRE_HEADER = 105, @@ -322,6 +322,7 @@ impl SType { TypeCode::SBOX => SBox, TypeCode::SAVL_TREE => SAvlTree, TypeCode::SCONTEXT => SContext, + TypeCode::SSTRING => SString, TypeCode::STYPE_VAR => STypeVar(stype_param::STypeVar::sigma_parse(r)?), TypeCode::SHEADER => SHeader, TypeCode::SPRE_HEADER => SPreHeader, @@ -362,6 +363,7 @@ impl SigmaSerializable for SType { SType::SBox => TypeCode::SBOX.sigma_serialize(w), SType::SAvlTree => TypeCode::SAVL_TREE.sigma_serialize(w), SType::SContext => TypeCode::SCONTEXT.sigma_serialize(w), + SType::SString => TypeCode::SSTRING.sigma_serialize(w), SType::SHeader => TypeCode::SHEADER.sigma_serialize(w), SType::SPreHeader => TypeCode::SPRE_HEADER.sigma_serialize(w), SType::SGlobal => TypeCode::SGLOBAL.sigma_serialize(w), @@ -384,7 +386,8 @@ impl SigmaSerializable for SType { SGroupElement => TypeCode::OPTION_COLL_GROUP_ELEMENT.sigma_serialize(w), SSigmaProp => TypeCode::OPTION_COLL_SIGMAPROP.sigma_serialize(w), STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | SColl(_) - | STuple(_) | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal => { + | STuple(_) | SFunc(_) | SContext | SString | SHeader | SPreHeader + | SGlobal => { // if not "embeddable" type fallback to generic Option type code following // elem type code TypeCode::OPTION.sigma_serialize(w)?; @@ -392,7 +395,7 @@ impl SigmaSerializable for SType { } }, STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | STuple(_) - | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal => { + | SFunc(_) | SContext | SString | SHeader | SPreHeader | SGlobal => { // if not "embeddable" type fallback to generic Option type code following // elem type code TypeCode::OPTION.sigma_serialize(w)?; @@ -419,7 +422,8 @@ impl SigmaSerializable for SType { SGroupElement => TypeCode::NESTED_COLL_GROUP_ELEMENT.sigma_serialize(w), SSigmaProp => TypeCode::NESTED_COLL_SIGMAPROP.sigma_serialize(w), STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | SColl(_) - | STuple(_) | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal => { + | STuple(_) | SFunc(_) | SContext | SString | SHeader | SPreHeader + | SGlobal => { // if not "embeddable" type fallback to generic Coll type code following // elem type code TypeCode::COLL.sigma_serialize(w)?; @@ -427,7 +431,7 @@ impl SigmaSerializable for SType { } }, STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | STuple(_) - | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal => { + | SFunc(_) | SContext | SString | SHeader | SPreHeader | SGlobal => { // if not "embeddable" type fallback to generic Coll type code following // elem type code TypeCode::COLL.sigma_serialize(w)?; @@ -515,9 +519,11 @@ impl SigmaSerializable for SType { } ( STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | SColl(_) - | STuple(_) | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal, + | STuple(_) | SFunc(_) | SContext | SString | SHeader | SPreHeader + | SGlobal, STypeVar(_) | SAny | SUnit | SBox | SAvlTree | SOption(_) | SColl(_) - | STuple(_) | SFunc(_) | SContext | SHeader | SPreHeader | SGlobal, + | STuple(_) | SFunc(_) | SContext | SString | SHeader | SPreHeader + | SGlobal, ) => { // Pair of non-primitive types (`(SBox, SAvlTree)`, `((Int, Byte), (Boolean,Box))`, etc.) TypeCode::TUPLE_PAIR1.sigma_serialize(w)?; diff --git a/ergotree-ir/src/types/stype.rs b/ergotree-ir/src/types/stype.rs index f73ffc0d2..31bb7744a 100644 --- a/ergotree-ir/src/types/stype.rs +++ b/ergotree-ir/src/types/stype.rs @@ -1,5 +1,6 @@ //! SType hierarchy +use alloc::string::String; use alloc::sync::Arc; use alloc::vec::Vec; use core::convert::TryInto; @@ -60,6 +61,8 @@ pub enum SType { SFunc(SFunc), /// Context object ("CONTEXT" in ErgoScript) SContext, + /// UTF-8 String type + SString, /// Header of a block SHeader, /// Header of a block without solved mining puzzle @@ -92,6 +95,7 @@ impl SType { | SType::SBox | SType::SAvlTree | SType::SContext + | SType::SString | SType::SBoolean | SType::SHeader | SType::SPreHeader @@ -150,6 +154,7 @@ impl core::fmt::Display for SType { SType::STuple(t) => write!(f, "{}", t), SType::SFunc(t) => write!(f, "{}", t), SType::SContext => write!(f, "Context"), + SType::SString => write!(f, "String"), SType::SHeader => write!(f, "Header"), SType::SPreHeader => write!(f, "PreHeader"), SType::SGlobal => write!(f, "Global"), @@ -259,6 +264,12 @@ impl LiftIntoSType for AvlTreeData { } } +impl LiftIntoSType for String { + fn stype() -> SType { + SType::SString + } +} + impl LiftIntoSType for Option { fn stype() -> SType { SType::SOption(Arc::new(T::stype())) @@ -295,6 +306,7 @@ pub(crate) mod tests { Just(SType::SBox), Just(SType::SAvlTree), Just(SType::SContext), + Just(SType::SString), Just(SType::SHeader), Just(SType::SPreHeader), Just(SType::SGlobal),