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