From 415b49a32e78f1c191e17cf48bd8c602cf69e0e0 Mon Sep 17 00:00:00 2001 From: Assaf Morami Date: Tue, 27 Jun 2023 20:03:55 +0300 Subject: [PATCH 1/3] Skeleton for MetaMask permits --- .gitignore | 1 - Cargo.lock | 468 ++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 5 +- src/permit.rs | 44 ++++- 4 files changed, 510 insertions(+), 8 deletions(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index 96ef6c0..ea8c4bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ /target -Cargo.lock diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1e630d8 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,468 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cosmwasm-schema" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2196586ea197eaa21129d09c84a19e2eb80bdce239eec8e6a4f108cb644c295f" +dependencies = [ + "schemars", + "serde_json", +] + +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "proc-macro2" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "query-authentication" +version = "0.1.0" +dependencies = [ + "bech32", + "cosmwasm-schema", + "remain", + "ripemd160", + "rust-crypto", + "schemars", + "secp256k1", + "secret-cosmwasm-std", + "secret-cosmwasm-storage", + "serde", + "sha2", + "snafu", +] + +[[package]] +name = "quote" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ac302d8f83c0c1974bf758f6b041c6c8ada916fbb44a609158ca8b064cc76c" +dependencies = [ + "libc", + "rand 0.4.6", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "remain" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13cca257d068dd3a390d04b2c3009a3fad2ee5048dfa8f239d048372810470c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.22", +] + +[[package]] +name = "ripemd160" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" +dependencies = [ + "block-buffer", + "digest", + "opaque-debug", +] + +[[package]] +name = "rust-crypto" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76d05d3993fd5f4af9434e8e436db163a12a9d40e1a58a726f27a01dfd12a2a" +dependencies = [ + "gcc", + "libc", + "rand 0.3.23", + "rustc-serialize", + "time", +] + +[[package]] +name = "rustc-serialize" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda" + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "schemars" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be77ed66abed6954aabf6a3e31a84706bedbf93750d267e92ef4a6d90bbd6a61" +dependencies = [ + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11af7a475c9ee266cfaa9e303a47c830ebe072bf3101ab907a7b7b9d816fa01d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 1.0.109", +] + +[[package]] +name = "secp256k1" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" +dependencies = [ + "cc", +] + +[[package]] +name = "secret-cosmwasm-std" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4b8fed972d924458d9c3c0e6c9fbf6c4c5e30655571e3d2b78be056d316e9" +dependencies = [ + "base64", + "schemars", + "serde", + "serde-json-wasm", + "snafu", +] + +[[package]] +name = "secret-cosmwasm-storage" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffee3243bb13c02ddcdc15458526288d36ec23422ec43681ac5d48e7325d8327" +dependencies = [ + "secret-cosmwasm-std", + "serde", +] + +[[package]] +name = "serde" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-json-wasm" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120bad73306616e91acd7ceed522ba96032a51cffeef3cc813de7f367df71e37" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.22", +] + +[[package]] +name = "serde_derive_internals" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dbab34ca63057a1f15280bdf3c39f2b1eb1b54c17e98360e511637aef7418c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_json" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer", + "cfg-if", + "cpufeatures", + "digest", + "opaque-debug", +] + +[[package]] +name = "snafu" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1508efa03c362e23817f96cde18abed596a25219a8b2c66e8db33c03543d315b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi", + "winapi", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-ident" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index 38d4dfc..3ff6438 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,6 @@ version = "0.1.0" edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] cosmwasm-std = { version = "0.10.1", package = "secret-cosmwasm-std" } cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } @@ -16,5 +15,5 @@ remain = "0.2.2" ripemd160 = "0.9.1" secp256k1 = "0.20.3" bech32 = "0.8.1" - -sha2 = { version = "0.9.1", default-features = false } \ No newline at end of file +sha2 = { version = "0.9.1", default-features = false } +rust-crypto = "0.2.36" diff --git a/src/permit.rs b/src/permit.rs index 8eca398..907dde0 100644 --- a/src/permit.rs +++ b/src/permit.rs @@ -2,6 +2,8 @@ use crate::sha_256; use crate::transaction::{PermitSignature, PubKeyValue, SignedTx}; use bech32::FromBase32; use cosmwasm_std::{to_binary, Api, Binary, CanonicalAddr, StdError, StdResult, Uint128}; +use crypto::digest::Digest; +use crypto::sha3::Sha3; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -43,7 +45,8 @@ impl Permit { ) -> StdResult { let pubkey = &signature.pub_key.value; - // Validate signature + // Try validating Cosmos signature + let signed_bytes = to_binary(signed_tx)?; let signed_bytes_hash = sha_256(signed_bytes.as_slice()); @@ -51,14 +54,47 @@ impl Permit { .secp256k1_verify(&signed_bytes_hash, &signature.signature.0, &pubkey.0) .map_err(|err| StdError::generic_err(err.to_string()))?; - if !verified { - return Err(StdError::generic_err("Signature verification failed")); + if verified { + return Ok(PubKeyValue(pubkey.clone())); + } + + // Try validating Ethereum signature + + let mut signed_bytes = vec![]; + signed_bytes.extend_from_slice(b"\x19Ethereum Signed Message:\n"); + + // TODO: figure out how to serialize signed_tx as a JSON with an indent of 4 + let signed_tx_pretty_amino_json = to_binary_pretty(signed_tx)?; + + signed_bytes.extend_from_slice(signed_tx_pretty_amino_json.len().to_string().as_bytes()); + signed_bytes.extend_from_slice(signed_tx_pretty_amino_json.as_slice()); + + let mut hasher = Sha3::keccak256(); + + hasher.input(&signed_bytes); + + let mut signed_bytes_hash = [0u8; 32]; + hasher.result(&mut signed_bytes_hash); + + let verified = api + .secp256k1_verify(&signed_bytes_hash, &signature.signature.0, &pubkey.0) + .map_err(|err| StdError::generic_err(err.to_string()))?; + + if verified { + return Ok(PubKeyValue(pubkey.clone())); } - Ok(PubKeyValue(pubkey.clone())) + return Err(StdError::generic_err("Signature verification failed")); } } +fn to_binary_pretty(data: &T) -> StdResult +where + T: Serialize + ?Sized, +{ + todo!(); +} + #[cfg(test)] mod signature_tests { use super::*; From 9ca75be489b67ca53920d1b95018134274b58185 Mon Sep 17 00:00:00 2001 From: kent-3 <100624004+kent-3@users.noreply.github.com> Date: Wed, 5 Jul 2023 03:04:52 -0400 Subject: [PATCH 2/3] use jsonxf crate for pretty print JSON --- Cargo.lock | 44 +++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 2 ++ src/permit.rs | 29 ++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e630d8..d4cb3b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,18 +91,43 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "jsonxf" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6889ea54a6add10ed8a757719ec88293201265fa7fe56e09ae66b6df038a6" +dependencies = [ + "getopts", + "memchr", +] + [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -124,6 +149,7 @@ version = "0.1.0" dependencies = [ "bech32", "cosmwasm-schema", + "jsonxf", "remain", "ripemd160", "rust-crypto", @@ -132,6 +158,7 @@ dependencies = [ "secret-cosmwasm-std", "secret-cosmwasm-storage", "serde", + "serde-json-wasm 0.5.1", "sha2", "snafu", ] @@ -289,7 +316,7 @@ dependencies = [ "base64", "schemars", "serde", - "serde-json-wasm", + "serde-json-wasm 0.2.3", "snafu", ] @@ -321,6 +348,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-json-wasm" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.164" @@ -433,6 +469,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 3ff6438..728d4f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ cosmwasm-std = { version = "0.10.1", package = "secret-cosmwasm-std" } cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } cosmwasm-schema = "0.10.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } +serde-json-wasm = "0.5.1" +jsonxf = "1.1" snafu = { version = "0.6.3" } schemars = "0.7" remain = "0.2.2" diff --git a/src/permit.rs b/src/permit.rs index 907dde0..53d9b28 100644 --- a/src/permit.rs +++ b/src/permit.rs @@ -1,7 +1,7 @@ use crate::sha_256; use crate::transaction::{PermitSignature, PubKeyValue, SignedTx}; use bech32::FromBase32; -use cosmwasm_std::{to_binary, Api, Binary, CanonicalAddr, StdError, StdResult, Uint128}; +use cosmwasm_std::{to_binary, to_vec, Api, Binary, CanonicalAddr, StdError, StdResult, Uint128}; use crypto::digest::Digest; use crypto::sha3::Sha3; use schemars::JsonSchema; @@ -63,7 +63,6 @@ impl Permit { let mut signed_bytes = vec![]; signed_bytes.extend_from_slice(b"\x19Ethereum Signed Message:\n"); - // TODO: figure out how to serialize signed_tx as a JSON with an indent of 4 let signed_tx_pretty_amino_json = to_binary_pretty(signed_tx)?; signed_bytes.extend_from_slice(signed_tx_pretty_amino_json.len().to_string().as_bytes()); @@ -92,7 +91,18 @@ fn to_binary_pretty(data: &T) -> StdResult where T: Serialize + ?Sized, { - todo!(); + // Serialize the data to a compact JSON string + let ugly_json = serde_json_wasm::to_string(data).unwrap(); + + // Customize Formatter to use 4 spaces for indents + let mut formatter = jsonxf::Formatter::pretty_printer(); + formatter.indent = String::from(" "); + + // Transform the JSON string to be pretty + let pretty_json = formatter.format(&ugly_json).unwrap(); + + // Serialize the JSON string to a JSON byte vector + to_vec(&pretty_json).map(Binary) } #[cfg(test)] @@ -167,6 +177,19 @@ mod signature_tests { permit.params.some_number = Uint128(100); // NOTE: SN mock deps dont have a valid working implementation of the dep functons for some reason //assert!(permit.validate(&deps.api, None).is_err()); + + // Serialize the data to a compact JSON string + let ugly_json = serde_json_wasm::to_string(&permit).unwrap(); + + // Customize Formatter to use 4 spaces for indents + let mut formatter = jsonxf::Formatter::pretty_printer(); + formatter.indent = String::from(" "); + + // Transform the JSON string to be pretty + let pretty_json = formatter.format(&ugly_json).unwrap(); + + // Print the JSON string + println!("{}", pretty_json); } const FILLERPERMITNAME: &str = "wasm/MsgExecuteContract"; From 4cbeca317a9db12e50dcced5d1586c9e31f3abfc Mon Sep 17 00:00:00 2001 From: kent-3 <100624004+kent-3@users.noreply.github.com> Date: Sun, 9 Jul 2023 02:03:17 -0400 Subject: [PATCH 3/3] Add pretty JSON serializer --- Cargo.lock | 45 +-- Cargo.toml | 5 +- src/lib.rs | 1 + src/permit.rs | 67 ++-- src/pretty/map.rs | 265 ++++++++++++++ src/pretty/mod.rs | 813 ++++++++++++++++++++++++++++++++++++++++++ src/pretty/seq.rs | 83 +++++ src/pretty/struct_.rs | 92 +++++ 8 files changed, 1304 insertions(+), 67 deletions(-) create mode 100644 src/pretty/map.rs create mode 100644 src/pretty/mod.rs create mode 100644 src/pretty/seq.rs create mode 100644 src/pretty/struct_.rs diff --git a/Cargo.lock b/Cargo.lock index d4cb3b1..5ecb83d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,43 +91,18 @@ dependencies = [ "version_check", ] -[[package]] -name = "getopts" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" -dependencies = [ - "unicode-width", -] - [[package]] name = "itoa" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" -[[package]] -name = "jsonxf" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d6889ea54a6add10ed8a757719ec88293201265fa7fe56e09ae66b6df038a6" -dependencies = [ - "getopts", - "memchr", -] - [[package]] name = "libc" version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - [[package]] name = "opaque-debug" version = "0.3.0" @@ -149,7 +124,6 @@ version = "0.1.0" dependencies = [ "bech32", "cosmwasm-schema", - "jsonxf", "remain", "ripemd160", "rust-crypto", @@ -158,7 +132,7 @@ dependencies = [ "secret-cosmwasm-std", "secret-cosmwasm-storage", "serde", - "serde-json-wasm 0.5.1", + "serde_derive", "sha2", "snafu", ] @@ -316,7 +290,7 @@ dependencies = [ "base64", "schemars", "serde", - "serde-json-wasm 0.2.3", + "serde-json-wasm", "snafu", ] @@ -348,15 +322,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde-json-wasm" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a62a1fad1e1828b24acac8f2b468971dade7b8c3c2e672bcadefefb1f8c137" -dependencies = [ - "serde", -] - [[package]] name = "serde_derive" version = "1.0.164" @@ -469,12 +434,6 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 728d4f4..d3b7163 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,6 @@ cosmwasm-std = { version = "0.10.1", package = "secret-cosmwasm-std" } cosmwasm-storage = { version = "0.10", package = "secret-cosmwasm-storage" } cosmwasm-schema = "0.10.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } -serde-json-wasm = "0.5.1" -jsonxf = "1.1" snafu = { version = "0.6.3" } schemars = "0.7" remain = "0.2.2" @@ -19,3 +17,6 @@ secp256k1 = "0.20.3" bech32 = "0.8.1" sha2 = { version = "0.9.1", default-features = false } rust-crypto = "0.2.36" + +[dev-dependencies] +serde_derive = "^1.0.80" diff --git a/src/lib.rs b/src/lib.rs index 25b85a5..c9ad912 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod permit; +mod pretty; pub mod transaction; pub mod viewing_keys; diff --git a/src/permit.rs b/src/permit.rs index 53d9b28..f025030 100644 --- a/src/permit.rs +++ b/src/permit.rs @@ -1,7 +1,7 @@ use crate::sha_256; use crate::transaction::{PermitSignature, PubKeyValue, SignedTx}; use bech32::FromBase32; -use cosmwasm_std::{to_binary, to_vec, Api, Binary, CanonicalAddr, StdError, StdResult, Uint128}; +use cosmwasm_std::{to_binary, Api, Binary, CanonicalAddr, StdError, StdResult, Uint128}; use crypto::digest::Digest; use crypto::sha3::Sha3; use schemars::JsonSchema; @@ -91,18 +91,10 @@ fn to_binary_pretty(data: &T) -> StdResult where T: Serialize + ?Sized, { - // Serialize the data to a compact JSON string - let ugly_json = serde_json_wasm::to_string(data).unwrap(); - - // Customize Formatter to use 4 spaces for indents - let mut formatter = jsonxf::Formatter::pretty_printer(); - formatter.indent = String::from(" "); - - // Transform the JSON string to be pretty - let pretty_json = formatter.format(&ugly_json).unwrap(); - - // Serialize the JSON string to a JSON byte vector - to_vec(&pretty_json).map(Binary) + const INDENT: &[u8; 4] = b" "; + super::pretty::to_vec_pretty(data, INDENT) + .map_err(|e| StdError::serialize_err(std::any::type_name::(), e)) + .map(Binary) } #[cfg(test)] @@ -177,19 +169,50 @@ mod signature_tests { permit.params.some_number = Uint128(100); // NOTE: SN mock deps dont have a valid working implementation of the dep functons for some reason //assert!(permit.validate(&deps.api, None).is_err()); + } - // Serialize the data to a compact JSON string - let ugly_json = serde_json_wasm::to_string(&permit).unwrap(); - - // Customize Formatter to use 4 spaces for indents - let mut formatter = jsonxf::Formatter::pretty_printer(); - formatter.indent = String::from(" "); + #[test] + fn test_pretty_print() { + let permit = TestPermit { + params: TestPermitMsg { + address: ADDRESS.to_string(), + some_number: Uint128(10), + }, + chain_id: Some("pulsar-1".to_string()), + sequence: None, + signature: PermitSignature { + pub_key: PubKey::new(Binary::from_base64(PUBKEY).unwrap()), + signature: Binary::from_base64(SIGNED_TX).unwrap(), + }, + account_number: None, + memo: None, + }; - // Transform the JSON string to be pretty - let pretty_json = formatter.format(&ugly_json).unwrap(); + const INDENT: &[u8; 4] = b" "; + let pretty_json = crate::pretty::to_string_pretty(&permit, INDENT).unwrap(); - // Print the JSON string println!("{}", pretty_json); + + assert_eq!( + pretty_json, + r#"{ + "params":{ + "address":"secret102nasmxnxvwp5agc4lp3flc6s23335xm8g7gn9", + "some_number":"10" + }, + "signature":{ + "pub_key":{ + "type":"tendermint/PubKeySecp256k1", + "value":"A0qzJ3s16OKUfn1KFyh533vBnBOQIT0jm+R/FBobJCfa" + }, + "signature":"4pZtghyHKHHmwiGNC5JD8JxCJiO+44j6GqaLPc19Q7lt85tr0IRZHYcnc0pkokIds8otxU9rcuvPXb0+etLyVA==" + }, + "account_number":null, + "chain_id":"pulsar-1", + "sequence":null, + "memo":null +}"# + ) } const FILLERPERMITNAME: &str = "wasm/MsgExecuteContract"; diff --git a/src/pretty/map.rs b/src/pretty/map.rs new file mode 100644 index 0000000..47271b8 --- /dev/null +++ b/src/pretty/map.rs @@ -0,0 +1,265 @@ +use std::fmt; + +use serde::{ser, Serialize}; + +use super::{seq::SerializeSeq, struct_::SerializeStruct}; +use super::{Error, Result, Serializer, Unreachable}; + +pub struct SerializeMap<'serializer, 'indent> { + ser: &'serializer mut Serializer<'indent>, + first: bool, +} + +impl<'serializer, 'indent: 'serializer> SerializeMap<'serializer, 'indent> { + pub(crate) fn new(ser: &'serializer mut Serializer<'indent>) -> Self { + SerializeMap { ser, first: true } + } +} + +impl<'serializer, 'indent: 'serializer> ser::SerializeMap for SerializeMap<'serializer, 'indent> { + type Ok = (); + type Error = Error; + + fn end(self) -> Result { + self.ser.current_indent -= 1; + if !self.first { + self.ser.buf.push(b'\n'); + self.ser.indent()?; + } + self.ser.buf.push(b'}'); + Ok(()) + } + + fn serialize_key(&mut self, key: &T) -> Result<()> + where + T: ser::Serialize, + { + if !self.first { + self.ser.buf.push(b','); + } + self.first = false; + self.ser.buf.push(b'\n'); + self.ser.indent()?; + + // Use key serializer to unsure key type validity. + key.serialize(MapKeySerializer { ser: self.ser })?; + self.ser.buf.extend_from_slice(b":"); + Ok(()) + } + + fn serialize_value(&mut self, value: &T) -> Result<()> + where + T: ser::Serialize, + { + value.serialize(&mut *self.ser)?; + Ok(()) + } +} + +/// Wrapper around Serializer that only allows serialization of valid JSON key types (strings). +struct MapKeySerializer<'serializer, 'indent> { + ser: &'serializer mut Serializer<'indent>, +} + +pub(crate) fn key_must_be_a_string() -> Error { + Error::Custom("JSON object key is required to be a string type.".to_string()) +} + +macro_rules! serialize_unsigned_key { + ($self:ident, $N:expr, $v:expr) => {{ + let ser = $self.ser; + ser.buf.push(b'"'); + let res: Result = super::serialize_unsigned!(ser, $N, $v); + res?; + ser.buf.push(b'"'); + Ok(()) + }}; +} + +macro_rules! serialize_signed_key { + ($self:ident, $N:expr, $v:expr, $ixx:ident, $uxx:ident) => {{ + let ser = $self.ser; + ser.buf.push(b'"'); + let res: Result = super::serialize_signed!(ser, $N, $v, $ixx, $uxx); + res?; + ser.buf.push(b'"'); + Ok(()) + }}; +} + +impl<'serializer, 'indent: 'serializer> ser::Serializer for MapKeySerializer<'serializer, 'indent> { + type Ok = (); + type Error = Error; + type SerializeSeq = SerializeSeq<'serializer, 'indent>; + type SerializeTuple = SerializeSeq<'serializer, 'indent>; + type SerializeTupleStruct = Unreachable; + type SerializeTupleVariant = Unreachable; + type SerializeMap = SerializeMap<'serializer, 'indent>; + type SerializeStruct = SerializeStruct<'serializer, 'indent>; + type SerializeStructVariant = SerializeStruct<'serializer, 'indent>; + + fn serialize_bool(self, _value: bool) -> Result<()> { + Err(key_must_be_a_string()) + } + #[inline] + fn serialize_str(self, value: &str) -> Result<()> { + self.ser.serialize_str(value) + } + + #[inline] + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result<()> { + self.ser.serialize_str(variant) + } + + #[inline] + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + value.serialize(self) + } + + fn serialize_i8(self, value: i8) -> Result<()> { + serialize_signed_key!(self, 4, value, i8, u8) + } + + fn serialize_i16(self, value: i16) -> Result<()> { + serialize_signed_key!(self, 6, value, i16, u16) + } + + fn serialize_i32(self, value: i32) -> Result<()> { + serialize_signed_key!(self, 11, value, i32, u32) + } + + fn serialize_i64(self, value: i64) -> Result<()> { + serialize_signed_key!(self, 20, value, i64, u64) + } + + fn serialize_i128(self, value: i128) -> Result<()> { + serialize_signed_key!(self, 40, value, i128, u128) + } + + fn serialize_u8(self, value: u8) -> Result<()> { + serialize_unsigned_key!(self, 3, value) + } + + fn serialize_u16(self, value: u16) -> Result<()> { + serialize_unsigned_key!(self, 5, value) + } + + fn serialize_u32(self, value: u32) -> Result<()> { + serialize_unsigned_key!(self, 10, value) + } + + fn serialize_u64(self, value: u64) -> Result<()> { + serialize_unsigned_key!(self, 20, value) + } + + fn serialize_u128(self, value: u128) -> Result<()> { + serialize_unsigned_key!(self, 39, value) + } + + fn serialize_f32(self, _value: f32) -> Result<()> { + Err(key_must_be_a_string()) + } + + fn serialize_f64(self, _value: f64) -> Result<()> { + Err(key_must_be_a_string()) + } + + fn serialize_char(self, value: char) -> Result<()> { + self.ser.serialize_str(&value.to_string()) + } + + fn serialize_bytes(self, _value: &[u8]) -> Result<()> { + Err(key_must_be_a_string()) + } + + fn serialize_unit(self) -> Result<()> { + Err(key_must_be_a_string()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result<()> { + Err(key_must_be_a_string()) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result<()> + where + T: ?Sized + Serialize, + { + Err(key_must_be_a_string()) + } + + fn serialize_none(self) -> Result<()> { + Err(key_must_be_a_string()) + } + + fn serialize_some(self, _value: &T) -> Result<()> + where + T: ?Sized + Serialize, + { + Err(key_must_be_a_string()) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { + Err(key_must_be_a_string()) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(key_must_be_a_string()) + } + + fn collect_str(self, _value: &T) -> Result<()> + where + T: ?Sized + fmt::Display, + { + unreachable!() + } +} diff --git a/src/pretty/mod.rs b/src/pretty/mod.rs new file mode 100644 index 0000000..663ab6e --- /dev/null +++ b/src/pretty/mod.rs @@ -0,0 +1,813 @@ +//! Serialize a Rust data structure into pretty-printed JSON data + +use std::{error, fmt}; + +use serde::ser; + +use std::vec::Vec; + +use serde::ser::SerializeStruct as _; + +use self::map::SerializeMap; +use self::seq::SerializeSeq; +use self::struct_::SerializeStruct; + +mod map; +mod seq; +mod struct_; + +/// Serialization result +pub type Result = ::core::result::Result; + +/// This type represents all possible errors that can occur when serializing JSON data +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// Buffer is full + BufferFull, + + /// Custom error message from serde + Custom(String), +} + +impl From<()> for Error { + fn from(_: ()) -> Error { + Error::BufferFull + } +} + +impl From for Error { + fn from(_: u8) -> Error { + Error::BufferFull + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } + + fn description(&self) -> &str { + "(use display)" + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::BufferFull => write!(f, "Buffer is full"), + Error::Custom(msg) => write!(f, "{}", &msg), + } + } +} + +/// Number of bytes reserved by default for the output JSON +static INITIAL_CAPACITY: usize = 1024; + +/// Serializer implements serde::ser::Serializer and allows us to serialize a +/// serde struct into JSON +pub struct Serializer<'indent> { + buf: Vec, + current_indent: usize, + indent: &'indent [u8], +} + +impl<'indent> Serializer<'indent> { + fn new(indent: &'indent [u8]) -> Self { + Serializer { + buf: Vec::with_capacity(INITIAL_CAPACITY), + current_indent: 0, + indent, + } + } + + /// Indent the content + pub fn indent(&mut self) -> Result<()> { + for _ in 0..self.current_indent { + self.buf.extend_from_slice(self.indent); + } + Ok(()) + } +} + +// NOTE(serialize_*signed) This is basically the numtoa implementation minus the lookup tables, +// which take 200+ bytes of ROM / Flash +macro_rules! serialize_unsigned { + ($self:ident, $N:expr, $v:expr) => {{ + let mut buf = [0u8; $N]; + + let mut v = $v; + let mut i = $N - 1; + loop { + buf[i] = (v % 10) as u8 + b'0'; + v /= 10; + + if v == 0 { + break; + } else { + i -= 1; + } + } + + $self.buf.extend_from_slice(&buf[i..]); + Ok(()) + }}; +} +// Export for use in map +pub(crate) use serialize_unsigned; + +macro_rules! serialize_signed { + ($self:ident, $N:expr, $v:expr, $ixx:ident, $uxx:ident) => {{ + let v = $v; + let (signed, mut v) = if v == $ixx::min_value() { + (true, $ixx::max_value() as $uxx + 1) + } else if v < 0 { + (true, -v as $uxx) + } else { + (false, v as $uxx) + }; + + let mut buf = [0u8; $N]; + let mut i = $N - 1; + loop { + buf[i] = (v % 10) as u8 + b'0'; + v /= 10; + + i -= 1; + + if v == 0 { + break; + } + } + + if signed { + buf[i] = b'-'; + } else { + i += 1; + } + $self.buf.extend_from_slice(&buf[i..]); + Ok(()) + }}; +} +// Export for use in map +pub(crate) use serialize_signed; + +/// Upper-case hex for value in 0..16, encoded as ASCII bytes +fn hex_4bit(c: u8) -> u8 { + if c <= 9 { + 0x30 + c + } else { + 0x41 + (c - 10) + } +} + +/// Upper-case hex for value in 0..256, encoded as ASCII bytes +fn hex(c: u8) -> (u8, u8) { + (hex_4bit(c >> 4), hex_4bit(c & 0x0F)) +} + +impl<'serializer, 'indent: 'serializer> ser::Serializer for &'serializer mut Serializer<'indent> { + type Ok = (); + type Error = Error; + type SerializeSeq = SerializeSeq<'serializer, 'indent>; + type SerializeTuple = SerializeSeq<'serializer, 'indent>; + type SerializeTupleStruct = Unreachable; + type SerializeTupleVariant = SerializeSeq<'serializer, 'indent>; + type SerializeMap = SerializeMap<'serializer, 'indent>; + type SerializeStruct = SerializeStruct<'serializer, 'indent>; + type SerializeStructVariant = SerializeStruct<'serializer, 'indent>; + + fn serialize_bool(self, v: bool) -> Result { + if v { + self.buf.extend_from_slice(b"true"); + } else { + self.buf.extend_from_slice(b"false"); + } + Ok(()) + } + + fn serialize_i8(self, v: i8) -> Result { + // "-128" + serialize_signed!(self, 4, v, i8, u8) + } + + fn serialize_i16(self, v: i16) -> Result { + // "-32768" + serialize_signed!(self, 6, v, i16, u16) + } + + fn serialize_i32(self, v: i32) -> Result { + // "-2147483648" + serialize_signed!(self, 11, v, i32, u32) + } + + fn serialize_i64(self, v: i64) -> Result { + // "-9223372036854775808" + serialize_signed!(self, 20, v, i64, u64) + } + + fn serialize_i128(self, v: i128) -> Result { + // -170141183460469231731687303715884105728 + self.buf.push(b'"'); + let res: Result = serialize_signed!(self, 40, v, i128, u128); + res?; + self.buf.push(b'"'); + Ok(()) + } + + fn serialize_u8(self, v: u8) -> Result { + // "255" + serialize_unsigned!(self, 3, v) + } + + fn serialize_u16(self, v: u16) -> Result { + // "65535" + serialize_unsigned!(self, 5, v) + } + + fn serialize_u32(self, v: u32) -> Result { + // "4294967295" + serialize_unsigned!(self, 10, v) + } + + fn serialize_u64(self, v: u64) -> Result { + // "18446744073709551615" + serialize_unsigned!(self, 20, v) + } + + fn serialize_u128(self, v: u128) -> Result { + // 340282366920938463463374607431768211455 + self.buf.push(b'"'); + let res: Result = serialize_unsigned!(self, 39, v); + res?; + self.buf.push(b'"'); + Ok(()) + } + + fn serialize_f32(self, _v: f32) -> Result { + unreachable!() + } + + fn serialize_f64(self, _v: f64) -> Result { + unreachable!() + } + + fn serialize_char(self, _v: char) -> Result { + unreachable!() + } + + fn serialize_str(self, v: &str) -> Result { + self.buf.push(b'"'); + + // Do escaping according to "6. MUST represent all strings (including object member names) in + // their minimal-length UTF-8 encoding": https://gibson042.github.io/canonicaljson-spec/ + // + // We don't need to escape lone surrogates because surrogate pairs do not exist in valid UTF-8, + // even if they can exist in JSON or JavaScript strings (UCS-2 based). As a result, lone surrogates + // cannot exist in a Rust String. If they do, the bug is in the String constructor. + // An excellent explanation is available at https://www.youtube.com/watch?v=HhIEDWmQS3w + + // Temporary storage for encoded a single char. + // A char is up to 4 bytes long when encoded to UTF-8. + let mut encoding_tmp = [0u8; 4]; + + for c in v.chars() { + match c { + '\\' => { + self.buf.push(b'\\'); + self.buf.push(b'\\'); + } + '"' => { + self.buf.push(b'\\'); + self.buf.push(b'"'); + } + '\u{0008}' => { + self.buf.push(b'\\'); + self.buf.push(b'b'); + } + '\u{0009}' => { + self.buf.push(b'\\'); + self.buf.push(b't'); + } + '\u{000A}' => { + self.buf.push(b'\\'); + self.buf.push(b'n'); + } + '\u{000C}' => { + self.buf.push(b'\\'); + self.buf.push(b'f'); + } + '\u{000D}' => { + self.buf.push(b'\\'); + self.buf.push(b'r'); + } + '\u{0000}'..='\u{001F}' => { + self.buf.push(b'\\'); + self.buf.push(b'u'); + self.buf.push(b'0'); + self.buf.push(b'0'); + let (hex1, hex2) = hex(c as u8); + self.buf.push(hex1); + self.buf.push(hex2); + } + _ => { + if c.len_utf8() == 1 { + self.buf.push(c as u8); + } else { + let encoded = c.encode_utf8(&mut encoding_tmp as &mut [u8]); + self.buf.extend_from_slice(encoded.as_bytes()); + } + } + } + } + + self.buf.push(b'"'); + Ok(()) + } + + fn serialize_bytes(self, _v: &[u8]) -> Result { + unreachable!() + } + + fn serialize_none(self) -> Result { + self.buf.extend_from_slice(b"null"); + Ok(()) + } + + fn serialize_some(self, value: &T) -> Result + where + T: ser::Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + // The unit type is a zero element tuple, so the consistent way to serialize this would be "[]". + // However, for compatibility with serde_json we serialize to "null". + self.buf.extend_from_slice(b"null"); + Ok(()) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + // Unit struct is serialized to (serde_json compatible) "null" + self.buf.extend_from_slice(b"null"); + Ok(()) + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + self.serialize_str(variant) + } + + fn serialize_newtype_struct(self, _name: &'static str, value: &T) -> Result + where + T: ser::Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + where + T: ser::Serialize, + { + self.current_indent += 1; + self.buf.push(b'{'); + let mut s = SerializeStruct::new(self); + s.serialize_field(variant, value)?; + s.end()?; + Ok(()) + } + + fn serialize_seq(self, _len: Option) -> Result { + self.current_indent += 1; + self.buf.push(b'['); + + Ok(SerializeSeq::new(self)) + } + + fn serialize_tuple(self, _len: usize) -> Result { + self.serialize_seq(Some(_len)) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + unreachable!() + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + self.current_indent += 1; + self.buf.push(b'{'); + self.serialize_str(variant)?; + self.buf.push(b':'); + self.serialize_tuple(len) + } + + fn serialize_map(self, _len: Option) -> Result { + self.current_indent += 1; + self.buf.push(b'{'); + + Ok(SerializeMap::new(self)) + } + + fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { + self.current_indent += 1; + self.buf.push(b'{'); + + Ok(SerializeStruct::new(self)) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + _len: usize, + ) -> Result { + self.current_indent += 1; + self.buf.extend_from_slice(b"{\n"); + self.indent()?; + self.buf.push(b'"'); + self.buf.extend_from_slice(variant.as_bytes()); + self.buf.extend_from_slice(b"\":{"); + self.current_indent += 1; + + Ok(SerializeStruct::new(self)) + } + + fn collect_str(self, _value: &T) -> Result + where + T: fmt::Display, + { + unreachable!() + } +} + +/// Serializes the given data structure as a pretty-printed string of JSON text +// #[cfg(feature = "heapless")] +pub fn to_string_pretty(value: &T, indent: &[u8]) -> Result +where + T: ser::Serialize + ?Sized, +{ + let mut ser = Serializer::new(indent); + value.serialize(&mut ser)?; + Ok(unsafe { String::from_utf8_unchecked(ser.buf) }) +} + +/// Serializes the given data structure as a pretty-printed JSON byte vector +pub fn to_vec_pretty(value: &T, indent: &[u8]) -> Result> +where + T: ser::Serialize + ?Sized, +{ + let mut ser = Serializer::new(indent); + value.serialize(&mut ser)?; + Ok(ser.buf) +} + +impl ser::Error for Error { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + Error::Custom(msg.to_string()) + } +} + +/// Unreachable is a placeholder for features that are not supported +/// (and should be unreachable, unless you use unsupported serde flags) +pub enum Unreachable {} + +impl ser::SerializeTupleStruct for Unreachable { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<()> { + unreachable!() + } + + fn end(self) -> Result { + unreachable!() + } +} + +impl ser::SerializeTupleVariant for Unreachable { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _value: &T) -> Result<()> { + unreachable!() + } + + fn end(self) -> Result { + unreachable!() + } +} + +impl ser::SerializeMap for Unreachable { + type Ok = (); + type Error = Error; + + fn serialize_key(&mut self, _key: &T) -> Result<()> + where + T: ser::Serialize, + { + unreachable!() + } + + fn serialize_value(&mut self, _value: &T) -> Result<()> + where + T: ser::Serialize, + { + unreachable!() + } + + fn end(self) -> Result { + unreachable!() + } +} + +impl ser::SerializeStructVariant for Unreachable { + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, _key: &'static str, _value: &T) -> Result<()> + where + T: ser::Serialize, + { + unreachable!() + } + + fn end(self) -> Result { + unreachable!() + } +} + +#[cfg(test)] +mod tests { + + use super::to_string_pretty; + use serde_derive::Serialize; + + const INDENT: &[u8] = b" "; + + #[test] + fn enum_() { + #[derive(Serialize)] + enum Type { + #[serde(rename = "boolean")] + Boolean, + #[serde(rename = "number")] + Number, + } + + assert_eq!( + to_string_pretty(&Type::Boolean, INDENT).unwrap(), + r#""boolean""# + ); + + assert_eq!( + to_string_pretty(&Type::Number, INDENT).unwrap(), + r#""number""# + ); + } + + #[test] + fn str() { + assert_eq!(to_string_pretty("hello", INDENT).unwrap(), r#""hello""#); + assert_eq!(to_string_pretty("", INDENT).unwrap(), r#""""#); + + // Characters unescaped if possible + assert_eq!(to_string_pretty("ä", INDENT).unwrap(), r#""ä""#); + assert_eq!(to_string_pretty("৬", INDENT).unwrap(), r#""৬""#); + // assert_eq!(to_string_pretty("\u{A0}").unwrap(), r#"" ""#); // non-breaking space + assert_eq!(to_string_pretty("ℝ", INDENT).unwrap(), r#""ℝ""#); // 3 byte character + assert_eq!(to_string_pretty("💣", INDENT).unwrap(), r#""💣""#); // 4 byte character + + // " and \ must be escaped + assert_eq!( + to_string_pretty("foo\"bar", INDENT).unwrap(), + r#""foo\"bar""# + ); + assert_eq!( + to_string_pretty("foo\\bar", INDENT).unwrap(), + r#""foo\\bar""# + ); + + // \b, \t, \n, \f, \r must be escaped in their two-character escaping + assert_eq!(to_string_pretty(" \u{0008} ", INDENT).unwrap(), r#"" \b ""#); + assert_eq!(to_string_pretty(" \u{0009} ", INDENT).unwrap(), r#"" \t ""#); + assert_eq!(to_string_pretty(" \u{000A} ", INDENT).unwrap(), r#"" \n ""#); + assert_eq!(to_string_pretty(" \u{000C} ", INDENT).unwrap(), r#"" \f ""#); + assert_eq!(to_string_pretty(" \u{000D} ", INDENT).unwrap(), r#"" \r ""#); + + // U+0000 through U+001F is escaped using six-character \u00xx uppercase hexadecimal escape sequences + assert_eq!( + to_string_pretty(" \u{0000} ", INDENT).unwrap(), + r#"" \u0000 ""# + ); + assert_eq!( + to_string_pretty(" \u{0001} ", INDENT).unwrap(), + r#"" \u0001 ""# + ); + assert_eq!( + to_string_pretty(" \u{0007} ", INDENT).unwrap(), + r#"" \u0007 ""# + ); + assert_eq!( + to_string_pretty(" \u{000e} ", INDENT).unwrap(), + r#"" \u000E ""# + ); + assert_eq!( + to_string_pretty(" \u{001D} ", INDENT).unwrap(), + r#"" \u001D ""# + ); + assert_eq!( + to_string_pretty(" \u{001f} ", INDENT).unwrap(), + r#"" \u001F ""# + ); + } + + #[test] + fn struct_bool() { + #[derive(Serialize)] + struct Led { + led: bool, + } + + assert_eq!( + to_string_pretty(&Led { led: true }, INDENT).unwrap(), + r#"{ + "led":true +}"# + ); + } + + #[test] + fn struct_i8() { + #[derive(Serialize)] + struct Temperature { + temperature: i8, + } + + assert_eq!( + to_string_pretty(&Temperature { temperature: 127 }, INDENT).unwrap(), + r#"{ + "temperature":127 +}"# + ); + + assert_eq!( + to_string_pretty(&Temperature { temperature: 20 }, INDENT).unwrap(), + r#"{ + "temperature":20 +}"# + ); + + assert_eq!( + to_string_pretty(&Temperature { temperature: -17 }, INDENT).unwrap(), + r#"{ + "temperature":-17 +}"# + ); + + assert_eq!( + to_string_pretty(&Temperature { temperature: -128 }, INDENT).unwrap(), + r#"{ + "temperature":-128 +}"# + ); + } + + #[test] + fn struct_option() { + #[derive(Serialize)] + struct Property<'a> { + description: Option<&'a str>, + } + + assert_eq!( + to_string_pretty( + &Property { + description: Some("An ambient temperature sensor"), + }, + INDENT + ) + .unwrap(), + r#"{ + "description":"An ambient temperature sensor" +}"# + ); + + // XXX Ideally this should produce "{}" + assert_eq!( + to_string_pretty(&Property { description: None }, INDENT).unwrap(), + r#"{ + "description":null +}"# + ); + } + + #[test] + fn struct_u8() { + #[derive(Serialize)] + struct Temperature { + temperature: u8, + } + + assert_eq!( + to_string_pretty(&Temperature { temperature: 20 }, INDENT).unwrap(), + r#"{ + "temperature":20 +}"# + ); + } + + #[test] + fn struct_() { + #[derive(Serialize)] + struct Empty {} + + assert_eq!(to_string_pretty(&Empty {}, INDENT).unwrap(), r#"{}"#); + + #[derive(Serialize)] + struct Tuple { + a: bool, + b: bool, + } + + assert_eq!( + to_string_pretty(&Tuple { a: true, b: false }, INDENT).unwrap(), + r#"{ + "a":true, + "b":false +}"# + ); + } + + #[test] + fn test_unit() { + let a = (); + assert_eq!(to_string_pretty(&a, INDENT).unwrap(), r#"null"#); + } + + #[test] + fn test_newtype_struct() { + #[derive(Serialize)] + struct A(pub u32); + let a = A(54); + assert_eq!(to_string_pretty(&a, INDENT).unwrap(), r#"54"#); + } + + #[test] + fn test_newtype_variant() { + #[derive(Serialize)] + enum A { + A(u32), + } + let a = A::A(54); + + assert_eq!( + to_string_pretty(&a, INDENT).unwrap(), + r#"{ + "A":54 +}"# + ); + } + + #[test] + fn test_struct_variant() { + #[derive(Serialize)] + enum A { + A { x: u32, y: u16 }, + } + let a = A::A { x: 54, y: 720 }; + + assert_eq!( + to_string_pretty(&a, INDENT).unwrap(), + r#"{ + "A":{ + "x":54, + "y":720 + } +}"# + ); + } +} diff --git a/src/pretty/seq.rs b/src/pretty/seq.rs new file mode 100644 index 0000000..ae77d38 --- /dev/null +++ b/src/pretty/seq.rs @@ -0,0 +1,83 @@ +use serde::ser; + +use super::{Error, Result, Serializer}; + +pub struct SerializeSeq<'serializer, 'indent> { + ser: &'serializer mut Serializer<'indent>, + first: bool, +} + +impl<'serializer, 'indent: 'serializer> SerializeSeq<'serializer, 'indent> { + pub(crate) fn new(ser: &'serializer mut Serializer<'indent>) -> Self { + SerializeSeq { ser, first: true } + } +} + +impl<'serializer, 'indent: 'serializer> ser::SerializeSeq for SerializeSeq<'serializer, 'indent> { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ser::Serialize, + { + if !self.first { + self.ser.buf.push(b','); + } + self.first = false; + + self.ser.buf.push(b'\n'); + self.ser.indent()?; + + value.serialize(&mut *self.ser)?; + Ok(()) + } + + fn end(self) -> Result { + self.ser.current_indent -= 1; + if !self.first { + self.ser.buf.push(b'\n'); + self.ser.indent()?; + } + self.ser.buf.push(b']'); + Ok(()) + } +} + +impl<'serializer, 'indent: 'serializer> ser::SerializeTuple for SerializeSeq<'serializer, 'indent> { + type Ok = (); + type Error = Error; + + fn serialize_element(&mut self, value: &T) -> Result<()> + where + T: ser::Serialize, + { + ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + ser::SerializeSeq::end(self) + } +} + +impl<'serializer, 'indent: 'serializer> ser::SerializeTupleVariant + for SerializeSeq<'serializer, 'indent> +{ + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, value: &T) -> Result<()> + where + T: ser::Serialize, + { + ser::SerializeSeq::serialize_element(self, value) + } + + fn end(self) -> Result { + // close sequence + self.ser.buf.push(b']'); + // close surrounding enum + self.ser.buf.push(b'}'); + Ok(()) + } +} diff --git a/src/pretty/struct_.rs b/src/pretty/struct_.rs new file mode 100644 index 0000000..8e34e89 --- /dev/null +++ b/src/pretty/struct_.rs @@ -0,0 +1,92 @@ +use serde::ser; + +use super::{Error, Result, Serializer}; + +pub struct SerializeStruct<'serializer, 'indent> { + ser: &'serializer mut Serializer<'indent>, + first: bool, +} + +impl<'serializer, 'indent: 'serializer> SerializeStruct<'serializer, 'indent> { + pub(crate) fn new(ser: &'serializer mut Serializer<'indent>) -> Self { + SerializeStruct { ser, first: true } + } +} + +impl<'serializer, 'indent: 'serializer> ser::SerializeStruct + for SerializeStruct<'serializer, 'indent> +{ + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ser::Serialize, + { + // XXX if `value` is `None` we not produce any output for this field + if !self.first { + self.ser.buf.push(b','); + } + self.first = false; + + self.ser.buf.push(b'\n'); + self.ser.indent()?; + self.ser.buf.push(b'"'); + self.ser.buf.extend_from_slice(key.as_bytes()); + self.ser.buf.extend_from_slice(b"\":"); + + value.serialize(&mut *self.ser)?; + + Ok(()) + } + + fn end(self) -> Result { + self.ser.current_indent -= 1; + if !self.first { + self.ser.buf.push(b'\n'); + self.ser.indent()?; + } + self.ser.buf.push(b'}'); + Ok(()) + } +} + +impl<'serializer, 'indent: 'serializer> ser::SerializeStructVariant + for SerializeStruct<'serializer, 'indent> +{ + type Ok = (); + type Error = Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<()> + where + T: ser::Serialize, + { + // XXX if `value` is `None` we not produce any output for this field + if !self.first { + self.ser.buf.push(b','); + } + self.first = false; + self.ser.buf.push(b'\n'); + self.ser.indent()?; + + self.ser.buf.push(b'"'); + self.ser.buf.extend_from_slice(key.as_bytes()); + self.ser.buf.extend_from_slice(b"\":"); + + value.serialize(&mut *self.ser)?; + + Ok(()) + } + + fn end(self) -> Result { + for _ in 0..2 { + self.ser.current_indent -= 1; + if !self.first { + self.ser.buf.push(b'\n'); + self.ser.indent()?; + } + self.ser.buf.push(b'}'); + } + Ok(()) + } +}