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 }