Skip to content

Commit

Permalink
Render asset codes as strings in JSON (#324)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
leighmcculloch authored Dec 1, 2023
1 parent 5dcb7a4 commit 9e7c662
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 43 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 = []

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 1 addition & 36 deletions src/curr/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, Self::Err> {
hex::decode(s).map_err(|_| Error::InvalidHex)?.try_into()
}
}
impl From<AssetCode4> for [u8; 4] {
#[must_use]
fn from(x: AssetCode4) -> Self {
Expand Down Expand Up @@ -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<Self, Self::Err> {
hex::decode(s).map_err(|_| Error::InvalidHex)?.try_into()
}
}
impl From<AssetCode12> for [u8; 12] {
#[must_use]
fn from(x: AssetCode12) -> Self {
Expand Down Expand Up @@ -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 {
Expand Down
75 changes: 73 additions & 2 deletions src/curr/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<stellar_strkey::DecodeError> for Error {
Expand Down Expand Up @@ -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<Self, Self::Err> {
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<Self, Self::Err> {
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<Self, Self::Err> {
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),
}
}
}
2 changes: 1 addition & 1 deletion tests/serde_tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ fn test_serde_tx() -> Result<(), Box<dyn std::error::Error>> {
"change_trust": {
"line": {
"credit_alphanum4": {
"asset_code": "41424344",
"asset_code": "ABCD",
"issuer": "GBB5BH2JFIVOHKQK5WHM5XFSE2SPOUFJB3FU4CPZVR3EUVJXZLMHOLOM"
}
},
Expand Down
147 changes: 145 additions & 2 deletions tests/str.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"))));
}

0 comments on commit 9e7c662

Please sign in to comment.