Skip to main content

steel_utils/nbt/snbt/
writer.rs

1use 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/// Renders an NBT value with Vanilla `Tag::toString` semantics.
11///
12/// Compound keys use Java string ordering, and floating-point values use the
13/// decimal selected by `Float.toString` / `Double.toString`.
14#[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}