diff --git a/CHANGELOG.md b/CHANGELOG.md index edb40a240..b33279a01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### API Changes +- Add `hex_as_raw` option to `PrettyConfig` to allow serializing hex strings without quotes ([#554](https://github.com/ron-rs/ron/pull/554)) + ## [0.9.0] - 2023-09-?? ### API Changes diff --git a/src/ser/mod.rs b/src/ser/mod.rs index 1d3b6c111..0c788d29e 100644 --- a/src/ser/mod.rs +++ b/src/ser/mod.rs @@ -111,6 +111,8 @@ pub struct PrettyConfig { pub compact_maps: bool, /// Enable explicit number type suffixes like `1u16` pub number_suffixes: bool, + /// Whether to serialize hex strings (0x...) without quotes + pub hex_as_raw: bool, /// Additional path-based field metadata to serialize pub path_meta: Option, } @@ -341,6 +343,27 @@ impl PrettyConfig { self } + + /// Configures whether hex strings should be serialized without quotes. + /// + /// When `true`, the hex string `0x1234` will serialize to + /// ```ignore + /// 0x1234 + /// # ; + /// ``` + /// When `false`, the hex string `0x1234` will serialize to + /// ```ignore + /// "0x1234" + /// # ; + /// ``` + /// + /// Default: `false` + #[must_use] + pub fn hex_as_raw(mut self, hex_as_raw: bool) -> Self { + self.hex_as_raw = hex_as_raw; + + self + } } impl Default for PrettyConfig { @@ -363,6 +386,7 @@ impl Default for PrettyConfig { compact_structs: false, compact_maps: false, number_suffixes: false, + hex_as_raw: false, path_meta: None, } } @@ -383,6 +407,15 @@ pub struct Serializer { implicit_some_depth: usize, } +/// Returns true if the string is a valid hexadecimal number starting with 0x/0X +#[inline] +fn is_valid_hex(s: &str) -> bool { + if !s.starts_with("0x") && !s.starts_with("0X") { + return false; + } + s[2..].chars().all(|c| c.is_ascii_hexdigit()) +} + fn indent(output: &mut W, config: &PrettyConfig, pretty: &Pretty) -> fmt::Result { if pretty.indent <= config.depth_limit { for _ in 0..pretty.indent { @@ -489,6 +522,12 @@ impl Serializer { .map_or(true, |(ref config, _)| config.escape_strings) } + fn hex_as_raw(&self) -> bool { + self.pretty + .as_ref() + .map_or(false, |(ref config, _)| config.hex_as_raw) + } + fn start_indent(&mut self) -> Result<()> { if let Some((ref config, ref mut pretty)) = self.pretty { pretty.indent += 1; @@ -777,7 +816,11 @@ impl<'a, W: fmt::Write> ser::Serializer for &'a mut Serializer { fn serialize_str(self, v: &str) -> Result<()> { if self.escape_strings() { - self.serialize_escaped_str(v)?; + if self.hex_as_raw() && is_valid_hex(v) { + self.output.write_str(v)?; + } else { + self.serialize_escaped_str(v)?; + } } else { self.serialize_unescaped_or_raw_str(v)?; } diff --git a/tests/numbers.rs b/tests/numbers.rs index bdd357478..bc3276b17 100644 --- a/tests/numbers.rs +++ b/tests/numbers.rs @@ -1,6 +1,7 @@ use ron::{ de::from_str, error::{Error, Position, SpannedError}, + ser::{to_string_pretty, PrettyConfig}, }; #[test] @@ -111,3 +112,35 @@ fn test_dec() { }) ); } + +#[test] +fn test_hex_serialization() { + let config = PrettyConfig::new() + .hex_as_raw(true); + + // Test valid hex without quotes + let val = "0x1234"; + let serialized = to_string_pretty(&val, config.clone()).unwrap(); + assert_eq!(serialized, "0x1234"); + + // Test uppercase hex + let val = "0X1A5B"; + let serialized = to_string_pretty(&val, config.clone()).unwrap(); + assert_eq!(serialized, "0X1A5B"); + + // Test that normal strings are still escaped + let val = "normal string"; + let serialized = to_string_pretty(&val, config.clone()).unwrap(); + assert_eq!(serialized, "\"normal string\""); + + // Test that invalid hex is treated as a normal string + let val = "0xGGG"; + let serialized = to_string_pretty(&val, config.clone()).unwrap(); + assert_eq!(serialized, "\"0xGGG\""); + + // Test with hex_as_raw disabled + let config = PrettyConfig::new().hex_as_raw(false); + let val = "0x1234"; + let serialized = to_string_pretty(&val, config).unwrap(); + assert_eq!(serialized, "\"0x1234\""); +}