Skip to content

Commit c405db6

Browse files
authored
Implement serialize_yaml_scalar using serde-saphyr (#1534)
1 parent 7950d57 commit c405db6

File tree

4 files changed

+68
-51
lines changed

4 files changed

+68
-51
lines changed

Cargo.lock

Lines changed: 0 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ libc = { version = "0.2.164" }
5252
# Enable static linking for liblzma
5353
# This is required for the `xz` feature in `async-compression`
5454
liblzma = { version = "0.4.5", features = ["static"] }
55-
libyaml = { version = "0.2.0" }
5655
mea = { version = "0.6.3" }
5756
memchr = { version = "2.7.5" }
5857
owo-colors = { version = "4.1.0" }

crates/prek/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ globset = { workspace = true }
5555
# Enable static linking for liblzma
5656
# This is required for the `xz` feature in `async-compression`
5757
liblzma = { workspace = true, features = ["static"] }
58-
libyaml = { workspace = true }
5958
mea = { workspace = true }
6059
memchr = { workspace = true }
6160
owo-colors = { workspace = true }

crates/prek/src/yaml.rs

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,57 @@
44
// option. This file may not be copied, modified, or distributed
55
// except according to those terms.
66

7-
use anyhow::Result;
8-
use bstr::ByteSlice;
9-
use libyaml::{Emitter, Encoding, Event, ScalarStyle};
7+
use std::fmt::Write;
108

119
/// Serialize a YAML scalar while preserving the caller's quote style.
12-
pub(crate) fn serialize_yaml_scalar(value: &str, quote: &str) -> Result<String> {
13-
let style = match quote {
14-
"'" => Some(ScalarStyle::SingleQuoted),
15-
"\"" => Some(ScalarStyle::DoubleQuoted),
16-
_ => None,
17-
};
10+
pub(crate) fn serialize_yaml_scalar(value: &str, quote: &str) -> anyhow::Result<String> {
11+
match quote {
12+
"'" => Ok(format!("'{}'", escape_single_quoted(value))),
13+
"\"" => Ok(format!("\"{}\"", escape_double_quoted(value))),
14+
_ => {
15+
if is_simple_plain(value) {
16+
Ok(value.to_owned())
17+
} else {
18+
// Defer to serde-saphyr to select quoting/escaping for non-trivial scalars.
19+
let rendered = serde_saphyr::to_string(&value)?;
20+
Ok(rendered.trim_end_matches('\n').to_owned())
21+
}
22+
}
23+
}
24+
}
1825

19-
let mut writer = Vec::new();
20-
{
21-
let mut emitter = Emitter::new(&mut writer)?;
22-
emitter.emit(Event::StreamStart {
23-
encoding: Some(Encoding::Utf8),
24-
})?;
25-
emitter.emit(Event::DocumentStart {
26-
version: None,
27-
tags: vec![],
28-
implicit: true,
29-
})?;
30-
emitter.emit(Event::Scalar {
31-
anchor: None,
32-
tag: None,
33-
value: value.to_owned(),
34-
plain_implicit: true,
35-
quoted_implicit: true,
36-
style,
37-
})?;
38-
emitter.emit(Event::DocumentEnd { implicit: true })?;
39-
emitter.emit(Event::StreamEnd {})?;
40-
emitter.flush()?;
26+
/// Fast-path: allow simple, plain scalars we want to keep unquoted.
27+
fn is_simple_plain(value: &str) -> bool {
28+
if value.is_empty() {
29+
return false;
4130
}
42-
let trimmed = writer.trim_end();
43-
Ok(str::from_utf8(trimmed)?.to_owned())
31+
value
32+
.chars()
33+
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '_' | '/' | '+' | '@'))
34+
}
35+
36+
/// YAML single-quoted strings escape a single quote by doubling it.
37+
fn escape_single_quoted(value: &str) -> String {
38+
value.replace('\'', "''")
39+
}
40+
41+
/// YAML double-quoted strings use backslash escapes for control characters.
42+
fn escape_double_quoted(value: &str) -> String {
43+
let mut escaped = String::with_capacity(value.len());
44+
for ch in value.chars() {
45+
match ch {
46+
'\\' => escaped.push_str("\\\\"),
47+
'"' => escaped.push_str("\\\""),
48+
'\t' => escaped.push_str("\\t"),
49+
'\n' => escaped.push_str("\\n"),
50+
'\r' => escaped.push_str("\\r"),
51+
c if c.is_control() => {
52+
let _ = write!(escaped, "\\u{:04X}", c as u32);
53+
}
54+
c => escaped.push(c),
55+
}
56+
}
57+
escaped
4458
}
4559

4660
#[cfg(test)]
@@ -61,11 +75,32 @@ mod tests {
6175
assert_eq!(rendered, "'123'");
6276
let rendered = serialize_yaml_scalar("123", "\"").unwrap();
6377
assert_eq!(rendered, "\"123\"");
78+
let rendered = serialize_yaml_scalar("a:b", "").unwrap();
79+
assert_eq!(rendered, "a:b");
6480
let rendered = serialize_yaml_scalar("a:b", "'").unwrap();
6581
assert_eq!(rendered, "'a:b'");
6682
let rendered = serialize_yaml_scalar("a\"b", "\"").unwrap();
6783
assert_eq!(rendered, "\"a\\\"b\"");
6884
let rendered = serialize_yaml_scalar("a'b", "'").unwrap();
6985
assert_eq!(rendered, "'a''b'");
86+
87+
let rendered = serialize_yaml_scalar("abc def", "").unwrap();
88+
assert_eq!(rendered, "abc def");
89+
let rendered = serialize_yaml_scalar("abc def", "'").unwrap();
90+
assert_eq!(rendered, "'abc def'");
91+
let rendered = serialize_yaml_scalar("abc def", "\"").unwrap();
92+
assert_eq!(rendered, "\"abc def\"");
93+
}
94+
95+
#[test]
96+
fn serialize_yaml_scalar_quotes_and_escapes() {
97+
let rendered = serialize_yaml_scalar("a\\b", "\"").unwrap();
98+
assert_eq!(rendered, "\"a\\\\b\"");
99+
let rendered = serialize_yaml_scalar("a\nb", "\"").unwrap();
100+
assert_eq!(rendered, "\"a\\nb\"");
101+
let rendered = serialize_yaml_scalar("a\tb", "\"").unwrap();
102+
assert_eq!(rendered, "\"a\\tb\"");
103+
let rendered = serialize_yaml_scalar("a\\b", "'").unwrap();
104+
assert_eq!(rendered, "'a\\b'");
70105
}
71106
}

0 commit comments

Comments
 (0)