compact binary serialization format with built-in compression
1//! Conversion utilities for converting NBT tags to Hateno format.
2use hateno::{Tag, TagKind};
3use nbtree_core::nbt::{Tag as NBTTag, TagId as NBTTagKind};
4use uuid::Uuid;
5
6#[derive(Debug, Clone, thiserror::Error)]
7pub enum Error {
8 #[error("Cannot convert NBT kind '{0:?}' to Hateno.")]
9 CannotConvertKind(NBTTagKind),
10}
11
12pub type Result<T> = core::result::Result<T, Error>;
13
14#[derive(Debug, Clone)]
15pub struct ConversionOptions {
16 /// Whether the converter should attempt to read Int Arrays with 4 items or
17 /// Long Arrays with 2 items as UUIDs (strictly v4).
18 pub attempt_to_convert_uuids: bool,
19}
20
21impl Default for ConversionOptions {
22 fn default() -> Self {
23 Self {
24 attempt_to_convert_uuids: false,
25 }
26 }
27}
28
29impl ConversionOptions {
30 pub fn attempt_to_convert_uuids(mut self, convert: bool) -> Self {
31 self.attempt_to_convert_uuids = convert;
32 self
33 }
34}
35
36/// Converts an NBT tag to its Hateno equivalent.
37///
38/// This function performs a recursive conversion of NBT data structures
39/// to Hateno format, handling the differences between the two formats:
40///
41/// - NBT byte/int/long arrays become Hateno `Array<i8>`/`Array<i32>`/`Array<i64>`
42/// - NBT lists of primitive types become Hateno homogeneous `Array`s
43/// - NBT lists of complex types become Hateno heterogeneous `List`s
44/// - NBT compounds become Hateno `Map`s with string keys
45pub fn convert_tag(tag: NBTTag, opts: &ConversionOptions) -> Result<Tag> {
46 match tag {
47 NBTTag::End => Err(Error::CannotConvertKind(NBTTagKind::End)),
48 NBTTag::Byte(byte) => Ok(Tag::I8(byte)),
49 NBTTag::Short(short) => Ok(Tag::I16(short)),
50 NBTTag::Int(int) => Ok(Tag::I32(int)),
51 NBTTag::Long(long) => Ok(Tag::I64(long)),
52 NBTTag::Float(float) => Ok(Tag::F32(float)),
53 NBTTag::Double(double) => Ok(Tag::F64(double)),
54 NBTTag::ByteArray(bytes) => Ok(Tag::Array(
55 TagKind::I8,
56 bytes.into_iter().map(Tag::I8).collect(),
57 )),
58 NBTTag::String(string) => Ok(Tag::String(string)),
59 NBTTag::List(tag_kind, tags) => match tag_kind {
60 NBTTagKind::Byte
61 | NBTTagKind::Short
62 | NBTTagKind::Int
63 | NBTTagKind::Long
64 | NBTTagKind::Float
65 | NBTTagKind::Double => {
66 let converted_kind = match tag_kind {
67 NBTTagKind::Byte => TagKind::I8,
68 NBTTagKind::Short => TagKind::I16,
69 NBTTagKind::Int => TagKind::I32,
70 NBTTagKind::Long => TagKind::I64,
71 NBTTagKind::Float => TagKind::F32,
72 NBTTagKind::Double => TagKind::F64,
73 _ => unreachable!(
74 "tag_kind should be one of the primitive types, got: {:?} despite already matching.",
75 tag_kind
76 ),
77 };
78
79 let converted_tags: core::result::Result<Vec<Tag>, _> = tags
80 .into_iter()
81 .map(|tag| convert_tag(tag, opts))
82 .collect();
83
84 Ok(Tag::Array(converted_kind, converted_tags?))
85 }
86 _ => {
87 let converted_tags: Result<Vec<Tag>> = tags
88 .into_iter()
89 .map(|tag| convert_tag(tag, opts))
90 .collect();
91 Ok(Tag::List(converted_tags?))
92 }
93 },
94 NBTTag::Compound(index_map) => {
95 let converted_map: Result<Vec<(Tag, Tag)>> = index_map
96 .into_iter()
97 .map(|(key, value)| {
98 Ok((Tag::String(key), convert_tag(*value, opts)?))
99 })
100 .collect();
101 Ok(Tag::Map(converted_map?))
102 }
103 NBTTag::IntArray(items) => {
104 if items.len() == 4 && opts.attempt_to_convert_uuids {
105 let bytes: [u8; 16] = [
106 (items[0] >> 24) as u8,
107 (items[0] >> 16) as u8,
108 (items[0] >> 8) as u8,
109 items[0] as u8,
110 (items[1] >> 24) as u8,
111 (items[1] >> 16) as u8,
112 (items[1] >> 8) as u8,
113 items[1] as u8,
114 (items[2] >> 24) as u8,
115 (items[2] >> 16) as u8,
116 (items[2] >> 8) as u8,
117 items[2] as u8,
118 (items[3] >> 24) as u8,
119 (items[3] >> 16) as u8,
120 (items[3] >> 8) as u8,
121 items[3] as u8,
122 ];
123
124 let uuid = Uuid::from_bytes(bytes);
125
126 if uuid.get_version() == Some(uuid::Version::Random) {
127 return Ok(Tag::Uuid(uuid));
128 } else {
129 return Ok(Tag::Array(
130 TagKind::I32,
131 items.into_iter().map(Tag::I32).collect(),
132 ));
133 }
134 } else {
135 Ok(Tag::Array(
136 TagKind::I32,
137 items.into_iter().map(Tag::I32).collect(),
138 ))
139 }
140 }
141 NBTTag::LongArray(items) => {
142 if items.len() == 2 && opts.attempt_to_convert_uuids {
143 let most = items[0] as u64; // treating as raw bits
144 let least = items[1] as u64; // ditto
145 let uuid = Uuid::from_u64_pair(most, least);
146
147 if uuid.get_version() == Some(uuid::Version::Random) {
148 Ok(Tag::Uuid(uuid))
149 } else {
150 Ok(Tag::Array(
151 TagKind::I64,
152 items.into_iter().map(Tag::I64).collect(),
153 ))
154 }
155 } else {
156 Ok(Tag::Array(
157 TagKind::I64,
158 items.into_iter().map(Tag::I64).collect(),
159 ))
160 }
161 }
162 }
163}