1 /// Authors: Thibaut CHARLES (Crom) cromfr@gmail.com 2 /// License: GPL-3.0 3 /// Copyright: Copyright Thibaut CHARLES 2016 4 5 module tools.nwnerf; 6 7 import std.stdio; 8 import std.conv: to, ConvException; 9 import std.traits; 10 import std.string; 11 import std.typecons: Tuple, Nullable; 12 import std.file; 13 import std.exception; 14 import std.path; 15 import std.algorithm; 16 alias writeFile = std.file.write; 17 version(unittest) import std.exception: assertThrown, assertNotThrown; 18 19 import tools.common.getopt; 20 import nwn.constants; 21 import nwn.erf; 22 23 class ArgException : Exception{ 24 @safe pure nothrow this(string msg, string f=__FILE__, size_t l=__LINE__, Throwable t=null){ 25 super(msg, f, l, t); 26 } 27 } 28 29 30 int main(string[] args){ 31 if(args.length <= 1 || args[1] == "--help" || args[1] == "-h"){ 32 writeln("Parsing and serialization tool for ERF archive files (erf, hak, mod, pwc, ...)"); 33 writefln("Usage: %s (create|extract|info|list)", args[0].baseName); 34 return args.length <= 1; 35 } 36 if(args.any!(a => a == "--version")){ 37 import nwn.ver: NWN_LIB_D_VERSION; 38 writeln(NWN_LIB_D_VERSION); 39 return 0; 40 } 41 42 immutable command = args[1]; 43 args = args[0] ~ args[2..$]; 44 45 switch(command){ 46 case "create":{ 47 string outputPath; 48 string buildDateStr; 49 string type; 50 auto res1 = getopt(args, 51 "o|output", "Output file name", &outputPath, 52 "t|type", "File type. If not provided the type will be guessed from the file extension. Valid values are: hak, mod, erf, pwc", &type, 53 "date", "Set erf build date field. Format 'YYYY-MM-DD', or just 'now'. Defaults to 1900-01-01", &buildDateStr); 54 if(res1.helpWanted){ 55 improvedGetoptPrinter( 56 "Pack multiple files into a single NWN2 ERF/HAK/MOD file\n" 57 ~"Example: "~args[0].baseName~" create -o out_file.erf file1 file2 ...", 58 res1.options); 59 return 0; 60 } 61 enforce(args.length > 1, "No input file provided"); 62 enforce(outputPath !is null, "No output file provided"); 63 64 auto erf = new NWN2Erf(); 65 erf.fileVersion = "V1.1"; 66 67 auto ext = (type !is null ? type : outputPath.extension[1..$]).toUpper; 68 switch(ext){ 69 case "HAK", "MOD", "ERF", "PWC": 70 erf.fileType = ext; 71 break; 72 default: 73 enforce(0, format!"Unknown ERF file type %s"(ext)); 74 } 75 76 import std.datetime: Clock, Date; 77 if(buildDateStr == "now") 78 erf.buildDate = cast(Date)Clock.currTime; 79 else if(buildDateStr !is null) 80 erf.buildDate = Date.fromISOExtString(buildDateStr); 81 82 83 void addFile(DirEntry file){ 84 if(file.isFile){ 85 erf.files ~= NWN2ErfFile(file); 86 } 87 else if(file.isDir){ 88 import std.algorithm: sort; 89 import std.array: array; 90 import std.uni: icmp; 91 foreach(path ; file.dirEntries(SpanMode.shallow).array.sort!"icmp(a.name, b.name) < 0"){ 92 addFile(DirEntry(path)); 93 } 94 } 95 else{ 96 writeln("Ignored file: '", file, "'"); 97 } 98 } 99 100 foreach(const ref path ; args[1..$]){ 101 addFile(DirEntry(path)); 102 } 103 104 erf.writeToFile(File(outputPath, "w+")); 105 106 }break; 107 108 case "extract":{ 109 string outputPath = "."; 110 bool recover = false; 111 auto res1 = getopt(args, 112 "o|output", "Output folder path", &outputPath, 113 "r|recover", "Recover files from truncated file", &recover, 114 ); 115 if(res1.helpWanted){ 116 improvedGetoptPrinter( 117 "Extract an ERF file content\n" 118 ~"Example: "~args[0].baseName~" extract -o dir/ yourfile.erf", 119 res1.options); 120 return !res1.helpWanted; 121 } 122 123 enforce(args.length > 1, "No input file provided"); 124 enforce(args.length <= 2, "Too many input files provided"); 125 126 auto erf = new NWN2Erf(cast(ubyte[])args[$-1].read, recover); 127 foreach(ref file ; erf.files){ 128 auto filePath = buildNormalizedPath( 129 outputPath, 130 file.name ~ "." ~ resourceTypeToFileExtension(file.type)); 131 132 if(recover){ 133 if(file.data.length == 0){ 134 writeln("No data available for file '", filePath.baseName, "'"); 135 continue; 136 } 137 else if(file.data.length != file.expectedLength) { 138 filePath ~= ".part"; 139 writeln("Truncated file: '", filePath.baseName, "'"); 140 } 141 } 142 std.file.write(filePath, file.data); 143 } 144 145 }break; 146 147 case "info":{ 148 if(args.any!(a => a == "-h" || a == "--help")){ 149 writeln("Extract an ERF file content"); 150 writeln("Example: "~args[0].baseName~" info yourfile.erf"); 151 return 0; 152 } 153 enforce(args.length > 1, "No input file provided"); 154 enforce(args.length <= 2, "Too many input files provided"); 155 156 auto erf = new NWN2Erf(cast(ubyte[])args[1].read); 157 writeln("File type: ", erf.fileType); 158 writeln("File version: ", erf.fileVersion); 159 writeln("Build date: ", erf.buildDate); 160 writeln("Module description: ", erf.description); 161 writeln("File count: ", erf.files.length); 162 }break; 163 164 case "list":{ 165 if(args.any!(a => a == "-h" || a == "--help")){ 166 writeln("List files contained inside a ERF file"); 167 writeln("Example: "~args[0].baseName~" list yourfile.erf"); 168 return 0; 169 } 170 enforce(args.length > 1, "No input file provided"); 171 enforce(args.length <= 2, "Too many input files provided"); 172 173 auto erf = new NWN2Erf(cast(ubyte[])args[1].read); 174 175 import std.math: log10; 176 int idxColWidth = cast(int)(log10(erf.files.length))+1; 177 foreach(i, const ref file ; erf.files){ 178 writeln(i.to!string.rightJustify(idxColWidth),"|", 179 file.name.leftJustify(32+1), 180 file.type); 181 } 182 }break; 183 184 default: 185 writefln("Unknown command '%s'. Try %s --help", command, args[0].baseName); 186 return -1; 187 } 188 return 0; 189 } 190 191 unittest { 192 import std.file; 193 import std.path; 194 195 auto erfFile = buildPath(tempDir, "unittest-nwn-lib-d-"~__MODULE__~".erf"); 196 scope(success) std.file.remove(erfFile); 197 198 auto stdout_ = stdout; 199 auto tmpOut = buildPath(tempDir, "unittest-nwn-lib-d-"~__MODULE__~".out"); 200 stdout = File(tmpOut, "w"); 201 scope(success) std.file.remove(tmpOut); 202 scope(exit) stdout = stdout_; 203 204 205 206 assert(main(["nwn-erf"]) != 0); 207 assert(main(["nwn-erf","--help"]) == 0); 208 assert(main(["nwn-erf","--version"]) == 0); 209 210 // Create 211 assertThrown(main(["nwn-erf","create"])); 212 assert(main(["nwn-erf","create","--help"]) == 0); 213 214 assertThrown(main(["nwn-erf","create","-o","nonstandard.extension","../../unittest/dds_test_rgba.dds"])); 215 assert(main(["nwn-erf","create","-o",erfFile,"../../unittest/dds_test_rgba.dds","../../unittest/test_cost_armor.uti","../../unittest/WalkmeshObjects.trx"]) == 0); 216 217 // Info 218 assertThrown(main(["nwn-erf","info"])); 219 assert(main(["nwn-erf","info","--help"]) == 0); 220 assertThrown(main(["nwn-erf","info",erfFile,"too_many_files.erf"])); 221 assert(main(["nwn-erf","info",erfFile]) == 0); 222 223 // List 224 assertThrown(main(["nwn-erf","list"])); 225 assert(main(["nwn-erf","info","--help"]) == 0); 226 assertThrown(main(["nwn-erf","list",erfFile,"too_many_files.erf"])); 227 stdout.reopen(null, "w"); 228 assert(main(["nwn-erf","list",erfFile]) == 0); 229 stdout.flush(); 230 assert(tmpOut.readText.splitLines.length == 3); 231 232 // Extract 233 assertThrown(main(["nwn-erf","extract"])); 234 assert(main(["nwn-erf","extract","--help"]) == 0); 235 assert(main(["nwn-erf","extract","-o",tempDir,erfFile]) == 0); 236 237 foreach(f ; ["dds_test_rgba.dds","test_cost_armor.uti","walkmeshobjects.trx"]) 238 std.file.remove(buildPath(tempDir, f)); 239 }