1 /// ERF file format (erf, hak, mod, ...)
2 module nwn.erf;
3 
4 import std.stdio: File;
5 import std.stdint;
6 import std.string;
7 import std.conv: to;
8 import std.datetime;
9 import std.typecons: Nullable;
10 import std.exception: enforce;
11 import nwnlibd.parseutils;
12 import nwn.constants;
13 public import nwn.constants: NwnVersion, ResourceType, Language;
14 
15 debug import std.stdio: writeln;
16 version(unittest) import std.exception: assertThrown, assertNotThrown;
17 
18 /// Parsing exception
19 class ErfParseException : Exception{
20 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
21 		super(msg, f, l, t);
22 	}
23 }
24 /// Value doesn't match constraints (ex: label too long)
25 class ErfValueSetException : Exception{
26 	@safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){
27 		super(msg, f, l, t);
28 	}
29 }
30 
31 ///
32 alias NWN1ErfFile = ErfFile!(NwnVersion.NWN1);
33 ///
34 alias NWN2ErfFile = ErfFile!(NwnVersion.NWN2);
35 
36 /// File stored in Erf class
37 struct ErfFile(NwnVersion NV){
38 	/// Construct a Erf file
39 	this(in string name, ResourceType type, ubyte[] data){
40 		this.name = name;
41 		this.type = type;
42 		this.data = data;
43 	}
44 
45 	this(string filePath){
46 		import std.file: read;
47 		import std.path: baseName, stripExtension, extension;
48 
49 		auto ext = ResourceType.invalid;
50 		immutable extStr = filePath.extension;
51 		if(extStr.length>1)
52 			ext = extStr[1..$].fileExtensionToResourceType;
53 
54 		this(
55 			filePath.stripExtension.baseName.toLower,
56 			ext,
57 			cast(ubyte[])filePath.read);
58 	}
59 	unittest{
60 		import std.file: tempDir, writeFile=write;
61 
62 		auto filePath = tempDir~"/unittest-nwn-lib-d-"~__MODULE__~".test.txt";
63 		filePath.writeFile("YOLO");
64 
65 		auto erfFile = NWN2ErfFile(filePath);
66 		assert(erfFile.name == "unittest-nwn-lib-d-nwn.erf.test");
67 		assert(erfFile.type == ResourceType.txt);
68 		assert(erfFile.data == "YOLO");
69 	}
70 
71 
72 	@property{
73 		/// File name (without file extension)
74 		string name()const{return m_name;}
75 		/// ditto
76 		void name(string value){
77 			if(value.length>(NV==NwnVersion.NWN1? 16 : 32))
78 				throw new ErfValueSetException(
79 					"file name cannot be longer than "~(NV==NwnVersion.NWN1? 16 : 32).to!string~" characters");
80 			m_name = value;
81 		}
82 	}
83 	private string m_name;
84 
85 	/// File type (related with its extension)
86 	ResourceType type;
87 
88 	/// File raw data
89 	ubyte[] data;
90 
91 	/// Expected file size. If not null, can be used to detect truncated files
92 	Nullable!size_t expectedLength;
93 }
94 
95 ///
96 alias NWN1Erf = Erf!(NwnVersion.NWN1);
97 ///
98 alias NWN2Erf = Erf!(NwnVersion.NWN2);
99 
100 /// ERF file parsing (.erf, .hak, .mod files)
101 class Erf(NwnVersion NV){
102 
103 	///
104 	this(){}
105 
106 	/// Parse raw binary data
107 	/// Params:
108 	///   data = ERF File raw data
109 	///   recover = Set to true to skip incomplete files and finish parsing a truncated ERF
110 	this(in ubyte[] data, bool recover = false){
111 		enforce!ErfParseException(data.length >= ErfHeader.sizeof, "ERF file is shorter than its header size. Cannot read");
112 
113 		const header = cast(ErfHeader*)data.ptr;
114 		fileType = header.file_type.idup.stripRight;
115 		fileVersion = header.file_version.idup.stripRight;
116 		auto date = Date(header.build_year+1900, 1, 1);
117 		date.dayOfYear = header.build_day + 1;
118 		buildDate = date;
119 
120 		enforce!ErfParseException(data.length > (header.localizedstrings_offset + header.localizedstrings_size),
121 			"Cannot read localized strings");
122 
123 		auto locStrings = ChunkReader(
124 				data[header.localizedstrings_offset
125 				.. header.localizedstrings_offset + header.localizedstrings_size]);
126 
127 		foreach(i ; 0..header.localizedstrings_count){
128 			immutable langage = cast(Language)locStrings.read!uint32_t;
129 			immutable length = locStrings.read!uint32_t;
130 
131 			description[langage] = locStrings.readArray!char(length).idup;
132 		}
133 
134 		const keys = cast(ErfKey*)(data.ptr + header.keys_offset);
135 		size_t keysLength = header.keys_count;
136 		const resources = cast(ErfResource*)(data.ptr + header.resources_offset);
137 
138 		enforce!ErfParseException(data.length > header.keys_offset, "Cannot read keys table");
139 		if(recover){
140 			if(header.keys_offset + ErfKey.sizeof * keysLength >= data.length){
141 				// Set lower keysLength
142 				const availableSize = data.length - header.keys_offset;
143 				keysLength = availableSize / ErfKey.sizeof;
144 			}
145 		}
146 
147 		// Read key table and allocate files
148 		files.length = keysLength;
149 		foreach(i, ref key ; keys[0 .. keysLength]){
150 			files[i].name = key.file_name.charArrayToString;
151 			files[i].type = cast(ResourceType)key.resource_type;
152 
153 			if(recover && header.resources_offset + ErfResource.sizeof * key.resource_id >= data.length){
154 				continue;
155 			}
156 			const res = &resources[key.resource_id];
157 			size_t startOffset = res.resource_offset;
158 			size_t endOffset = res.resource_offset + res.resource_size;
159 			files[i].expectedLength = res.resource_size;
160 
161 			if(recover){
162 				if(startOffset > data.length){
163 					// Null-sized file
164 					startOffset = 0;
165 					endOffset = 0;
166 				}
167 				else{
168 					// Truncate file
169 					endOffset = endOffset > data.length? data.length : endOffset;
170 				}
171 			}
172 
173 			files[i].data = data[startOffset .. endOffset].dup;
174 
175 		}
176 
177 	}
178 
179 	/// Localized module description
180 	string[Language] description;
181 
182 	/// Files contained in the erf file
183 	ErfFile!NV[] files;
184 
185 
186 	@property{
187 		/// File type (ERF, HAK, MOD, ...)
188 		/// Max width: 4 chars
189 		string fileType()const{return m_fileType;}
190 		/// ditto
191 		void fileType(string value){
192 			if(value.length>4)
193 				throw new ErfValueSetException("fileType cannot be longer than 4 characters");
194 			m_fileType = value;
195 		}
196 	}
197 	private string m_fileType;
198 
199 	@property{
200 		/// File version ("V1.0" for NWN1, "V1.1" for NWN2)
201 		/// Max width: 4 chars
202 		string fileVersion()const{return m_fileVersion;}
203 		/// ditto
204 		void fileVersion(string value){
205 			if(value.length>4)
206 				throw new ErfValueSetException("fileVersion cannot be longer than 4 characters");
207 			m_fileVersion = value;
208 		}
209 	}
210 	private string m_fileVersion;
211 
212 	@property{
213 		/// Date when the erf file has been created
214 		Date buildDate()const{return m_buildDate;}
215 		/// ditto
216 		void buildDate(Date value){
217 			if(value.year<1900)
218 				throw new ErfValueSetException("buildDate year must be >= 1900");
219 			m_buildDate = value;
220 		}
221 	}
222 	private Date m_buildDate = Date(1900, 1, 1);
223 
224 
225 	/// Serialize erf data except file content
226 	ubyte[] serializeHead(){
227 		ubyte[] ret;
228 		ret.length = ErfHeader.sizeof;
229 
230 		with(cast(ErfHeader*)ret.ptr){
231 			file_type = "    ";
232 			file_type[0..fileType.length] = fileType;
233 			file_version = "    ";
234 			file_version[0..fileVersion.length] = fileVersion;
235 
236 			localizedstrings_count = cast(uint32_t)description.length;
237 			keys_count = cast(uint32_t)files.length;
238 
239 			build_year = m_buildDate.year - 1900;
240 			build_day = m_buildDate.dayOfYear - 1;
241 
242 			file_description_strref = 0;//TODO: seems to be always 0
243 			//reserved: keep null values
244 
245 			localizedstrings_offset = ErfHeader.sizeof;
246 		}
247 
248 		size_t locstringLength = 0;
249 
250 		import std.algorithm: sort;
251 		import std.array: array;
252 		foreach(ref kv ; description.byKeyValue.array.sort!((a,b)=>a.key<b.key)){
253 			immutable langID = cast(uint32_t)kv.key;
254 			immutable length = cast(uint32_t)kv.value.length;
255 
256 			locstringLength += 4+4+length;
257 
258 			ret ~= cast(ubyte[])(&langID)[0..1].dup;
259 			ret ~= cast(ubyte[])(&length)[0..1].dup;
260 			ret ~= kv.value.dup;
261 		}
262 
263 		immutable keysOffset = ret.length;
264 		immutable resourcesOffset = keysOffset + files.length*ErfKey.sizeof;
265 
266 		with(cast(ErfHeader*)ret.ptr){
267 			localizedstrings_size = cast(uint32_t)locstringLength;
268 			keys_offset = cast(uint32_t)keysOffset;
269 			resources_offset = cast(uint32_t)resourcesOffset;
270 		}
271 
272 		ret.length += files.length*(ErfKey.sizeof + ErfResource.sizeof);
273 
274 
275 		size_t fileDataOffset = ret.length;
276 
277 		foreach(index, ref file ; files){
278 			with((cast(ErfKey*)(ret.ptr+keysOffset))[index]){
279 				file_name[0..file.name.length] = file.name.dup[0..$];
280 				resource_id   = cast(uint32_t)index;
281 				resource_type = file.type;
282 				reserved      = 0;
283 			}
284 
285 			with((cast(ErfResource*)(ret.ptr+resourcesOffset))[index]){
286 				resource_offset = cast(uint32_t)fileDataOffset;
287 				resource_size   = cast(uint32_t)file.data.length;
288 
289 				fileDataOffset += resource_size;
290 			}
291 		}
292 
293 		return ret;
294 	}
295 
296 	/// Serializes the erf data.
297 	/// Warning: can allocate a huge chunk of RAM
298 	ubyte[] serialize(){
299 		ubyte[] ret = serializeHead();
300 
301 		foreach(ref f ; files){
302 			ret ~= f.data;
303 		}
304 		return ret;
305 	}
306 
307 	/// Efficient writing to file, with less RAM consumption
308 	void writeToFile(File file){
309 		auto wr = file.lockingBinaryWriter;
310 		wr.rawWrite(serializeHead());
311 
312 		foreach(ref f ; files){
313 			wr.rawWrite(f.data);
314 		}
315 	}
316 
317 
318 private:
319 	align(1) static struct ErfHeader{
320 		char[4] file_type;
321 		char[4] file_version;
322 
323 		uint32_t localizedstrings_count;
324 		uint32_t localizedstrings_size;
325 		uint32_t keys_count;
326 
327 		uint32_t localizedstrings_offset;
328 		uint32_t keys_offset;
329 		uint32_t resources_offset;
330 
331 		uint32_t build_year;
332 		uint32_t build_day;
333 		uint32_t file_description_strref;
334 		ubyte[116] reserved;
335 	}
336 	align(1) static struct ErfKey{
337 		char[NV == NwnVersion.NWN1? 16 : 32] file_name;
338 		uint32_t resource_id;
339 		uint16_t resource_type;
340 		uint16_t reserved;
341 	}
342 	align(1) static struct ErfResource{
343 		uint32_t resource_offset;
344 		uint32_t resource_size;
345 	}
346 
347 }
348 
349 unittest{
350 	auto hak = new NWN2Erf(cast(ubyte[])import("test.hak"));
351 
352 	assert(hak.files[0].name == "eye");
353 	assert(hak.files[0].type == ResourceType.tga);
354 	assert(hak.files[1].name == "test");
355 	assert(hak.files[1].type == ResourceType.txt);
356 	assert(cast(string)hak.files[1].data == "Hello world\n");
357 
358 	auto modData = cast(ubyte[])import("module.mod");
359 	auto mod = new NWN2Erf(modData);
360 	assert(mod.description[Language.English]=="module description");
361 	assert(mod.buildDate == Date(2016, 6, 9));
362 
363 	assert(mod.serialize() == modData);
364 }