Silly NuShell plugin to read Minecraft NBT
1use std::io::Cursor;
2
3use nu_plugin::PluginCommand;
4use nu_protocol::{
5 Category, IntoPipelineData, IntoValue, LabeledError, PipelineData, Record, Signature, Span,
6 Type, Value, record,
7};
8use simdnbt::{
9 FromNbtTag,
10 borrow::{Nbt, NbtCompound, NbtList, NbtTag},
11};
12
13use crate::tags;
14
15pub struct FromNbt;
16
17fn tag_val(tag: u8, list_type: Option<u8>, val: Value, span: Span) -> Value {
18 Value::record(
19 record!("nbt_tag" => Value::int(tag as i64, span), "nbt_list_type" => list_type.map(|t| t as i64).into_value(span), "value" => val),
20 span,
21 )
22}
23
24macro_rules! arm {
25 ($struct:ident, $func:ident, $tag:ident, $span:ident) => {
26 Ok($struct::$func(&$tag).unwrap().into_value($span))
27 };
28}
29
30macro_rules! arm_array {
31 ($struct:ident, $func:ident, $tag:ident, $span:ident) => {
32 Ok(Vec::from($struct::$func(&$tag).unwrap()).into_value($span))
33 };
34}
35
36macro_rules! match_tag {
37 ($id:ident, $tag:ident, $struct:ident, $span:ident, [$($tag_id:pat => $ty:ident$(,)?)*], [$($arrtag_id:pat => $func:ident$(,)?)*], [$($rest_id:pat => $rest:expr$(,)?)*], $def:expr) => {
38 match $id {
39 $($tag_id => arm!($struct, $ty, $tag, $span),)*
40 $($arrtag_id => arm_array!($struct, $func, $tag, $span),)*
41 $($rest_id => $rest,)*
42 _ => $def,
43 }
44 }
45}
46
47fn nbt_to_val<'a: 'tape, 'tape>(
48 tag: NbtTag<'a, 'tape>,
49 span: Span,
50 do_tags: bool,
51) -> Result<Value, LabeledError> {
52 // Roundabout way of doing this, unwraps are safe bc we're checking with [NbtTag::id] first and
53 // the conversion functions do that too
54 let id = tag.id();
55 let list_type = tag.list().map(|l| l.id());
56 let res = match_tag!(id, tag, NbtTag, span, [
57 tags::BYTE_ID => byte,
58 tags::SHORT_ID => short,
59 tags::INT_ID => int,
60 tags::LONG_ID => long,
61 tags::FLOAT_ID => float,
62 tags::DOUBLE_ID => double,
63 ], [
64 tags::BYTE_ARRAY_ID => byte_array,
65 tags::INT_ARRAY_ID => int_array,
66 tags::LONG_ARRAY_ID => long_array,
67 ], [
68 tags::STRING_ID => Ok(String::from_nbt_tag(tag).unwrap().into_value(span)),
69 tags::COMPOUND_ID => compound_to_record(tag.compound().unwrap(), span, do_tags).map(|r| r.into_value(span)),
70 tags::LIST_ID => list_to_val(tag.list().unwrap(), span, do_tags),
71 ], Err(LabeledError::new(format!("Unknown NBT tag {id}"))));
72
73 if do_tags {
74 res.map(|v| tag_val(id, list_type, v, span))
75 } else {
76 res
77 }
78}
79
80fn list_to_val<'a: 'tape, 'tape>(
81 list: NbtList<'a, 'tape>,
82 span: Span,
83 do_tags: bool,
84) -> Result<Value, LabeledError> {
85 let id = list.id();
86 match_tag!(id, list, NbtList, span, [
87 tags::SHORT_ID => shorts,
88 tags::INT_ID => ints,
89 tags::LONG_ID => longs,
90 tags::FLOAT_ID => floats,
91 tags::DOUBLE_ID => doubles,
92 ], [
93 tags::BYTE_ID => bytes,
94 ], [
95 tags::STRING_ID => Ok(list.strings().unwrap().iter().map(|s| s.to_string()).collect::<Vec<_>>().into_value(span)),
96 tags::BYTE_ARRAY_ID => Ok(list.byte_arrays().unwrap().iter().map(|a| a.iter().copied().collect()).collect::<Vec<Vec<_>>>().into_value(span)),
97 tags::INT_ARRAY_ID => Ok(list.int_arrays().unwrap().iter().map(|a| a.to_vec()).collect::<Vec<_>>().into_value(span)),
98 tags::LONG_ARRAY_ID => Ok(list.long_arrays().unwrap().iter().map(|a| a.to_vec()).collect::<Vec<_>>().into_value(span)),
99 tags::LIST_ID => Ok(list.lists().unwrap().into_iter().map(|l| {
100 let inner_id = l.id();
101 let val = list_to_val(l, span, do_tags);
102 if do_tags { val.map(|v| tag_val(tags::LIST_ID, Some(inner_id), v, span)) } else { val }
103 }).collect::<Result<Vec<_>, _>>()?.into_value(span)),
104 tags::COMPOUND_ID => Ok(list.compounds().unwrap().into_iter().map(|c| {
105 let record = compound_to_record(c, span, do_tags);
106 record.map(|r| r.into_value(span))
107 }).collect::<Result<Vec<_>, _>>()?.into_value(span)),
108 tags::END_ID => Ok(Vec::<u8>::new().into_value(span)),
109 ], Err(LabeledError::new(format!("Unknown NBT list type: {id}"))))
110}
111
112fn compound_to_record<'a: 'tape, 'tape>(
113 compound: NbtCompound<'a, 'tape>,
114 span: Span,
115 do_tags: bool,
116) -> Result<Record, LabeledError> {
117 compound
118 .iter()
119 .map(|(s, v)| {
120 let key = s.to_string();
121 let value = nbt_to_val(v, span, do_tags);
122 value.map(|value| (key, value))
123 })
124 .collect()
125}
126
127fn parse_nbt(
128 src: &[u8],
129 src_span: Span,
130 call_span: Span,
131 do_tags: bool,
132) -> Result<PipelineData, LabeledError> {
133 let mut decoded_src_decoder = flate2::read::GzDecoder::new(&src[..]);
134 let mut input = Vec::new();
135 if std::io::Read::read_to_end(&mut decoded_src_decoder, &mut input).is_err() {
136 // oh probably wasn't gzipped then
137 input = src.to_vec();
138 }
139 let input = input.as_slice();
140
141 let nbt = simdnbt::borrow::read(&mut Cursor::new(input)).map_err(|err| {
142 let msg = format!("Failed to parse NBT data:\n{err:?}");
143 LabeledError::new(&msg).with_label("Invalid NBT data passed in".to_string(), src_span)
144 })?;
145
146 match nbt {
147 Nbt::Some(nbt) => {
148 let record = compound_to_record(nbt.as_compound(), call_span, do_tags)?;
149 Ok(Value::record(record, call_span))
150 }
151 Nbt::None => Ok(Value::nothing(call_span)),
152 }
153 .map(Value::into_pipeline_data)
154}
155
156impl PluginCommand for FromNbt {
157 type Plugin = crate::NbtPlugin;
158
159 fn name(&self) -> &str {
160 "from nbt"
161 }
162
163 fn signature(&self) -> nu_protocol::Signature {
164 Signature::build(self.name())
165 .input_output_type(Type::Binary, Type::record())
166 .switch("with-tags", "Whether to output NBT tag info (use this if you want a format that you can save data from)", Some('t'))
167 .category(Category::Formats)
168 }
169
170 fn description(&self) -> &str {
171 "Convert from a stream of raw NBT data"
172 }
173
174 fn run(
175 &self,
176 _plugin: &Self::Plugin,
177 _engine: &nu_plugin::EngineInterface,
178 call: &nu_plugin::EvaluatedCall,
179 input: nu_protocol::PipelineData,
180 ) -> Result<nu_protocol::PipelineData, nu_protocol::LabeledError> {
181 let do_tags = call.has_flag("with-tags")?;
182 match input {
183 PipelineData::Value(v, _) => {
184 let data = v.as_binary()?;
185 parse_nbt(&data, v.span(), call.head, do_tags)
186 }
187 PipelineData::ByteStream(stream, _) => {
188 let span = stream.span();
189 let data = stream.into_bytes()?;
190 parse_nbt(&data, span, call.head, do_tags)
191 }
192 _ => Err(
193 LabeledError::new("Expected value or byte stream from pipeline").with_label(
194 format!("requires value or byte stream; got {}", input.get_type(),),
195 call.head,
196 ),
197 ),
198 }
199 }
200}