1 module zthor.thor;
2 
3 import std.stdio : File;
4 import std.typecons : Flag, No, Yes;
5 import zthor.constants;
6 import zthor.exception;
7 import zthor.types;
8 
9 /**
10  * Open the internal filehandle of the THOR struct.
11  *
12  * Uses std.stdio.File.reopen()
13  *
14  * Params:
15  *   thor = The THOR to open the filehandle for
16  *
17  * Throws: ErrnoException in case of error
18  */
19 void open(ref THOR thor)
20 in (!thor.filehandle.isOpen(), "Filehandle is already open")
21 {
22     thor.filehandle.reopen(thor.filename, "rb");
23 }
24 
25 /**
26  * Close the internal file handle of the THOR struct.
27  *
28  * Params:
29  *  thor = The THOR to close the filehandle for
30  *
31  * Throws: ErrnoException in case of error
32  */
33 void close(ref THOR thor)
34 {
35     thor.filehandle.close();
36 }
37 
38 /**
39  * Parses the header of the given THOR
40  *
41  * Format description: https://z0q.neocities.org/ragnarok-online-formats/thor/
42  *
43  * Params:
44  *   thor = The THOR struct to parse the header for
45  *
46  * Returns:
47  *   Input thor for easy chaining
48  *
49  * Throws:
50  *   ThorException if parsing fails due to wrong format. Exception or
51  *   ErrnoException in case of file operation failure.
52  */
53 ref THOR readHeader(return ref THOR thor)
54 in (thor.filehandle.isOpen(), "Filehandle of the THOR struct must be open " ~
55         "to read the header")
56 {
57     if (thor.header.ver > 0)
58     {
59         // Check if the header has been parsed already
60         // and prevent doing it again
61         return thor;
62     }
63     import core.stdc.stdio : SEEK_SET;
64 
65     thor.filehandle.seek(0, SEEK_SET);
66 
67     import zthor.constants : MIN_HEADER_LEN, MAX_HEADER_LEN;
68 
69     ubyte[MAX_HEADER_LEN] buffer;
70     auto actualRead = thor.filehandle.rawRead(buffer);
71 
72     if (actualRead.length < MIN_HEADER_LEN)
73     {
74         import std.format : format;
75 
76         throw new ThorException(format("THOR's filesize is too small to be valid. " ~
77                     "Expected the filesize to be at least %d bytes large but was actually " ~
78                     "%d bytes large.", MIN_HEADER_LEN, actualRead.length));
79     }
80 
81     thor.header.ver = 0x01;
82 
83     import std.bitmanip : littleEndianToNative;
84     import std.exception : collectException, enforce;
85     import std.conv : ConvException, to;
86 
87     thor.header.signature = buffer[0 .. 24];
88 
89     Exception err = collectException!ConvException(buffer[24].to!MergeMode,
90             thor.header.mergeMode);
91     enforce!ThorException(!err, err.msg);
92 
93     thor.header.filecount = littleEndianToNative!uint(buffer[25 .. 29]);
94 
95     err = collectException!ConvException((littleEndianToNative!ushort(buffer[29 .. 31])).to!ContainerMode,
96             thor.header.containerMode);
97     enforce!ThorException(!err, err.msg);
98 
99     import std.string : assumeUTF;
100     import std.encoding : transcode, Latin1String;
101 
102     ubyte grfTargetNameLength = buffer[31];
103     size_t idx = 32;
104 
105     if (grfTargetNameLength > 0)
106     {
107         (cast(Latin1String) buffer[idx .. (idx + grfTargetNameLength)]).transcode(thor.header.grfTargetName);
108         idx += grfTargetNameLength;
109     }
110 
111     if (thor.header.containerMode == ContainerMode.multiple)
112     {
113         ubyte[8] slice = buffer[idx .. (idx + 8)];
114         thor.header.filetableCompressedSize = littleEndianToNative!int(slice[0 .. 4]);
115         thor.header.filetableOffset = littleEndianToNative!int(slice[4 .. 8]);
116         idx += 8;
117     } else if (thor.header.containerMode == ContainerMode.single)
118     {
119         thor.header.filetableCompressedSize = 0;
120         thor.header.filetableOffset = 0;
121     }
122 
123     return thor;
124 }
125 
126 /**
127  * Parses the filetable of the given THOR.
128  * If filters is provided then only the files which
129  * matches the filters will be loaded.
130  *
131  * Params:
132  *  thor = The THOR to read the filetable from
133  *  filters = Array of filters
134  *
135  * Returns:
136  *  Input thor for easy chaining
137  */
138 ref THOR readFiletable(return ref THOR thor, const(wstring)[] filters = [],
139         Flag!"includeRemovals" includeRemovals = Yes.includeRemovals)
140 {
141     if (thor.header.containerMode == ContainerMode.multiple)
142     {
143         import zthor.filetable : fill;
144 
145         fill(thor, thor.files, filters, includeRemovals);
146     }
147     else if (thor.header.containerMode == ContainerMode.single)
148     {
149         import zthor.filetable : fillSingleFile;
150 
151         fillSingleFile(thor, thor.files, filters, includeRemovals);
152     }
153 
154     return thor;
155 }
156 
157 /**
158  * Parses a THOR file given optional filters.
159  *
160  * Calls [readHeader] and [readFiletable] on the input THOR.
161  *
162  * Params:
163  *  thor = The THOR file to parse
164  *  filters = The filters to use when parsing the filetable
165  *
166  * Returns:
167  *  Input thor for easy chaining
168  */
169 ref THOR parse(return ref THOR thor, const(wstring)[] filters = [],
170         Flag!"includeRemovals" includeRemovals = Yes.includeRemovals)
171 {
172     return thor.readHeader().readFiletable(filters, includeRemovals);
173 }
174 
175 /**
176  * Get the uncompressed data of a file inside the input THOR.
177  *
178  * This function will allocate new memory and always call the
179  * uncompressing routines _unless_ cache is set to true.
180  *
181  * Params:
182  *  thor = The THOR to read the file from
183  *  file = The metadata about the file to be read
184  *  thorHandle = Use this file handle instead of the one from thor
185  *  useCache = Return the data from cache if it exists
186  *
187  * Returns:
188  *  The unencrypted and uncompressed file data
189  */
190 ubyte[] getFileData(ref THOR thor, ref THORFile file, File thorHandle,
191         Flag!"useCache" useCache = No.useCache)
192 {
193     if (file.flags == FileFlags.remove || file.compressed_size == 0 || file.size == 0)
194     {
195         return [];
196     }
197     if (useCache && file.data != file.data.init)
198     {
199         return file.data;
200     }
201 
202     import core.stdc.stdio : SEEK_SET;
203 
204     thorHandle.seek(file.offset, SEEK_SET);
205 
206     scope ubyte[] compressedData = new ubyte[file.compressed_size];
207     thorHandle.rawRead(compressedData);
208 
209     import zgrf.compression : uncompress;
210 
211     if (useCache)
212     {
213         file.data = uncompress(compressedData, file.size);
214         return file.data;
215     }
216 
217     return uncompress(compressedData, file.size);
218 }
219 
220 
221 /**
222  * Get the uncompressed data of a file inside the input THOR.
223  *
224  * This function will always allocate new memory and always call the
225  * uncompressing routines.
226  *
227  * Params:
228  *  thor = The THOR to read the file from
229  *  file = The metadata about the file to be read
230  *  useCache = Return the data from cache if it exists
231  *
232  * Returns:
233  *  The uncompressed file data
234  */
235 ubyte[] getFileData(ref THOR thor, ref THORFile file, Flag!"useCache" useCache = No.useCache)
236 {
237     return getFileData(thor, file, thor.filehandle, useCache);
238 }
239 
240 /// ditto
241 ubyte[] getFileData(ref THORFile file, Flag!"useCache" useCache = No.useCache)
242 {
243     if (file.thor is null)
244     {
245         return [];
246     }
247 
248     return getFileData(*file.thor, file, useCache);
249 }
250 
251 /// ditto
252 ubyte[] getFileData(ref THOR thor, const wstring filename, Flag!"useCache" useCache = No.useCache)
253 {
254     import std.zlib : crc32;
255 
256     const uint hash = crc32(0, filename);
257     if (hash in thor.files)
258     {
259         return getFileData(thor, thor.files[hash], useCache);
260     }
261     else
262     {
263         return [];
264     }
265 }