diff --git a/src/metadata/yaml/emit.rs b/src/metadata/yaml/emit.rs index e0c0fb18..8fc1053d 100644 --- a/src/metadata/yaml/emit.rs +++ b/src/metadata/yaml/emit.rs @@ -178,10 +178,16 @@ fn is_yaml_whitespace(c: char) -> bool { c == ' ' || c == '\t' } +fn is_yaml_line_break(c: char) -> bool { + c == '\x0A' || c == '\x0D' +} + fn is_flow_indicator(c: char) -> bool { matches!(c, '[' | ']' | '{' | '}' | ',') } +/// This disallows multi-line unquoted strings, which YAML does allow in some +/// contexts, but there's no expectation of such strings coming out of libloot. fn can_emit_unquoted(value: &str, style: YamlStyle) -> bool { // if value.is_empty() @@ -230,11 +236,13 @@ fn can_emit_unquoted(value: &str, style: YamlStyle) -> bool { return false; } - if style == YamlStyle::Flow { - !value.contains(is_flow_indicator) - } else { - true + if style == YamlStyle::Flow && value.contains(is_flow_indicator) { + return false; } + + value + .chars() + .all(|c| is_printable(c) && !is_yaml_line_break(c) && c != '\u{FEFF}') } fn single_quote(value: &str) -> String { @@ -245,7 +253,7 @@ fn single_quote(value: &str) -> String { fn double_quote(value: &str) -> String { // - value + let escaped: String = value .chars() .map(|c| { if is_printable(c) { @@ -275,7 +283,9 @@ fn double_quote(value: &str) -> String { } } }) - .collect() + .collect(); + + format!("\"{}\"", escaped) } impl EmitYaml for &[T] { @@ -307,3 +317,69 @@ impl EmitYaml for Vec { self.as_slice().emit_yaml(emitter); } } + +#[cfg(test)] +mod tests { + use super::*; + + mod yaml_emitter { + use super::*; + + mod unquoted_str { + use super::*; + + #[test] + fn unquoted_str_should_emit_string_as_given() { + let value = "hello world"; + let mut emitter = YamlEmitter::new(); + emitter.unquoted_str(value); + + assert_eq!(value, emitter.into_string()); + } + + #[test] + fn unquoted_str_should_fall_back_to_single_quoting_string_if_it_cannot_be_emitted_unquoted() + { + let value = " hello world"; + let mut emitter = YamlEmitter::new(); + emitter.unquoted_str(value); + + assert_eq!(format!("'{}'", value), emitter.into_string()); + } + + #[test] + fn unquoted_str_should_fall_back_to_double_quoting_string_if_it_cannot_be_unquoted_or_single_quoted() + { + let value = "\x1B[1mhello world\x1B[0m"; + let mut emitter = YamlEmitter::new(); + emitter.unquoted_str(value); + + assert_eq!("\"\\e[1mhello world\\e[0m\"", emitter.into_string()); + } + } + + mod single_quoted_str { + use super::*; + + #[test] + fn single_quoted_str_should_emit_string_wrapped_in_single_quotes_and_with_single_quotes_doubled() + { + let value = "hello 'world'"; + let mut emitter = YamlEmitter::new(); + emitter.single_quoted_str(value); + + assert_eq!("'hello ''world'''", emitter.into_string()); + } + + #[test] + fn single_quoted_str_should_fall_back_to_double_quoting_string_if_it_contains_non_printable_characters() + { + let value = "\x1B[1mhello world\x1B[0m"; + let mut emitter = YamlEmitter::new(); + emitter.single_quoted_str(value); + + assert_eq!("\"\\e[1mhello world\\e[0m\"", emitter.into_string()); + } + } + } +}