diff --git a/soroban-env-host/src/native_contract/base_types.rs b/soroban-env-host/src/native_contract/base_types.rs index 12d96bde6..c63f524d4 100644 --- a/soroban-env-host/src/native_contract/base_types.rs +++ b/soroban-env-host/src/native_contract/base_types.rs @@ -2,7 +2,7 @@ use crate::host::metered_clone::MeteredClone; use crate::host::{Host, HostError}; use core::cmp::Ordering; -use soroban_env_common::xdr::{AccountId, ScAddress}; +use soroban_env_common::xdr::{AccountId, ScAddress, ScErrorCode, ScErrorType}; use soroban_env_common::{ AddressObject, BytesObject, Compare, ConversionError, Env, EnvBase, MapObject, StringObject, TryFromVal, Val, VecObject, @@ -57,22 +57,18 @@ impl From for StringObject { } } -// TODO: check metering impl String { - pub fn copy_to_rust_string(&self, env: &Host) -> Result { - let len: u32 = env - .string_len(self.object) - .map_err(|_| ConversionError)? - .into(); - let len = len as usize; - let mut vec = std::vec![0; len]; - env.string_copy_to_slice(self.object, Val::U32_ZERO, &mut vec) - .map_err(|_| ConversionError)?; - std::string::String::from_utf8(vec).map_err(|_| ConversionError.into()) - } - pub fn to_array(&self) -> Result<[u8; N], HostError> { let mut slice = [0_u8; N]; + let len = self.host.string_len(self.object)?; + if u32::from(len) as usize != N { + return Err(self.host.err( + ScErrorType::Value, + ScErrorCode::UnexpectedSize, + "String had unexpected size", + &[len.to_val()], + )); + } self.host .string_copy_to_slice(self.object, Val::U32_ZERO, &mut slice)?; Ok(slice) diff --git a/soroban-env-host/src/native_contract/token/metadata.rs b/soroban-env-host/src/native_contract/token/metadata.rs index 00d378159..095427122 100644 --- a/soroban-env-host/src/native_contract/token/metadata.rs +++ b/soroban-env-host/src/native_contract/token/metadata.rs @@ -1,8 +1,12 @@ +use std::fmt::Write; + use soroban_native_sdk_macros::contracttype; use stellar_strkey::ed25519; -use crate::{host::Host, HostError}; -use soroban_env_common::{Env, EnvBase, StorageType, SymbolSmall, TryFromVal, TryIntoVal}; +use crate::{host::Host, native_contract::base_types::BytesN, HostError}; +use soroban_env_common::{ + ConversionError, Env, EnvBase, StorageType, SymbolSmall, TryFromVal, TryIntoVal, +}; use crate::native_contract::base_types::String; @@ -20,6 +24,138 @@ pub struct TokenMetadata { pub const DECIMAL: u32 = 7; +// This does a specific and fairly unique escaping transformation as defined +// in TxRep / SEP-0011. +fn render_sep0011_asset_code( + buf: &[u8], + out: &mut std::string::String, +) -> Result<(), ConversionError> { + if buf.len() != 4 && buf.len() != 12 { + return Err(ConversionError); + } + for (i, x) in buf.iter().enumerate() { + match *x { + // When dealing with a 4-byte asset code we stop at the first NUL. + 0 if buf.len() == 4 => break, + // When dealing with a 12-byte asset code we continue escaping NULs + // as \x00 until past the 5th byte, so that the result is + // unambiguously different than a 4-byte code. + 0 if buf.len() == 12 && i > 4 => break, + b':' | b'\\' | 0..=0x20 | 0x7f..=0xff => { + write!(out, r"\x{:02x}", x).map_err(|_| ConversionError)? + } + _ => out.push(*x as char), + } + } + Ok(()) +} + +#[test] +fn test_render_sep0011_asset_code() { + fn check_pair(a: &[u8], b: &str) { + let mut out = std::string::String::new(); + render_sep0011_asset_code(a, &mut out).unwrap(); + assert_eq!(out, b); + } + // 4 byte codes + check_pair(&[0, 0, 0, 0], r""); + check_pair(&[0, b'X', b'L', b'M'], r""); + check_pair(&[b'X', 0, 0, 0], r"X"); + check_pair(&[b'X', b'L', 0, 0], r"XL"); + check_pair(&[b'X', b'L', b'M', 0], r"XLM"); + check_pair(&[b'y', b'X', b'L', b'M'], r"yXLM"); + check_pair(&[1, b'X', b'L', b'M'], r"\x01XLM"); + check_pair(&[b'X', 1, b'L', b'M'], r"X\x01LM"); + check_pair(&[b'X', b':', b'L', b'M'], r"X\x3aLM"); + check_pair(&[b'X', b'\\', b'L', b'M'], r"X\x5cLM"); + check_pair(&[b'1', b'!', b'/', b'0'], r"1!/0"); + + // 12 byte codes + check_pair( + &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + r"\x00\x00\x00\x00\x00", + ); + check_pair( + &[0, b'X', b'L', b'M', 0, 0, 0, 0, 0, 0, 0, 0], + r"\x00XLM\x00", + ); + check_pair( + &[b'X', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + r"X\x00\x00\x00\x00", + ); + check_pair( + &[b'X', b'L', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + r"XL\x00\x00\x00", + ); + check_pair( + &[b'X', b'L', b'M', 0, 0, 0, 0, 0, 0, 0, 0, 0], + r"XLM\x00\x00", + ); + check_pair( + &[b'y', b'X', b'L', b'M', 0, 0, 0, 0, 0, 0, 0, 0], + r"yXLM\x00", + ); + check_pair( + &[1, b'X', b'L', b'M', 0, 0, 0, 0, 0, 0, 0, 0], + r"\x01XLM\x00", + ); + check_pair( + &[b'X', 1, b'L', b'M', 0, 0, 0, 0, 0, 0, 0, 0], + r"X\x01LM\x00", + ); + check_pair( + &[b'X', b':', b'L', b'M', 0, 0, 0, 0, 0, 0, 0, 0], + r"X\x3aLM\x00", + ); + check_pair( + &[b'X', b'\\', b'L', b'M', 0, 0, 0, 0, 0, 0, 0, 0], + r"X\x5cLM\x00", + ); + check_pair( + &[b'X', b'L', b'M', b'L', b'X', 0, 0, 0, 0, 0, 0, 0], + r"XLMLX", + ); + check_pair( + &[b'X', b'L', b'M', b'L', b'X', b'A', 0, 0, 0, 0, 0, 0], + r"XLMLXA", + ); + check_pair( + &[ + b'1', b'!', b'/', b'0', b'a', b'<', b'N', b'!', b'[', b'K', b'z', b'^', + ], + r"1!/0a( + e: &Host, + symbol: String, + issuer: BytesN<32>, +) -> Result<(String, String), HostError> { + let symbuf = symbol.to_array::()?; + let strkey_len = 56; + + // Biggest resulting string has each byte escaped to 4 bytes. + let capacity = symbuf.len() * 4 + 1 + strkey_len; + + // We also have to charge for strkey_len again since PublicKey::to_string does + // a std::string::String allocation of its own. + let charge = capacity + strkey_len; + e.charge_budget( + crate::xdr::ContractCostType::HostMemAlloc, + Some(charge as u64), + )?; + + let mut s: std::string::String = std::string::String::with_capacity(capacity); + render_sep0011_asset_code(&symbuf, &mut s)?; + s.push(':'); + s.push_str(&ed25519::PublicKey(issuer.to_array()?).to_string()); + Ok(( + String::try_from_val(e, &e.string_new_from_slice(s.as_str())?)?, + symbol, + )) +} + pub fn set_metadata(e: &Host) -> Result<(), HostError> { let name_and_symbol: (String, String) = match read_asset_info(e)? { AssetInfo::Native => { @@ -27,26 +163,10 @@ pub fn set_metadata(e: &Host) -> Result<(), HostError> { (n.clone(), n) } AssetInfo::AlphaNum4(asset) => { - let symbol: String = asset.asset_code; - let mut name = symbol.copy_to_rust_string(e)?; - name.push(':'); - let k = ed25519::PublicKey(asset.issuer.to_array()?); - name.push_str(k.to_string().as_str()); - ( - String::try_from_val(e, &e.string_new_from_slice(name.as_str())?)?, - symbol, - ) + render_sep0011_asset::<4>(e, asset.asset_code, asset.issuer)? } AssetInfo::AlphaNum12(asset) => { - let symbol: String = asset.asset_code; - let mut name = symbol.copy_to_rust_string(e)?; - name.push(':'); - let k = ed25519::PublicKey(asset.issuer.to_array()?); - name.push_str(k.to_string().as_str()); - ( - String::try_from_val(e, &e.string_new_from_slice(name.as_str())?)?, - symbol, - ) + render_sep0011_asset::<12>(e, asset.asset_code, asset.issuer)? } };