1 /*
2  * Copyright (c) 2017-2020 sel-project
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in all
12  * copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20  * SOFTWARE.
21  *
22  */
23 /**
24  * Copyright: Copyright (c) 2017-2020 sel-project
25  * License: MIT
26  * Authors: Kripth
27  * Source: $(HTTP github.com/sel-project/sel-nbt/sel/nbt/file.d, sel/nbt/file.d)
28  */
29 module sel.nbt.file;
30 
31 import std.conv : to;
32 
33 static import std.bitmanip;
34 static import std.file;
35 import std.system : Endian;
36 import std.traits : isAbstractClass;
37 import std.typetuple : TypeTuple;
38 import std.zlib;
39 
40 import xbuffer.buffer : Buffer;
41 
42 import sel.nbt.stream;
43 import sel.nbt.tags;
44 
45 enum Compression {
46 
47 	deflate,
48 	gzip,
49 	none
50 
51 }
52 
53 /**
54  * Example:
55  * ---
56  * alias Level = Format!(Named!Compound, ClassicStream!(Endian.bigEndian), Compression.gzip);
57  * auto level = Level.read("level.dat");
58  * level.get!Compound("data")["LevelName"] = "Edited from code!";
59  * Level.write(level, "level.dat");
60  * ---
61  */
62 class Format(T : Tag, S : Stream, Compression c, int level=6) if(!isAbstractClass!T && !isAbstractClass!S) {
63 
64 	private static S stream;
65 
66 	public static this() {
67 		stream = new S(new Buffer(1024));
68 	}
69 
70 	public string location;
71 	public T tag;
72 
73 	public this(string location, T tag=null) {
74 		this.location = location;
75 		this.tag = tag;
76 	}
77 
78 	public this(T tag, string location="") {
79 		this(location, tag);
80 	}
81 	
82 	public T load() {
83 		
84 		stream.buffer.data = std.file.read(this.location);
85 
86 		this.loadHeader(stream.buffer);
87 		
88 		static if(c != Compression.none) {
89 			UnCompress uc = new UnCompress(cast(HeaderFormat)c);
90 			void[] data = stream.buffer.data!void.dup;
91 			stream.buffer.reset();
92 			stream.buffer.write(uc.uncompress(data));
93 			stream.buffer.write(uc.flush());
94 		}
95 
96 		//TODO nameless tag
97 		this.tag = cast(T)stream.readTag();
98 
99 		return this.tag;
100 		
101 	}
102 
103 	protected void loadHeader(Buffer buffer) {}
104 
105 	public T save() {
106 
107 		stream.buffer.reset();
108 		//TODO nameless tag
109 		stream.writeTag(this.tag);
110 
111 		static if(c != Compression.none) {
112 			Compress compress = new Compress(level, cast(HeaderFormat)c);
113 			stream.buffer.data = compress.compress(stream.buffer.data!void);
114 			stream.buffer.write(compress.flush());
115 		}
116 
117 		this.saveHeader(stream.buffer);
118 
119 		std.file.write(this.location, stream.buffer.data!void);
120 
121 		return this.tag;
122 
123 	}
124 
125 	protected void saveHeader(Buffer buffer) {}
126 
127 	alias tag this;
128 
129 }
130 
131 alias JavaLevelFormat = Format!(Compound, ClassicStream!(Endian.bigEndian), Compression.gzip);
132 
133 class PocketLevelFormat : Format!(Compound, ClassicStream!(Endian.littleEndian), Compression.none) {
134 
135 	private uint v = 5;
136 
137 	public this(string location, Compound tag=null) {
138 		super(location, tag);
139 	}
140 
141 	public this(Compound tag, string location="") {
142 		super(tag, location);
143 	}
144 
145 	protected override void loadHeader(Buffer buffer) {
146 		this.v = buffer.read!(Endian.littleEndian, uint)();
147 		buffer.read!(Endian.littleEndian, uint)(); // size
148 	}
149 
150 	protected override void saveHeader(Buffer buffer) {
151 		void[] data = buffer.data!void.dup;
152 		buffer.reset();
153 		buffer.write!(Endian.littleEndian)(this.v);
154 		buffer.write!(Endian.littleEndian)(data.length.to!uint);
155 		buffer.write(data);
156 	}
157 
158 	alias tag this;
159 
160 }
161 
162 unittest {
163 
164 	auto java = new JavaLevelFormat("test/java.dat");
165 	java.load();
166 	auto data = java.get!Compound("Data", null);
167 	assert(data !is null);
168 	assert(data.has!String("LevelName") && data.get!String("LevelName", null) == "New World");
169 	assert(data.has!Int("version") && data.get!Int("version", null) == 19133);
170 
171 	java = new JavaLevelFormat(new Compound(new Named!Int("Test", 42)), "test.dat");
172 	java.save();
173 	auto u = new UnCompress(HeaderFormat.gzip);
174 	auto b = cast(ubyte[])u.uncompress(std.file.read("test.dat"));
175 	b ~= cast(ubyte[])u.flush();
176 	assert(b == [10, 0, 0, 3, 0, 4, 84, 101, 115, 116, 0, 0, 0, 42, 0]);
177 
178 	auto pocket = new PocketLevelFormat("test/pocket.dat");
179 	pocket.load();
180 	assert(pocket.has!String("LevelName") && pocket.get!String("LevelName", null) == "AAAAAAAAAA");
181 	assert(pocket.has!Int("Difficulty") && pocket.get!Int("Difficulty", null) == 2);
182 
183 	pocket = new PocketLevelFormat(new Compound(new Named!Int("Test", 42)), "test.dat");
184 	pocket.save();
185 	assert(cast(ubyte[])std.file.read("test.dat") == [5, 0, 0, 0, 15, 0, 0, 0, 10, 0, 0, 3, 4, 0, 84, 101, 115, 116, 42, 0, 0, 0, 0]);
186 
187 }