git.delta.rocks / jrsonnet / refs/commits / 9d0e47dd9d08

difftreelog

refactor implement yaml escaping rules as in std.jsonnet

trrytlnrYaroslav Bolyukin2026-02-08parent: #0ad7242.patch.diff
in: master

1 file changed

modifiedcrates/jrsonnet-stdlib/src/manifest/yaml.rsdiffbeforeafterboth
28 /// safe_key: 128 /// safe_key: 1
29 /// ```29 /// ```
30 quote_keys: bool,30 quote_keys: bool,
31 quote_values: bool,
31 /// If true - then order of fields is preserved as written,32 /// If true - then order of fields is preserved as written,
32 /// instead of sorting alphabetically33 /// instead of sorting alphabetically
33 #[cfg(feature = "exp-preserve-order")]34 #[cfg(feature = "exp-preserve-order")]
43 padding: Cow::Owned(padding.clone()),44 padding: Cow::Owned(padding.clone()),
44 arr_element_padding: Cow::Owned(padding),45 arr_element_padding: Cow::Owned(padding),
45 quote_keys: false,46 quote_keys: false,
47 quote_values: false,
46 #[cfg(feature = "exp-preserve-order")]48 #[cfg(feature = "exp-preserve-order")]
47 preserve_order,49 preserve_order,
48 }50 }
56 padding: Cow::Borrowed(" "),58 padding: Cow::Borrowed(" "),
57 arr_element_padding: Cow::Borrowed(if indent_array_in_object { " " } else { "" }),59 arr_element_padding: Cow::Borrowed(if indent_array_in_object { " " } else { "" }),
58 quote_keys,60 quote_keys,
61 quote_values: true,
59 #[cfg(feature = "exp-preserve-order")]62 #[cfg(feature = "exp-preserve-order")]
60 preserve_order,63 preserve_order,
61 }64 }
67 }70 }
68}71}
6972
70/// From <https://github.com/chyh1990/yaml-rust/blob/da52a68615f2ecdd6b7e4567019f280c433c1521/src/emitter.rs#L289>
71/// With added date check
72fn yaml_needs_quotes(string: &str) -> bool {73fn bare_safe(key: &str) -> bool {
74 fn count_char_u(k: &str, c: char) -> usize {
75 let cu = c.to_ascii_uppercase();
76 k.chars().filter(|v| *v == c || *v == cu).count()
77 }
78 fn count_char(k: &str, c: char) -> usize {
79 k.chars().filter(|v| *v == c).count()
80 }
73 fn need_quotes_spaces(string: &str) -> bool {81 fn is_reserved(key: &str) -> bool {
82 const RESERVED: &[&str] = &[
83 // Boolean types taken from https://yaml.org/type/bool.html
84 "true", "false", "yes", "no", "on", "off", "y", "n",
85 // Numerical words taken from https://yaml.org/type/float.html
86 ".nan", "-.inf", "+.inf", ".inf", "null",
87 // Invalid keys that contain no invalid characters
88 "-", "---", "",
89 ];
74 string.starts_with(' ') || string.ends_with(' ')90 RESERVED.iter().any(|k| key.eq_ignore_ascii_case(k))
75 }91 }
7692
77 string.is_empty()93 // Check for unsafe characters
78 || need_quotes_spaces(string)94 if !key
79 || string.starts_with(['&' , '*' , '?' , '|' , '-' , '<' , '>' , '=' , '!' , '%' , '@'])95 .chars()
80 || string.contains(|c| matches!(c, ':' | '{' | '}' | '[' | ']' | ',' | '#' | '`' | '\"' | '\'' | '\\' | '\0'..='\x06' | '\t' | '\n' | '\r' | '\x0e'..='\x1a' | '\x1c'..='\x1f'))96 .all(|v| matches!(v, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '/'))
81 || [97 {
82 // http://yaml.org/type/bool.html98 return false;
83 "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "true", "False", "FALSE", "false",99 }
84 "on", "On", "ON", "off", "Off", "OFF", // http://yaml.org/type/null.html100 // Check for reserved words
85 "null", "Null", "NULL", "~",101 if is_reserved(key) {
86 // > Quoted in std.jsonnet, however, in serde_yaml they were quoted:102 return false;
87 // > Note: 'y', 'Y', 'n', 'N', is not quoted deliberately, as in libyaml. PyYAML also parse103 }
88 // > them as string, not booleans, although it is violating the YAML 1.1 specification.104 // Check for timestamp values. Since spaces and colons are already forbidden,
89 // > See https://github.com/dtolnay/serde-yaml/pull/83#discussion_r152628088.105 // all that could potentially pass is the standard date format (ex MM-DD-YYYY, YYYY-DD-MM, etc).
90 "y", "Y", "n", "N",106 // This check is even more conservative: Keys that meet all of the following:
91 "-.inf", "+.inf", ".inf",107 // - all characters match [0-9\-]
92 "-", "---", ""108 // - has exactly 2 dashes
109 // are considered dates.
93 ].contains(&string)110 if key.chars().all(|v| matches!(v, '0'..='9' | '-')) && count_char(key, '-') == 2 {
111 return false;
112 }
113 // Check for integers. Keys that meet all of the following:
114 // - all characters match [0-9_\-]
115 // - has at most 1 dash
116 // are considered integers.
94 || (string.chars().all(|c| matches!(c, '0'..='9' | '-'))117 else if key.chars().all(|v| matches!(v, '0'..='9' | '-' | '_')) && count_char(key, '-') < 2 {
95 && string.chars().filter(|c| *c == '-').count() == 2)118 return false;
96 || string.starts_with('.')119 }
120 // Check for binary integers. Keys that meet all of the following:
121 // - all characters match [0-9b_\-]
122 // - has at least 3 characters
123 // - starts with (-)0b
124 // are considered binary integers.
125 else if key
126 .chars()
127 .all(|v| matches!(v, '0'..='9' | '-' | '_' | 'b' | 'B'))
97 || string.starts_with("0x")128 && (key.starts_with("0b") || key.starts_with("-0b"))
98 || string.parse::<i64>().is_ok()129 && key.len() > 2
130 {
131 return false;
132 }
133 // Check for floats. Keys that meet all of the following:
134 // - all characters match [0-9e._\-]
135 // - has at most a single period
136 // - has at most two dashes
137 // - has at most 1 'e'
138 // are considered floats.
139 else if key
140 .chars()
141 .all(|v| matches!(v, '0'..='9' | '-' | '_' | 'e' | 'E' | '.'))
142 && count_char_u(key, 'e') < 2
143 && count_char(key, '-') < 3
144 && count_char(key, '.') <= 1
145 {
146 return false;
147 }
148 // Check for hexadecimals. Keys that meet all of the following:
149 // - all characters match [0-9a-fx_\-]
150 // - has at most 1 dash
151 // - has at least 3 characters
152 // - starts with (-)0x
153 // are considered hexadecimals.
154 else if key
155 .chars()
99 || string.parse::<f64>().is_ok()156 .all(|v| matches!(v, '0'..='9' | '-' | '_' | 'x' | 'X' | 'a'..='f' | 'A'..='F' ))
157 && key.len() >= 3
158 && count_char(key, '-') < 2
159 && (key.starts_with("-0x") || key.starts_with("0x"))
160 {
161 return false;
162 }
163 true
100}164}
101165
102#[allow(dead_code)]166#[allow(dead_code)]
142 buf.push_str(&options.padding);206 buf.push_str(&options.padding);
143 buf.push_str(line);207 buf.push_str(line);
144 }208 }
145 } else if !options.quote_keys && !yaml_needs_quotes(&s) {209 } else if !options.quote_values && bare_safe(&s) {
146 buf.push_str(&s);210 buf.push_str(&s);
147 } else {211 } else {
148 escape_string_json_buf(&s, buf);212 escape_string_json_buf(&s, buf);
203 buf.push('\n');267 buf.push('\n');
204 buf.push_str(cur_padding);268 buf.push_str(cur_padding);
205 }269 }
206 if !options.quote_keys && !yaml_needs_quotes(&key) {270 if !options.quote_keys && bare_safe(&key) {
207 buf.push_str(&key);271 buf.push_str(&key);
208 } else {272 } else {
209 escape_string_json_buf(&key, buf);273 escape_string_json_buf(&key, buf);