From 314a7440752d994944f4bbc4fa7551af6ee9c980 Mon Sep 17 00:00:00 2001 From: Eric Nordelo Date: Fri, 7 Aug 2026 14:31:20 +0200 Subject: [PATCH] fix: L-09 --- .../src/attribute/type_hash/definition.rs | 6 ++- .../macros/src/attribute/type_hash/parser.rs | 38 +++++++++++++++++-- ..._hash__json_escaped_enum_variant_name.snap | 26 +++++++++++++ ..._type_hash__json_escaped_struct_names.snap | 26 +++++++++++++ packages/macros/src/tests/test_type_hash.rs | 26 +++++++++++++ 5 files changed, 117 insertions(+), 5 deletions(-) create mode 100644 packages/macros/src/tests/snapshots/openzeppelin_macros__tests__test_type_hash__json_escaped_enum_variant_name.snap create mode 100644 packages/macros/src/tests/snapshots/openzeppelin_macros__tests__test_type_hash__json_escaped_struct_names.snap diff --git a/packages/macros/src/attribute/type_hash/definition.rs b/packages/macros/src/attribute/type_hash/definition.rs index 9f38cfb17..bad91f4f8 100644 --- a/packages/macros/src/attribute/type_hash/definition.rs +++ b/packages/macros/src/attribute/type_hash/definition.rs @@ -216,7 +216,11 @@ fn generate_code( pub fn __{type_name}_encoded_type() {{ println!("{}"); }}"#, - type_hash_string.replace("\"", "\\\"") + type_hash_string + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('{', "{{") + .replace('}', "}}") ) } else { String::new() diff --git a/packages/macros/src/attribute/type_hash/parser.rs b/packages/macros/src/attribute/type_hash/parser.rs index 45686c7da..710af0e3e 100644 --- a/packages/macros/src/attribute/type_hash/parser.rs +++ b/packages/macros/src/attribute/type_hash/parser.rs @@ -102,7 +102,7 @@ impl<'db, 'a> TypeHashParser<'db, 'a> { .collect::>>(); // 2. Build the string representation - let mut encoded_type = format!("\"{primary_type_name}\"("); + let mut encoded_type = format!("{}(", encode_json_string(primary_type_name)); let mut member_names = HashSet::new(); for result in members_types { let (name, s12_type) = result?; @@ -110,15 +110,16 @@ impl<'db, 'a> TypeHashParser<'db, 'a> { return Err(Diagnostic::error(errors::DUPLICATE_SNIP12_NAME(&name))); } let type_name = s12_type.get_snip12_type_name()?; + let encoded_name = encode_json_string(&name); // Format the member depending on the type variant match self.plugin_type_info.type_variant { TypeVariant::Struct => { - encoded_type.push_str(&format!("\"{name}\":\"{type_name}\",")) + encoded_type.push_str(&format!("{encoded_name}:\"{type_name}\",")) } TypeVariant::Enum => { let tuple = maybe_tuple(&type_name)?; - encoded_type.push_str(&format!("\"{}\"({}),", name, tuple)) + encoded_type.push_str(&format!("{encoded_name}({tuple}),")) } }; @@ -318,6 +319,27 @@ fn decode_escaped_string(s: &str) -> Option { Some(decoded) } +/// Encodes a string as a JSON string literal, including the surrounding quotes. +fn encode_json_string(value: &str) -> String { + let mut encoded = String::with_capacity(value.len() + 2); + encoded.push('"'); + + for ch in value.chars() { + match ch { + '"' => encoded.push_str("\\\""), + '\\' => encoded.push_str("\\\\"), + '\n' => encoded.push_str("\\n"), + '\r' => encoded.push_str("\\r"), + '\t' => encoded.push_str("\\t"), + '\u{0}'..='\u{1f}' => encoded.push_str(&format!("\\u{:04x}", ch as u32)), + _ => encoded.push(ch), + } + } + + encoded.push('"'); + encoded +} + /// Returns the enum compliant string representation of a tuple for the encoded type. /// /// If the input is not a tuple, it returns the input itself. @@ -343,7 +365,7 @@ fn maybe_tuple(s: &str) -> Result { #[cfg(test)] mod tests { - use super::{parse_snip12_args, parse_string_arg}; + use super::{encode_json_string, parse_snip12_args, parse_string_arg}; #[test] fn rejects_duplicate_snip12_name_argument() { @@ -363,6 +385,14 @@ mod tests { assert_eq!(args.kind, "felt252"); } + #[test] + fn encodes_json_string() { + assert_eq!( + encode_json_string("quote\" slash\\ newline\n nul\0 unit\u{1f}"), + r#""quote\" slash\\ newline\n nul\u0000 unit\u001f""# + ); + } + #[test] fn parses_plain_string_arg() { assert_eq!(parse_string_arg(r#""example""#).unwrap(), "example"); diff --git a/packages/macros/src/tests/snapshots/openzeppelin_macros__tests__test_type_hash__json_escaped_enum_variant_name.snap b/packages/macros/src/tests/snapshots/openzeppelin_macros__tests__test_type_hash__json_escaped_enum_variant_name.snap new file mode 100644 index 000000000..4127ff440 --- /dev/null +++ b/packages/macros/src/tests/snapshots/openzeppelin_macros__tests__test_type_hash__json_escaped_enum_variant_name.snap @@ -0,0 +1,26 @@ +--- +source: src/tests/test_type_hash.rs +assertion_line: 549 +expression: result +--- +TokenStream: + +pub enum MyEnum { + #[snip12(name: "variant\"\\")] + Value: felt252, +} +pub fn __MY_ENUM_encoded_type() { + println!("\"MyEnum\"(\"variant\\\"\\\\\"(\"felt\"))"); +} +pub const MY_ENUM_TYPE_HASH: felt252 = + 0x2ef1a57e9a7173f9bfa51b0ff1aacc069fdb14a65b86ac5d3bbf5e52ee1f227; + + + +Diagnostics: + +None + +AuxData: + +None diff --git a/packages/macros/src/tests/snapshots/openzeppelin_macros__tests__test_type_hash__json_escaped_struct_names.snap b/packages/macros/src/tests/snapshots/openzeppelin_macros__tests__test_type_hash__json_escaped_struct_names.snap new file mode 100644 index 000000000..0844bb755 --- /dev/null +++ b/packages/macros/src/tests/snapshots/openzeppelin_macros__tests__test_type_hash__json_escaped_struct_names.snap @@ -0,0 +1,26 @@ +--- +source: src/tests/test_type_hash.rs +assertion_line: 536 +expression: result +--- +TokenStream: + +pub struct MyType { + #[snip12(name: "member\"\\}\n\r\t\0")] + pub value: felt252, +} +pub fn __MY_TYPE_encoded_type() { + println!("\"type\\\"\\\\{{\\n\\r\\t\\u0000\"(\"member\\\"\\\\}}\\n\\r\\t\\u0000\":\"felt\")"); +} +pub const MY_TYPE_TYPE_HASH: felt252 = + 0x2c33f80e28d377d8e4a8188f359b29a57599d5747e8284e37a1d6daefd76077; + + + +Diagnostics: + +None + +AuxData: + +None diff --git a/packages/macros/src/tests/test_type_hash.rs b/packages/macros/src/tests/test_type_hash.rs index f85fa93e1..e9f043a75 100644 --- a/packages/macros/src/tests/test_type_hash.rs +++ b/packages/macros/src/tests/test_type_hash.rs @@ -641,6 +641,19 @@ fn test_duplicate_snip12_struct_name() { assert_snapshot!(result); } +#[test] +fn test_json_escaped_struct_names() { + let item = quote! { + pub struct MyType { + #[snip12(name: "member\"\\}\n\r\t\0")] + pub value: felt252, + } + }; + let attr_stream = quote! { (name: "type\"\\{\n\r\t\0", debug: true) }; + let result = get_string_result(attr_stream, item); + assert_snapshot!(result); +} + #[test] fn test_duplicate_snip12_enum_name() { let item = quote! { @@ -654,6 +667,19 @@ fn test_duplicate_snip12_enum_name() { assert_snapshot!(result); } +#[test] +fn test_json_escaped_enum_variant_name() { + let item = quote! { + pub enum MyEnum { + #[snip12(name: "variant\"\\")] + Value: felt252, + } + }; + let attr_stream = quote! { (debug: true) }; + let result = get_string_result(attr_stream, item); + assert_snapshot!(result); +} + #[test] fn test_debug_attribute() { let item = quote! {