diff --git a/ergotree-ir/src/mir/constant.rs b/ergotree-ir/src/mir/constant.rs index ae61153e9..9c7263259 100644 --- a/ergotree-ir/src/mir/constant.rs +++ b/ergotree-ir/src/mir/constant.rs @@ -65,6 +65,8 @@ pub enum Literal { Int(i32), /// Long Long(i64), + /// String type + String(Arc), /// Big integer BigInt(BigInt256), /// Sigma property @@ -115,6 +117,7 @@ impl std::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), } } } @@ -172,6 +175,7 @@ impl std::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})"), } } } @@ -361,6 +365,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()), @@ -592,6 +597,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)] @@ -830,6 +853,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 {:?}", + std::any::type_name::(), + v + ))), + } + } +} + impl TryExtractFrom for AvlTreeData { fn try_extract_from(v: Literal) -> Result { match v { @@ -1118,6 +1154,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))] @@ -1250,5 +1299,11 @@ 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 9f01c9b69..4749d5670 100644 --- a/ergotree-ir/src/mir/value.rs +++ b/ergotree-ir/src/mir/value.rs @@ -194,6 +194,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 @@ -238,6 +240,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, @@ -308,6 +311,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()), @@ -378,6 +382,7 @@ impl std::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), diff --git a/ergotree-ir/src/serialization/data.rs b/ergotree-ir/src/serialization/data.rs index 49a1ef5e1..2a0ecdce6 100644 --- a/ergotree-ir/src/serialization/data.rs +++ b/ergotree-ir/src/serialization/data.rs @@ -38,6 +38,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)?, @@ -94,6 +98,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 32a008191..3060359ef 100644 --- a/ergotree-ir/src/serialization/types.rs +++ b/ergotree-ir/src/serialization/types.rs @@ -104,7 +104,7 @@ pub enum TypeCode { SBOX = 99, SAVL_TREE = 100, SCONTEXT = 101, - // SSTRING = 102, + SSTRING = 102, STYPE_VAR = 103, SHEADER = 104, SPRE_HEADER = 105, @@ -320,6 +320,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, @@ -358,6 +359,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), @@ -380,7 +382,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)?; @@ -388,7 +391,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)?; @@ -415,7 +418,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)?; @@ -423,7 +427,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)?; @@ -511,9 +515,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 41efd553d..bf1be6476 100644 --- a/ergotree-ir/src/types/stype.rs +++ b/ergotree-ir/src/types/stype.rs @@ -59,6 +59,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 @@ -91,6 +93,7 @@ impl SType { | SType::SBox | SType::SAvlTree | SType::SContext + | SType::SString | SType::SBoolean | SType::SHeader | SType::SPreHeader @@ -149,6 +152,7 @@ impl std::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"), @@ -293,6 +297,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),