1 /** 2 Copyright: Copyright (c) 2016 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 module nbt; 7 8 private import std..string : format; 9 private import std.traits : Unqual, isArray, isAssociativeArray, isBoolean, isDynamicArray, 10 isExpressionTuple, isFloatingPoint, isIntegral, isSomeChar, isStaticArray, isUnsigned; 11 public import std.typecons : Flag, Yes, No; 12 private import std.range : ElementEncodingType, hasLength, save, tee; 13 private import std.conv : to; 14 private import std.utf : byChar; 15 private import std.range : isInputRange, isOutputRange, ElementType; 16 private import std.typecons : isTuple; 17 18 enum VisitRes {r_break, r_continue} 19 unittest 20 { 21 enum testFile = `D:\voxelman\tools\minecraft_import\extra\test.data`; 22 import std.file; 23 auto fileData = cast(ubyte[])read(testFile); 24 printNbtStream(fileData[]); 25 26 double[3] pos; 27 28 VisitRes visitor(ref ubyte[] input, NbtTag tag) 29 { 30 import std.stdio; 31 if (tag.name == "Pos") 32 { 33 pos[0] = decodeNbtTag(input, NbtTagType.tag_double, "").floating; 34 pos[1] = decodeNbtTag(input, NbtTagType.tag_double, "").floating; 35 pos[2] = decodeNbtTag(input, NbtTagType.tag_double, "").floating; 36 writefln("Found Pos %s", pos); 37 return VisitRes.r_break; 38 } 39 else 40 { 41 writefln("visit %s %s int %s", tag.type, tag.name, tag.integer); 42 return visitNbtValue(input, tag, &visitor); 43 } 44 //return VisitRes.r_continue; 45 } 46 47 visitNbtStream(fileData, &visitor); 48 } 49 50 51 enum NbtTagType : ubyte { 52 tag_end, 53 tag_byte, 54 tag_short, 55 tag_int, 56 tag_long, 57 tag_float, 58 tag_double, 59 tag_byte_array, 60 tag_string, 61 tag_list, 62 tag_compound, // map 63 tag_int_array, 64 } 65 66 struct NbtTag 67 { 68 NbtTagType type; 69 string name; 70 union 71 { 72 long integer; 73 double floating; 74 struct { 75 // used for storing arrays, map and string size 76 uint length; 77 NbtTagType itemType; // shows list items' type 78 } 79 } 80 81 this(NbtTagType type) { this.type = type; } 82 this(NbtTagType type, string name) { this.type = type; this.name = name; } 83 84 this(NbtTagType type, string name, double floating) { 85 this.type = type; 86 this.name = name; 87 this.floating = floating; 88 } 89 90 this(NbtTagType type, string name, long integer) { 91 this.type = type; 92 this.name = name; 93 this.integer = integer; 94 } 95 96 this(NbtTagType type, string name, uint length) { 97 this.type = type; 98 this.name = name; 99 this.length = length; 100 } 101 102 this(NbtTagType type, string name, NbtTagType itemType, uint length) { 103 this.type = type; 104 this.name = name; 105 this.itemType = itemType; 106 this.length = length; 107 } 108 } 109 110 NbtTag decodeNbtNamedTag(R)(auto ref R input) 111 if(isInputRange!R && is(ElementType!R == ubyte)) 112 { 113 import std.array; 114 115 if (input.empty) onInsufficientInput(); 116 117 NbtTagType type = cast(NbtTagType)input.front; 118 input.popFront; 119 120 if (type > NbtTagType.max) onUnsupportedTag(type); 121 122 if (type == NbtTagType.tag_end) 123 return NbtTag(NbtTagType.tag_end); 124 125 ushort nameLength = readInteger!ushort(input); 126 string name = cast(string)readBytes(input, nameLength); 127 128 return decodeNbtTag(input, type, name); 129 } 130 131 NbtTag decodeNbtTag(R)(auto ref R input, NbtTagType type, string name) 132 if(isInputRange!R && is(ElementType!R == ubyte)) 133 { 134 final switch(type) with(NbtTagType) 135 { 136 case tag_end: 137 return NbtTag(tag_end); 138 case tag_byte: 139 return NbtTag(type, name, cast(long)readInteger!byte(input)); 140 case tag_short: 141 return NbtTag(type, name, cast(long)readInteger!short(input)); 142 case tag_int: 143 return NbtTag(type, name, cast(long)readInteger!int(input)); 144 case tag_long: 145 return NbtTag(type, name, cast(long)readInteger!long(input)); 146 case tag_float: 147 __FloatRep fr = {u : readInteger!uint(input)}; 148 return NbtTag(type, name, fr.f); 149 case tag_double: 150 __DoubleRep dr = {u : readInteger!ulong(input)}; 151 return NbtTag(type, name, dr.d); 152 case tag_byte_array: 153 return NbtTag(type, name, cast(uint)readInteger!uint(input)); 154 case tag_string: 155 return NbtTag(type, name, cast(uint)readInteger!ushort(input)); 156 case tag_list: 157 return NbtTag(type, name, cast(NbtTagType)readInteger!ubyte(input), cast(uint)readInteger!uint(input)); 158 case tag_compound: 159 return NbtTag(type, name); 160 case tag_int_array: 161 return NbtTag(type, name, cast(uint)readInteger!uint(input)); 162 } 163 assert(false); 164 } 165 166 private union __FloatRep { float f; uint u;} 167 private union __DoubleRep { double d; ulong u; } 168 169 VisitRes visitNbtStream(R, V)(auto ref R input, V visitor) 170 if(isInputRange!R && is(ElementType!R == ubyte)) 171 { 172 while(input.length > 0) 173 { 174 NbtTag tag = decodeNbtNamedTag(input); 175 if (tag.type == NbtTagType.tag_end) 176 return VisitRes.r_continue; 177 if (visitor(input, tag) == VisitRes.r_break) 178 return VisitRes.r_break; 179 } 180 return VisitRes.r_continue; 181 } 182 183 VisitRes visitNbtValue(R, V)(auto ref R input, NbtTag tag, V visitor) 184 if(isInputRange!R && is(ElementType!R == ubyte)) 185 { 186 final switch(tag.type) with(NbtTagType) 187 { 188 case tag_end: return VisitRes.r_continue; 189 190 case tag_byte: return VisitRes.r_continue; 191 case tag_short: return VisitRes.r_continue; 192 case tag_int: return VisitRes.r_continue; 193 case tag_long: return VisitRes.r_continue; 194 195 case tag_float: return VisitRes.r_continue; 196 case tag_double: return VisitRes.r_continue; 197 198 case tag_byte_array: readBytes(input, tag.length); return VisitRes.r_continue; 199 case tag_string: readBytes(input, tag.length); return VisitRes.r_continue; 200 case tag_list: return visitNbtList(input, visitor, tag.itemType, tag.length); 201 case tag_compound: return visitNbtStream(input, visitor); 202 case tag_int_array: readBytes(input, tag.length*4); return VisitRes.r_continue; 203 } 204 } 205 206 VisitRes visitNbtList(string singleIndent=" ", R, V)( 207 auto ref R input, 208 V visitor, 209 NbtTagType type, 210 uint length 211 ) 212 if(isInputRange!R && is(ElementType!R == ubyte)) 213 { 214 foreach(i; 0..length) { 215 NbtTag tag = decodeNbtTag(input, type, ""); 216 if (visitor(input, tag) == VisitRes.r_break) 217 return VisitRes.r_break; 218 //printNbtValue!singleIndent(input, sink, tag, ulong.max, indent); 219 } 220 return VisitRes.r_continue; 221 } 222 223 224 /// Outputs textual representation of Nbt stream into sink or stdout if not provided. 225 void printNbtStream(string singleIndent=" ", R)(auto ref R input) 226 { 227 import std.stdio : stdout; 228 auto writer = stdout.lockingTextWriter; 229 printNbtStream!singleIndent(input, writer); 230 } 231 232 /// ditto 233 void printNbtStream(string singleIndent=" ", Sink, R)( 234 auto ref R input, 235 auto ref Sink sink, 236 ulong numItems = ulong.max, 237 string indent = "" 238 ) 239 if(isInputRange!R && is(ElementType!R == ubyte) && isOutputRange!(Sink, char)) 240 { 241 while(input.length > 0 && numItems > 0) 242 { 243 NbtTag tag = decodeNbtNamedTag(input); 244 if (tag.type == NbtTagType.tag_end) 245 return; 246 printNbtValue!singleIndent(input, sink, tag, ulong.max, indent); 247 --numItems; 248 } 249 } 250 251 void printNbtList(string singleIndent=" ", Sink, R)( 252 auto ref R input, 253 auto ref Sink sink, 254 NbtTagType type, 255 uint length, 256 string indent = "" 257 ) 258 if(isInputRange!R && is(ElementType!R == ubyte) && isOutputRange!(Sink, char)) 259 { 260 foreach(i; 0..length) { 261 NbtTag tag = decodeNbtTag(input, type, ""); 262 printNbtValue!singleIndent(input, sink, tag, ulong.max, indent); 263 } 264 } 265 266 void printNbtIntArray(string singleIndent=" ", Sink, R)( 267 auto ref R input, 268 auto ref Sink sink, 269 uint length, 270 string indent = "" 271 ) 272 if(isInputRange!R && is(ElementType!R == ubyte) && isOutputRange!(Sink, char)) 273 { 274 import std.format : formattedWrite; 275 if (length) 276 { 277 uint integer = readInteger!uint(input); 278 formattedWrite(sink, "%s(%s", indent, integer); 279 } 280 else 281 formattedWrite(sink, "%s(", indent); 282 283 284 auto bytes = readBytes(input, (length-1)*4); 285 //foreach(i; 1..length) { 286 // uint integer = readInteger!uint(input); 287 // formattedWrite(sink, ", %s", integer); 288 //} 289 formattedWrite(sink, ")\n"); 290 } 291 292 void printNbtValue(string singleIndent=" ", Sink, R)( 293 auto ref R input, 294 auto ref Sink sink, 295 NbtTag tag, 296 ulong numItems = ulong.max, 297 string indent = "" 298 ) 299 if(isInputRange!R && is(ElementType!R == ubyte) && isOutputRange!(Sink, char)) 300 { 301 import std.format : formattedWrite; 302 final switch(tag.type) with(NbtTagType) 303 { 304 case tag_end: return; 305 306 case tag_byte: formattedWrite(sink, "%sbyte(%s): %s\n", indent, tag.name, tag.integer); break; 307 case tag_short: formattedWrite(sink, "%sshort(%s): %s\n", indent, tag.name, tag.integer); break; 308 case tag_int: formattedWrite(sink, "%sint(%s): %s\n", indent, tag.name, tag.integer); break; 309 case tag_long: formattedWrite(sink, "%slong(%s): %s\n", indent, tag.name, tag.integer); break; 310 311 case tag_float: formattedWrite(sink, "%stag_float(%s): %s\n", indent, tag.name, tag.floating); break; 312 case tag_double: formattedWrite(sink, "%stag_double(%s): %s\n", indent, tag.name, tag.floating); break; 313 314 case tag_byte_array: 315 formattedWrite(sink, "%sbyte array(%s): %s\n", 316 indent, tag.name, tag.length, ); 317 auto bytes = readBytes(input, tag.length); 318 //formattedWrite(sink, "%s%s(%(%02x%))\n", indent, singleIndent, bytes); 319 break; 320 case tag_string: 321 formattedWrite(sink, "%sstring(%s): %s\n%s%s\"%s\"\n", 322 indent, tag.name, tag.length, indent, singleIndent, cast(string)readBytes(input, tag.length)); 323 break; 324 case tag_list: 325 formattedWrite(sink, "%slist(%s): %s\n", indent, tag.name, tag.length); 326 printNbtList!singleIndent(input, sink, tag.itemType, tag.length, indent~singleIndent); 327 break; 328 case tag_compound: 329 formattedWrite(sink, "%scompound(%s)\n", indent, tag.name); 330 printNbtStream!singleIndent(input, sink, ulong.max, indent~singleIndent); 331 break; 332 case tag_int_array: 333 formattedWrite(sink, "%sint array(%s): %s\n", indent, tag.name, tag.length); 334 printNbtIntArray!singleIndent(input, sink, tag.length, indent~singleIndent); 335 break; 336 } 337 } 338 339 private T readInteger(T, R)(auto ref R input) 340 if(isInputRange!R && is(ElementType!R == ubyte)) 341 { 342 enum ubyte size = T.sizeof; 343 import std.algorithm : copy; 344 import std.bitmanip : bigEndianToNative; 345 import std.range : dropExactly, take; 346 347 static assert(T.sizeof == size); 348 static assert(size > 0); 349 if (input.length < size) onInsufficientInput(); 350 351 ubyte[size] data; 352 353 copy(take(input, size), data[]); 354 input = input.dropExactly(size); 355 T result = bigEndianToNative!(T, size)(data); 356 357 return result; 358 } 359 360 // Reads byte array from input range. On 32-bit can read up to uint.max bytes. 361 // If ubyte[] is passed as input, a slice will be returned. 362 // Make sure to dup array when input buffer is reused. 363 ubyte[] readBytes(R)(auto ref R input, ulong length) 364 if(isInputRange!R && is(ElementType!R == ubyte)) 365 { 366 import std.array; 367 import std.range : take; 368 if (input.length < length) onInsufficientInput(); 369 370 static if (size_t.sizeof < ulong.sizeof) 371 if (length > size_t.max) 372 throw new NbtException(format("Array size is too big %s", length)); 373 374 size_t dataLength = cast(size_t)length; 375 ubyte[] result; 376 static if (is(R == ubyte[])) 377 { 378 result = input[0..dataLength]; 379 input = input[dataLength..$]; 380 } 381 else 382 { 383 result = take(input, dataLength).array; // TODO allocation 384 } 385 386 return result; 387 } 388 389 390 class NbtException : Exception 391 { 392 @trusted pure @nogc this(string message, string file = __FILE__, size_t line = __LINE__) 393 { 394 super(message, file, line); 395 } 396 } 397 398 private: 399 400 auto customEmplace(T, A...)(void[] buffer, A args) @nogc 401 { 402 buffer[] = typeid(T).initializer; 403 return (cast(T)buffer.ptr).__ctor(args); 404 } 405 406 NbtException getException(A...)(string file, size_t line, string fmt, A args) @nogc 407 { 408 static ubyte[__traits(classInstanceSize, NbtException)] exceptionBuffer; 409 static char[512] charBuffer; 410 import core.stdc.stdio : snprintf; 411 int written = snprintf(charBuffer.ptr, charBuffer.length, fmt.ptr, args); 412 return customEmplace!NbtException(exceptionBuffer, cast(string)charBuffer[0..written], file, line); 413 } 414 415 void onCastErrorToFrom(To)(NbtTagType from, string file = __FILE__, size_t line = __LINE__) @nogc 416 { 417 throw getException(file, line, "Attempt to cast %s to %s", from, typeid(To)); 418 } 419 420 void onInsufficientInput(string file = __FILE__, size_t line = __LINE__) @nogc 421 { 422 throw getException(file, line, "Input range is too short"); 423 } 424 425 void onUnsupportedTag(ubyte tag, string file = __FILE__, size_t line = __LINE__) @nogc 426 { 427 throw getException(file, line, "Unsupported tag found: %02x", tag); 428 }