From 822502ab03552b4aec84227329f6b74cd2c034c2 Mon Sep 17 00:00:00 2001 From: mgros Date: Mon, 28 Sep 2026 00:11:03 +0200 Subject: [PATCH] Refactor YAML handling by centralizing reserved word checks and byte escaping --- build.rs | 2 ++ src/bin/package_rules.rs | 2 ++ src/lib.rs | 1 + src/pretty_print.rs | 61 +++++++++------------------------------- src/rules_archive.rs | 45 ++--------------------------- src/yaml_helpers.rs | 50 ++++++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 90 deletions(-) create mode 100644 src/yaml_helpers.rs diff --git a/build.rs b/build.rs index e5a07cd8d..73692e178 100644 --- a/build.rs +++ b/build.rs @@ -4,6 +4,8 @@ #![allow(clippy::needless_return)] #![allow(dead_code)] // include! of rules_archive.rs pulls in the downloadable packager too +#[path = "src/yaml_helpers.rs"] +mod yaml_helpers; include!("src/rules_archive.rs"); fn main() { diff --git a/src/bin/package_rules.rs b/src/bin/package_rules.rs index 971e95175..7e3a0af72 100644 --- a/src/bin/package_rules.rs +++ b/src/bin/package_rules.rs @@ -6,6 +6,8 @@ #![allow(clippy::needless_return)] +#[path = "../yaml_helpers.rs"] +mod yaml_helpers; include!("../rules_archive.rs"); use clap::Parser; diff --git a/src/lib.rs b/src/lib.rs index ac577a47f..42669789f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,6 +38,7 @@ mod prefs; mod tts; mod xpath_functions; mod definitions; +mod yaml_helpers; pub mod pretty_print; mod chemistry; diff --git a/src/pretty_print.rs b/src/pretty_print.rs index 6a3ccd979..99c603bbb 100644 --- a/src/pretty_print.rs +++ b/src/pretty_print.rs @@ -178,43 +178,8 @@ fn escape_str(wr: &mut dyn fmt::Write, v: &str) -> Result<(), fmt::Error> { let mut start = 0; for (i, byte) in v.bytes().enumerate() { - let escaped = match byte { - b'"' => "\\\"", - b'\\' => "\\\\", - b'\x00' => "\\u0000", - b'\x01' => "\\u0001", - b'\x02' => "\\u0002", - b'\x03' => "\\u0003", - b'\x04' => "\\u0004", - b'\x05' => "\\u0005", - b'\x06' => "\\u0006", - b'\x07' => "\\u0007", - b'\x08' => "\\b", - b'\t' => "\\t", - b'\n' => "\\n", - b'\x0b' => "\\u000b", - b'\x0c' => "\\f", - b'\r' => "\\r", - b'\x0e' => "\\u000e", - b'\x0f' => "\\u000f", - b'\x10' => "\\u0010", - b'\x11' => "\\u0011", - b'\x12' => "\\u0012", - b'\x13' => "\\u0013", - b'\x14' => "\\u0014", - b'\x15' => "\\u0015", - b'\x16' => "\\u0016", - b'\x17' => "\\u0017", - b'\x18' => "\\u0018", - b'\x19' => "\\u0019", - b'\x1a' => "\\u001a", - b'\x1b' => "\\u001b", - b'\x1c' => "\\u001c", - b'\x1d' => "\\u001d", - b'\x1e' => "\\u001e", - b'\x1f' => "\\u001f", - b'\x7f' => "\\u007f", - _ => continue, + let Some(escaped) = crate::yaml_helpers::escaped_byte(byte) else { + continue; }; if start < i { @@ -453,17 +418,7 @@ fn need_quotes(string: &str) -> bool { | '\r' | '\x0e'..='\x1a' | '\x1c'..='\x1f') ) - || [ - // http://yaml.org/type/bool.html - // Note: 'y', 'Y', 'n', 'N', is not quoted deliberately, as in libyaml. PyYAML also parse - // them as string, not booleans, although it is violating the YAML 1.1 specification. - // See https://github.com/dtolnay/serde-yaml/pull/83#discussion_r152628088. - "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE", - "false", "on", "On", "ON", "off", "Off", "OFF", - // http://yaml.org/type/null.html - "null", "Null", "NULL", "~", - ] - .contains(&string) + || crate::yaml_helpers::RESERVED_WORDS.contains(&string) || string.starts_with('.') || string.starts_with("0x") || string.parse::().is_ok() @@ -476,6 +431,16 @@ mod tests { use sxd_document_no_unsafe::dom::{ChildOfElement, ChildOfRoot}; use sxd_document_no_unsafe::parser; + #[test] + /// Keeps reserved YAML words quoted and escapes control bytes in strings. + fn yaml_scalar_quoting_and_escaping() { + assert_eq!(yaml_to_string(&Yaml::String("True".into()), 0), "\"True\""); + assert_eq!( + yaml_to_string(&Yaml::String("a\0\t\x7f".into()), 0), + "\"a\\u0000\\t\\u007f\"" + ); + } + /// helper function fn first_element(package: &sxd_document_no_unsafe::Package) -> Element<'_> { let doc = package.as_document(); diff --git a/src/rules_archive.rs b/src/rules_archive.rs index 938937683..dd10d3935 100644 --- a/src/rules_archive.rs +++ b/src/rules_archive.rs @@ -157,11 +157,7 @@ fn need_quotes(string: &str) -> bool { | '\x1c'..='\x1f' ) }) - || [ - "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE", - "false", "on", "On", "ON", "off", "Off", "OFF", "null", "Null", "NULL", "~", - ] - .contains(&string) + || crate::yaml_helpers::RESERVED_WORDS.contains(&string) || string.starts_with('.') || string.starts_with("0x") || string.parse::().is_ok() @@ -172,43 +168,8 @@ fn escape_yaml_string(out: &mut String, v: &str) { out.push('"'); let mut start = 0; for (i, byte) in v.bytes().enumerate() { - let escaped = match byte { - b'"' => "\\\"", - b'\\' => "\\\\", - b'\x00' => "\\u0000", - b'\x01' => "\\u0001", - b'\x02' => "\\u0002", - b'\x03' => "\\u0003", - b'\x04' => "\\u0004", - b'\x05' => "\\u0005", - b'\x06' => "\\u0006", - b'\x07' => "\\u0007", - b'\x08' => "\\b", - b'\t' => "\\t", - b'\n' => "\\n", - b'\x0b' => "\\u000b", - b'\x0c' => "\\f", - b'\r' => "\\r", - b'\x0e' => "\\u000e", - b'\x0f' => "\\u000f", - b'\x10' => "\\u0010", - b'\x11' => "\\u0011", - b'\x12' => "\\u0012", - b'\x13' => "\\u0013", - b'\x14' => "\\u0014", - b'\x15' => "\\u0015", - b'\x16' => "\\u0016", - b'\x17' => "\\u0017", - b'\x18' => "\\u0018", - b'\x19' => "\\u0019", - b'\x1a' => "\\u001a", - b'\x1b' => "\\u001b", - b'\x1c' => "\\u001c", - b'\x1d' => "\\u001d", - b'\x1e' => "\\u001e", - b'\x1f' => "\\u001f", - b'\x7f' => "\\u007f", - _ => continue, + let Some(escaped) = crate::yaml_helpers::escaped_byte(byte) else { + continue; }; if start < i { out.push_str(&v[start..i]); diff --git a/src/yaml_helpers.rs b/src/yaml_helpers.rs new file mode 100644 index 000000000..3c2b2a01a --- /dev/null +++ b/src/yaml_helpers.rs @@ -0,0 +1,50 @@ +//! YAML scalar quoting helpers shared by the pretty printer and Rules packager. + +/// Words that YAML may parse as booleans or null, so string values need quotes. +pub(crate) const RESERVED_WORDS: &[&str] = &[ + "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE", + "false", "on", "On", "ON", "off", "Off", "OFF", "null", "Null", "NULL", "~", +]; + +/// Returns the escape sequence for a byte in a double-quoted YAML string. +/// Returns `None` when the byte can be written without escaping. +pub(crate) fn escaped_byte(byte: u8) -> Option<&'static str> { + Some(match byte { + b'"' => "\\\"", + b'\\' => "\\\\", + b'\x00' => "\\u0000", + b'\x01' => "\\u0001", + b'\x02' => "\\u0002", + b'\x03' => "\\u0003", + b'\x04' => "\\u0004", + b'\x05' => "\\u0005", + b'\x06' => "\\u0006", + b'\x07' => "\\u0007", + b'\x08' => "\\b", + b'\t' => "\\t", + b'\n' => "\\n", + b'\x0b' => "\\u000b", + b'\x0c' => "\\f", + b'\r' => "\\r", + b'\x0e' => "\\u000e", + b'\x0f' => "\\u000f", + b'\x10' => "\\u0010", + b'\x11' => "\\u0011", + b'\x12' => "\\u0012", + b'\x13' => "\\u0013", + b'\x14' => "\\u0014", + b'\x15' => "\\u0015", + b'\x16' => "\\u0016", + b'\x17' => "\\u0017", + b'\x18' => "\\u0018", + b'\x19' => "\\u0019", + b'\x1a' => "\\u001a", + b'\x1b' => "\\u001b", + b'\x1c' => "\\u001c", + b'\x1d' => "\\u001d", + b'\x1e' => "\\u001e", + b'\x1f' => "\\u001f", + b'\x7f' => "\\u007f", + _ => return None, + }) +}