steel_utils/nbt/snbt/
writer.rs1use std::fmt::Write as _;
2
3use simdnbt::owned::NbtTag;
4
5use crate::{
6 java::{double_to_string, float_to_string},
7 nbt::nbt_list_values,
8};
9
10#[must_use]
15pub fn to_canonical_snbt(tag: &NbtTag) -> Option<String> {
16 let mut output = String::new();
17 write_canonical_snbt(tag, &mut output)?;
18 Some(output)
19}
20
21fn write_canonical_snbt(tag: &NbtTag, output: &mut String) -> Option<()> {
22 match tag {
23 NbtTag::Byte(value) => write!(output, "{value}b").ok()?,
24 NbtTag::Short(value) => write!(output, "{value}s").ok()?,
25 NbtTag::Int(value) => write!(output, "{value}").ok()?,
26 NbtTag::Long(value) => write!(output, "{value}L").ok()?,
27 NbtTag::Float(value) => write!(output, "{}f", float_to_string(*value)).ok()?,
28 NbtTag::Double(value) => write!(output, "{}d", double_to_string(*value)).ok()?,
29 NbtTag::ByteArray(values) => {
30 output.push_str("[B;");
31 for (index, value) in values.iter().enumerate() {
32 if index != 0 {
33 output.push(',');
34 }
35 write!(output, "{}B", *value as i8).ok()?;
36 }
37 output.push(']');
38 }
39 NbtTag::String(value) => {
40 quote_and_escape(&value.to_owned().try_into_string().ok()?, output);
41 }
42 NbtTag::List(values) => {
43 output.push('[');
44 for (index, value) in nbt_list_values(values).iter().enumerate() {
45 if index != 0 {
46 output.push(',');
47 }
48 write_canonical_snbt(value, output)?;
49 }
50 output.push(']');
51 }
52 NbtTag::Compound(compound) => {
53 let mut entries = compound
54 .iter()
55 .map(|(key, value)| Some((key.to_owned().try_into_string().ok()?, value)))
56 .collect::<Option<Vec<_>>>()?;
57 entries.sort_by(|(left, _), (right, _)| left.encode_utf16().cmp(right.encode_utf16()));
58 output.push('{');
59 for (index, (key, value)) in entries.into_iter().enumerate() {
60 if index != 0 {
61 output.push(',');
62 }
63 write_key(&key, output);
64 output.push(':');
65 write_canonical_snbt(value, output)?;
66 }
67 output.push('}');
68 }
69 NbtTag::IntArray(values) => {
70 output.push_str("[I;");
71 for (index, value) in values.iter().enumerate() {
72 if index != 0 {
73 output.push(',');
74 }
75 write!(output, "{value}").ok()?;
76 }
77 output.push(']');
78 }
79 NbtTag::LongArray(values) => {
80 output.push_str("[L;");
81 for (index, value) in values.iter().enumerate() {
82 if index != 0 {
83 output.push(',');
84 }
85 write!(output, "{value}L").ok()?;
86 }
87 output.push(']');
88 }
89 }
90 Some(())
91}
92
93fn write_key(value: &str, output: &mut String) {
94 let mut chars = value.chars();
95 let simple = !value.eq_ignore_ascii_case("true")
96 && !value.eq_ignore_ascii_case("false")
97 && chars.next().is_some_and(|character| {
98 character.is_ascii_alphabetic() || matches!(character, '.' | '_')
99 })
100 && chars.all(|character| {
101 character.is_ascii_alphanumeric() || matches!(character, '.' | '_' | '+' | '-')
102 });
103 if simple {
104 output.push_str(value);
105 } else {
106 quote_and_escape(value, output);
107 }
108}
109
110fn quote_and_escape(value: &str, output: &mut String) {
111 let quote = value
112 .chars()
113 .find_map(|character| match character {
114 '"' => Some('\''),
115 '\'' => Some('"'),
116 _ => None,
117 })
118 .unwrap_or('"');
119 output.push(quote);
120 for character in value.chars() {
121 match character {
122 '\\' => output.push_str("\\\\"),
123 character if character == quote => {
124 output.push('\\');
125 output.push(character);
126 }
127 '\u{0008}' => output.push_str("\\b"),
128 '\t' => output.push_str("\\t"),
129 '\n' => output.push_str("\\n"),
130 '\u{000c}' => output.push_str("\\f"),
131 '\r' => output.push_str("\\r"),
132 character if character < ' ' => {
133 let _ = write!(output, "\\x{:02x}", u32::from(character));
134 }
135 character => output.push(character),
136 }
137 }
138 output.push(quote);
139}