From 9e7c662d1b51e3a02bb9797fe631bc2d4af599bf Mon Sep 17 00:00:00 2001 From: Leigh McCulloch <351529+leighmcculloch@users.noreply.github.com> Date: Fri, 1 Dec 2023 02:19:47 -0800 Subject: [PATCH] Render asset codes as strings in JSON (#324) * Render asset codes as strings in JSON * fix * fix doc comment * fix test * Escape asset code strings preserving their values * test * fix * upd version of escape-bytes --- Cargo.lock | 7 ++ Cargo.toml | 3 +- Makefile | 2 +- src/curr/generated.rs | 37 +---------- src/curr/str.rs | 75 ++++++++++++++++++++- tests/serde_tx.rs | 2 +- tests/str.rs | 147 +++++++++++++++++++++++++++++++++++++++++- 7 files changed, 230 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b9cd805f..f08da150 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -244,6 +244,12 @@ dependencies = [ "syn 1.0.98", ] +[[package]] +name = "escape-bytes" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3871d161fec5b6fade5fe7afe2e196b86839c5526a8d256c7b1a04dbbe5241a4" + [[package]] name = "fnv" version = "1.0.7" @@ -493,6 +499,7 @@ dependencies = [ "base64 0.13.0", "clap", "crate-git-revision", + "escape-bytes", "hex", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index c2c35dea..69f6fd65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ stellar-strkey = { version = "0.0.8", optional = true } base64 = { version = "0.13.0", optional = true } serde = { version = "1.0.139", features = ["derive"], optional = true } serde_with = { version = "3.0.0", optional = true } +escape-bytes = { version = "0.1.0", default-features = false, optional = true } hex = { version = "0.4.3", optional = true } arbitrary = {version = "1.1.3", features = ["derive"], optional = true} clap = { version = "4.2.4", default-features = false, features = ["std", "derive", "usage", "help"], optional = true } @@ -35,7 +36,7 @@ serde_json = "1.0.89" [features] default = ["std", "curr"] std = ["alloc"] -alloc = ["dep:hex", "dep:stellar-strkey"] +alloc = ["dep:hex", "dep:stellar-strkey", "dep:escape-bytes"] curr = [] next = [] diff --git a/Makefile b/Makefile index 4e00fe21..767c9f7d 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ CARGO_HACK_ARGS=--feature-powerset --exclude-features default --group-features b CARGO_DOC_ARGS?=--open XDRGEN_VERSION=cbff4b31 -XDRGEN_TYPES_CUSTOM_STR_IMPL=PublicKey,AccountId,MuxedAccount,MuxedAccountMed25519,SignerKey,SignerKeyEd25519SignedPayload,NodeId,ScAddress +XDRGEN_TYPES_CUSTOM_STR_IMPL=PublicKey,AccountId,MuxedAccount,MuxedAccountMed25519,SignerKey,SignerKeyEd25519SignedPayload,NodeId,ScAddress,AssetCode,AssetCode4,AssetCode12 all: build test diff --git a/src/curr/generated.rs b/src/curr/generated.rs index 10bd4dc0..79e18d06 100644 --- a/src/curr/generated.rs +++ b/src/curr/generated.rs @@ -10345,23 +10345,6 @@ impl core::fmt::Debug for AssetCode4 { Ok(()) } } -impl core::fmt::Display for AssetCode4 { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let v = &self.0; - for b in v { - write!(f, "{b:02x}")?; - } - Ok(()) - } -} - -#[cfg(feature = "alloc")] -impl core::str::FromStr for AssetCode4 { - type Err = Error; - fn from_str(s: &str) -> core::result::Result { - hex::decode(s).map_err(|_| Error::InvalidHex)?.try_into() - } -} impl From for [u8; 4] { #[must_use] fn from(x: AssetCode4) -> Self { @@ -10461,23 +10444,6 @@ impl core::fmt::Debug for AssetCode12 { Ok(()) } } -impl core::fmt::Display for AssetCode12 { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - let v = &self.0; - for b in v { - write!(f, "{b:02x}")?; - } - Ok(()) - } -} - -#[cfg(feature = "alloc")] -impl core::str::FromStr for AssetCode12 { - type Err = Error; - fn from_str(s: &str) -> core::result::Result { - hex::decode(s).map_err(|_| Error::InvalidHex)?.try_into() - } -} impl From for [u8; 12] { #[must_use] fn from(x: AssetCode12) -> Self { @@ -10689,8 +10655,7 @@ impl WriteXdr for AssetType { #[cfg_attr(feature = "arbitrary", derive(Arbitrary))] #[cfg_attr( all(feature = "serde", feature = "alloc"), - derive(serde::Serialize, serde::Deserialize), - serde(rename_all = "snake_case") + derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr) )] #[allow(clippy::large_enum_variant)] pub enum AssetCode { diff --git a/src/curr/str.rs b/src/curr/str.rs index c5ad3e8b..c00d65bb 100644 --- a/src/curr/str.rs +++ b/src/curr/str.rs @@ -13,11 +13,16 @@ //# - SignerKey //# - SignerKeyEd25519SignedPayload //# - NodeId +//# +//# ## Asset Codes +//# - AssetCode +//# - AssetCode4 +//# - AssetCode12 #![cfg(feature = "alloc")] use super::{ - AccountId, Error, Hash, MuxedAccount, MuxedAccountMed25519, NodeId, PublicKey, ScAddress, - SignerKey, SignerKeyEd25519SignedPayload, Uint256, + AccountId, AssetCode, AssetCode12, AssetCode4, Error, Hash, MuxedAccount, MuxedAccountMed25519, + NodeId, PublicKey, ScAddress, SignerKey, SignerKeyEd25519SignedPayload, Uint256, }; impl From for Error { @@ -254,3 +259,69 @@ impl core::fmt::Display for ScAddress { Ok(()) } } + +impl core::str::FromStr for AssetCode4 { + type Err = Error; + fn from_str(s: &str) -> core::result::Result { + let mut code = AssetCode4([0u8; 4]); + escape_bytes::unescape_into(&mut code.0, s.as_bytes()).map_err(|_| Error::Invalid)?; + Ok(code) + } +} + +impl core::fmt::Display for AssetCode4 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if let Some(last_idx) = self.0.iter().rposition(|c| *c != 0) { + for b in escape_bytes::Escape::new(&self.0[..=last_idx]) { + write!(f, "{}", b as char)?; + } + } + Ok(()) + } +} + +impl core::str::FromStr for AssetCode12 { + type Err = Error; + fn from_str(s: &str) -> core::result::Result { + let mut code = AssetCode12([0u8; 12]); + escape_bytes::unescape_into(&mut code.0, s.as_bytes()).map_err(|_| Error::Invalid)?; + Ok(code) + } +} + +impl core::fmt::Display for AssetCode12 { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if let Some(last_idx) = self.0.iter().rposition(|c| *c != 0) { + for b in escape_bytes::Escape::new(&self.0[..=last_idx]) { + write!(f, "{}", b as char)?; + } + } + Ok(()) + } +} + +impl core::str::FromStr for AssetCode { + type Err = Error; + fn from_str(s: &str) -> core::result::Result { + let mut code = [0u8; 12]; + let n = escape_bytes::unescape_into(&mut code, s.as_bytes()).map_err(|_| Error::Invalid)?; + if n <= 4 { + Ok(AssetCode::CreditAlphanum4(AssetCode4([ + code[0], code[1], code[2], code[3], + ]))) + } else if n <= 12 { + Ok(AssetCode::CreditAlphanum12(AssetCode12(code))) + } else { + Err(Error::Invalid) + } + } +} + +impl core::fmt::Display for AssetCode { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + AssetCode::CreditAlphanum4(c) => c.fmt(f), + AssetCode::CreditAlphanum12(c) => c.fmt(f), + } + } +} diff --git a/tests/serde_tx.rs b/tests/serde_tx.rs index 9ec4904c..1db87f1a 100644 --- a/tests/serde_tx.rs +++ b/tests/serde_tx.rs @@ -68,7 +68,7 @@ fn test_serde_tx() -> Result<(), Box> { "change_trust": { "line": { "credit_alphanum4": { - "asset_code": "41424344", + "asset_code": "ABCD", "issuer": "GBB5BH2JFIVOHKQK5WHM5XFSE2SPOUFJB3FU4CPZVR3EUVJXZLMHOLOM" } }, diff --git a/tests/str.rs b/tests/str.rs index 57e129f8..1ffb2e99 100644 --- a/tests/str.rs +++ b/tests/str.rs @@ -4,8 +4,8 @@ use stellar_xdr::curr as stellar_xdr; use stellar_xdr::{ - AccountId, Error, Hash, MuxedAccount, MuxedAccountMed25519, NodeId, PublicKey, ScAddress, - SignerKey, SignerKeyEd25519SignedPayload, Uint256, + AccountId, AssetCode, AssetCode12, AssetCode4, Error, Hash, MuxedAccount, MuxedAccountMed25519, + NodeId, PublicKey, ScAddress, SignerKey, SignerKeyEd25519SignedPayload, Uint256, }; use std::str::FromStr; @@ -398,3 +398,146 @@ fn sc_address_from_str_with_invalid() { ); assert_eq!(v, Err(Error::Invalid)); } + +#[test] +fn asset_code_4_from_str() { + assert_eq!(AssetCode4::from_str(""), Ok(AssetCode4(*b"\0\0\0\0"))); + assert_eq!(AssetCode4::from_str("a"), Ok(AssetCode4(*b"a\0\0\0"))); + assert_eq!(AssetCode4::from_str("ab"), Ok(AssetCode4(*b"ab\0\0"))); + assert_eq!(AssetCode4::from_str("abc"), Ok(AssetCode4(*b"abc\0"))); + assert_eq!(AssetCode4::from_str("abcd"), Ok(AssetCode4(*b"abcd"))); + + assert_eq!(AssetCode4::from_str("abcde"), Err(Error::Invalid)); +} + +#[test] +fn asset_code_4_to_string() { + assert_eq!(AssetCode4(*b"\0\0\0\0").to_string(), ""); + assert_eq!(AssetCode4(*b"a\0\0\0").to_string(), "a"); + assert_eq!(AssetCode4(*b"ab\0\0").to_string(), "ab"); + assert_eq!(AssetCode4(*b"abc\0").to_string(), "abc"); + assert_eq!(AssetCode4(*b"abcd").to_string(), "abcd"); + + // Preserve as much of the code as possible, even if it contains nul bytes. + assert_eq!(AssetCode4(*b"a\0cd").to_string(), r"a\0cd"); + + // Replace bytes that are not valid utf8 with the replacement character � and preserve length. + assert_eq!(AssetCode4(*b"a\xc3\x28d").to_string(), r"a\xc3(d"); + assert_eq!(AssetCode4(*b"a\xc3\xc3\x28").to_string(), r"a\xc3\xc3("); + assert_eq!(AssetCode4(*b"a\xc3\xc3\xc3").to_string(), r"a\xc3\xc3\xc3"); +} + +#[test] +#[rustfmt::skip] +fn asset_code_12_from_str() { + assert_eq!(AssetCode12::from_str(""), Ok(AssetCode12(*b"\0\0\0\0\0\0\0\0\0\0\0\0"))); + assert_eq!(AssetCode12::from_str("a"), Ok(AssetCode12(*b"a\0\0\0\0\0\0\0\0\0\0\0"))); + assert_eq!(AssetCode12::from_str("ab"), Ok(AssetCode12(*b"ab\0\0\0\0\0\0\0\0\0\0"))); + assert_eq!(AssetCode12::from_str("abc"), Ok(AssetCode12(*b"abc\0\0\0\0\0\0\0\0\0"))); + assert_eq!(AssetCode12::from_str("abcd"), Ok(AssetCode12(*b"abcd\0\0\0\0\0\0\0\0"))); + assert_eq!(AssetCode12::from_str("abcde"), Ok(AssetCode12(*b"abcde\0\0\0\0\0\0\0"))); + assert_eq!(AssetCode12::from_str("abcdef"), Ok(AssetCode12(*b"abcdef\0\0\0\0\0\0"))); + assert_eq!(AssetCode12::from_str("abcdefg"), Ok(AssetCode12(*b"abcdefg\0\0\0\0\0"))); + assert_eq!(AssetCode12::from_str("abcdefgh"), Ok(AssetCode12(*b"abcdefgh\0\0\0\0"))); + assert_eq!(AssetCode12::from_str("abcdefghi"), Ok(AssetCode12(*b"abcdefghi\0\0\0"))); + assert_eq!(AssetCode12::from_str("abcdefghij"), Ok(AssetCode12(*b"abcdefghij\0\0"))); + assert_eq!(AssetCode12::from_str("abcdefghijk"), Ok(AssetCode12(*b"abcdefghijk\0"))); + assert_eq!(AssetCode12::from_str("abcdefghijkl"), Ok(AssetCode12(*b"abcdefghijkl"))); + + assert_eq!(AssetCode12::from_str("abcdefghijklm"), Err(Error::Invalid)); +} + +#[test] +#[rustfmt::skip] +fn asset_code_12_to_string() { + assert_eq!(AssetCode12(*b"\0\0\0\0\0\0\0\0\0\0\0\0").to_string(), ""); + assert_eq!(AssetCode12(*b"a\0\0\0\0\0\0\0\0\0\0\0").to_string(), "a"); + assert_eq!(AssetCode12(*b"ab\0\0\0\0\0\0\0\0\0\0").to_string(), "ab"); + assert_eq!(AssetCode12(*b"abc\0\0\0\0\0\0\0\0\0").to_string(), "abc"); + assert_eq!(AssetCode12(*b"abcd\0\0\0\0\0\0\0\0").to_string(), "abcd"); + assert_eq!(AssetCode12(*b"abcde\0\0\0\0\0\0\0").to_string(), "abcde"); + assert_eq!(AssetCode12(*b"abcdef\0\0\0\0\0\0").to_string(), "abcdef"); + assert_eq!(AssetCode12(*b"abcdefg\0\0\0\0\0").to_string(), "abcdefg"); + assert_eq!(AssetCode12(*b"abcdefgh\0\0\0\0").to_string(), "abcdefgh"); + assert_eq!(AssetCode12(*b"abcdefghi\0\0\0").to_string(), "abcdefghi"); + assert_eq!(AssetCode12(*b"abcdefghij\0\0").to_string(), "abcdefghij"); + assert_eq!(AssetCode12(*b"abcdefghijk\0").to_string(), "abcdefghijk"); + assert_eq!(AssetCode12(*b"abcdefghijkl").to_string(), "abcdefghijkl"); + + // Preserve as much of the code as possible, even if it contains nul bytes. + assert_eq!(AssetCode12(*b"a\0cd\0\0\0\0\0\0\0\0").to_string(), r"a\0cd"); + + // Replace bytes that are not valid utf8 with the replacement character � and preserve length. + assert_eq!(AssetCode12(*b"a\xc3\x28d\0\0\0\0\0\0\0\0").to_string(), r"a\xc3(d"); + assert_eq!(AssetCode12(*b"a\xc3\xc3\x28d\0\0\0\0\0\0\0").to_string(), r"a\xc3\xc3(d"); +} + +#[test] +#[rustfmt::skip] +fn asset_code_from_str() { + assert_eq!(AssetCode::from_str(""), Ok(AssetCode::CreditAlphanum4(AssetCode4(*b"\0\0\0\0")))); + assert_eq!(AssetCode::from_str("a"), Ok(AssetCode::CreditAlphanum4(AssetCode4(*b"a\0\0\0")))); + assert_eq!(AssetCode::from_str("ab"), Ok(AssetCode::CreditAlphanum4(AssetCode4(*b"ab\0\0")))); + assert_eq!(AssetCode::from_str("abc"), Ok(AssetCode::CreditAlphanum4(AssetCode4(*b"abc\0")))); + assert_eq!(AssetCode::from_str("abcd"), Ok(AssetCode::CreditAlphanum4(AssetCode4(*b"abcd")))); + + assert_eq!(AssetCode::from_str("abcde"), Ok(AssetCode::CreditAlphanum12(AssetCode12(*b"abcde\0\0\0\0\0\0\0")))); + assert_eq!(AssetCode::from_str("abcdef"), Ok(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdef\0\0\0\0\0\0")))); + assert_eq!(AssetCode::from_str("abcdefg"), Ok(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefg\0\0\0\0\0")))); + assert_eq!(AssetCode::from_str("abcdefgh"), Ok(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefgh\0\0\0\0")))); + assert_eq!(AssetCode::from_str("abcdefghi"), Ok(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefghi\0\0\0")))); + assert_eq!(AssetCode::from_str("abcdefghij"), Ok(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefghij\0\0")))); + assert_eq!(AssetCode::from_str("abcdefghijk"), Ok(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefghijk\0")))); + assert_eq!(AssetCode::from_str("abcdefghijkl"), Ok(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefghijkl")))); + + assert_eq!(AssetCode::from_str("abcdefghijklm"), Err(Error::Invalid)); +} + +#[test] +#[rustfmt::skip] +fn asset_code_to_string() { + assert_eq!(AssetCode::CreditAlphanum4(AssetCode4(*b"\0\0\0\0")).to_string(), ""); + assert_eq!(AssetCode::CreditAlphanum4(AssetCode4(*b"a\0\0\0")).to_string(), "a"); + assert_eq!(AssetCode::CreditAlphanum4(AssetCode4(*b"ab\0\0")).to_string(), "ab"); + assert_eq!(AssetCode::CreditAlphanum4(AssetCode4(*b"abc\0")).to_string(), "abc"); + assert_eq!(AssetCode::CreditAlphanum4(AssetCode4(*b"abcd")).to_string(), "abcd"); + + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"\0\0\0\0\0\0\0\0\0\0\0\0")).to_string(), ""); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"a\0\0\0\0\0\0\0\0\0\0\0")).to_string(), "a"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"ab\0\0\0\0\0\0\0\0\0\0")).to_string(), "ab"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abc\0\0\0\0\0\0\0\0\0")).to_string(), "abc"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abcd\0\0\0\0\0\0\0\0")).to_string(), "abcd"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abcde\0\0\0\0\0\0\0")).to_string(), "abcde"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdef\0\0\0\0\0\0")).to_string(), "abcdef"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefg\0\0\0\0\0")).to_string(), "abcdefg"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefgh\0\0\0\0")).to_string(), "abcdefgh"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefghi\0\0\0")).to_string(), "abcdefghi"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefghij\0\0")).to_string(), "abcdefghij"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefghijk\0")).to_string(), "abcdefghijk"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"abcdefghijkl")).to_string(), "abcdefghijkl"); + + // Preserve as much of the code as possible, even if it contains nul bytes. + assert_eq!(AssetCode::CreditAlphanum4(AssetCode4(*b"a\0cd")).to_string(), r"a\0cd"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"a\0cd\0\0\0\0\0\0\0\0")).to_string(), r"a\0cd"); + + // Replace bytes that are not valid utf8 with the replacement character � and preserve length. + assert_eq!(AssetCode::CreditAlphanum4(AssetCode4(*b"a\xc3\x28d")).to_string(), r"a\xc3(d"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"a\xc3\x28d\0\0\0\0\0\0\0\0")).to_string(), r"a\xc3(d"); + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"a\xc3\xc3\x28d\0\0\0\0\0\0\0")).to_string(), r"a\xc3\xc3(d"); +} + +#[test] +#[rustfmt::skip] +fn asset_code_from_str_to_string_roundtrip_unicode() { + // Round tripped to correct variant based on byte length, not code point length. + assert_eq!(AssetCode::CreditAlphanum12(AssetCode12(*b"a\xd9\xaa\xd9\xaa\0\0\0\0\0\0\0")).to_string(), r"a\xd9\xaa\xd9\xaa"); + assert_eq!(AssetCode::from_str(r"a\xd9\xaa\xd9\xaa"), Ok(AssetCode::CreditAlphanum12(AssetCode12(*b"a\xd9\xaa\xd9\xaa\0\0\0\0\0\0\0")))); + + // Round tripped to correct variant based on byte length even when utf8 + // parsing error occurs. To preserve type consistency when round tripping + // the data, the length when parsing errors occur must be consistent with + // the input length, which is why a nul byte is expected instead of a + // Unicode Replacement Character, which would be two bytes. + assert_eq!(AssetCode::CreditAlphanum4(AssetCode4(*b"a\xc3\xc3d")).to_string(), r"a\xc3\xc3d"); + assert_eq!(AssetCode::from_str(r"a\xc3\xc3d"), Ok(AssetCode::CreditAlphanum4(AssetCode4(*b"a\xc3\xc3d")))); +}