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 }