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 }