Silly NuShell plugin to read Minecraft NBT
at main 200 lines 7.1 kB view raw
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}