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 }