1 /**
2  * Command-line interface.
3  *
4  * Authors: dd86k <dd@dax.moe>
5  * Copyright: None
6  * License: Public domain
7  */
8 module main;
9 
10 import std.conv : text;
11 import std.compiler : version_major, version_minor;
12 import std.file : dirEntries, DirEntry, SpanMode;
13 import std.format : format, formattedRead;
14 import std.getopt;
15 import std.path : baseName, dirName;
16 import std.stdio;
17 import ddh;
18 
19 private:
20 
21 enum PROJECT_VERSION = "1.1.0";
22 enum PROJECT_NAME    = "ddh";
23 
24 // Leave GC enabled, but avoid cleanup on exit
25 extern (C) __gshared string[] rt_options = [ "cleanup:none" ];
26 
27 // The DRT CLI is pretty useless
28 extern (C) __gshared bool rt_cmdline_enabled = false;
29 
30 debug enum BUILD_TYPE = "-debug";
31 else  enum BUILD_TYPE = "";
32 
33 immutable string TEXT_VERSION =
34 PROJECT_NAME~` v`~PROJECT_VERSION~BUILD_TYPE~` (`~__TIMESTAMP__~`)
35 Compiler: `~__VENDOR__~" FE v"~format("%u.%03u", version_major, version_minor);
36 
37 immutable string TEXT_HELP =
38 `Usage:
39   ddh page
40   ddh alias [options...] [{file|-}...]
41 `;
42 
43 immutable string TEXT_LICENSE =
44 `This is free and unencumbered software released into the public domain.
45 
46 Anyone is free to copy, modify, publish, use, compile, sell, or
47 distribute this software, either in source code form or as a compiled
48 binary, for any purpose, commercial or non-commercial, and by any
49 means.
50 
51 In jurisdictions that recognize copyright laws, the author or authors
52 of this software dedicate any and all copyright interest in the
53 software to the public domain. We make this dedication for the benefit
54 of the public at large and to the detriment of our heirs and
55 successors. We intend this dedication to be an overt act of
56 relinquishment in perpetuity of all present and future rights to this
57 software under copyright law.
58 
59 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
60 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
61 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
62 IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
63 OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
64 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
65 OTHER DEALINGS IN THE SOFTWARE.
66 
67 For more information, please refer to <http://unlicense.org/>`;
68 
69 immutable string STDIN_NAME = "-";
70 
71 enum DEFAULT_CHUNK_SIZE = 64 * 1024; // Seemed the best in benchmarks at least
72 
73 enum EntryMethod { file, text, list }
74 
75 struct Settings
76 {
77 	EntryMethod method;
78 	DDH_T ddh;
79 	char[] result;
80 	string listPath;
81 	size_t bufferSize = DEFAULT_CHUNK_SIZE;
82 	SpanMode spanMode;
83 	bool follow = true;
84 	bool textMode;
85 	bool tag;
86 	
87 	int function(ref Settings, string) hash = &hashFile;
88 	
89 	int select(DDHType type)
90 	{
91 		if (ddh_init(ddh, type))
92 			return 2;
93 		return 0;
94 	}
95 	
96 	void setEntryMode(string opt)
97 	{
98 		final switch (opt)
99 		{
100 		case "F|file":   hash = &hashFile; break;
101 		case "M|mmfile": hash = &hashMmfile; break;
102 		case "a|arg":	 method = EntryMethod.text; break;
103 		case "c|check":	 method = EntryMethod.list; break;
104 		}
105 	}
106 	
107 	void setFileModeText()
108 	{
109 		textMode = true;
110 	}
111 	
112 	void setFileModeBinary()
113 	{
114 		textMode = false;
115 	}
116 	
117 	void setBufferSize(string, string val)
118 	{
119 		ulong v = void;
120 		if (strtobin(&v, val))
121 			throw new GetOptException("Couldn't unformat buffer size");
122 		version (D_LP64) {}
123 		else {
124 			if (v >= uint.max)
125 				throw new GetOptException("Buffer size overflows");
126 		}
127 		bufferSize = cast(size_t)v;
128 	}
129 	
130 	void setSpanMode(string opt)
131 	{
132 		final switch (opt)
133 		{
134 		case "s|depth": spanMode = SpanMode.depth; return;
135 		case "shallow": spanMode = SpanMode.shallow; return;
136 		case "breath": spanMode = SpanMode.breadth; return;
137 		}
138 	}
139 	
140 	void setFollow()
141 	{
142 		follow = true;
143 	}
144 	
145 	void setNofollow()
146 	{
147 		follow = false;
148 	}
149 }
150 
151 version (Trace)
152 void trace(string func = __FUNCTION__, A...)(string fmt, A args)
153 {
154 	write("TRACE:", func, ": ");
155 	writefln(fmt, args);
156 }
157 
158 int printError(string func = __FUNCTION__, A...)(string fmt, A args)
159 {
160 	stderr.write(func, ": ");
161 	stderr.writefln(fmt, args);
162 	return 1;
163 }
164 int printError(ref Exception ex, string func = __FUNCTION__)
165 {
166 	debug stderr.writeln(ex);
167 	else  stderr.writefln("%s: %s", func, ex.msg);
168 	return 1;
169 }
170 void printResult(string fmt = "%s")(ref Settings settings, in char[] file)
171 {
172 	if (settings.tag)
173 	{
174 		write(meta_info[settings.ddh.type].tagname, '(');
175 		writef(fmt, file);
176 		writeln(")= ", settings.result);
177 	}
178 	else
179 	{
180 		write(settings.result, "  ");
181 		writefln(fmt, file);
182 	}
183 }
184 
185 // String to binary size
186 int strtobin(ulong *size, string input) {
187 	enum {
188 		K = 1024,
189 		M = K * 1024,
190 		G = M * 1024,
191 		T = G * 1024,
192 	}
193 	
194 	float f = void;
195 	char c = void;
196 	try
197 	{
198 		if (input.formattedRead!"%f%c"(f, c) != 2)
199 			return 1;
200 	}
201 	catch (Exception ex)
202 	{
203 		return 2;
204 	}
205 	
206 	if (f <= 0.0f) return 3;
207 	
208 	ulong u = cast(ulong)f;
209 	switch (c) {
210 	case 'T', 't': u *= T; break;
211 	case 'G', 'g': u *= G; break;
212 	case 'M', 'm': u *= M; break;
213 	case 'K', 'k': u *= K; break;
214 	case 'B', 'b': break;
215 	default: return 4;
216 	}
217 	
218 	// limit
219 	version (D_LP64) {
220 		if (u > (4L * G))
221 			return 5;
222 	} else {
223 		if (u > (2 * G))
224 			return 5;
225 	}
226 	
227 	*size = u;
228 	return 0;
229 }
230 /// 
231 unittest
232 {
233 	ulong s = void;
234 	strtobin(&s, "1K");
235 	assert(s == 1024);
236 	strtobin(&s, "1.1K");
237 	assert(s == 1024 + 102); // 102.4
238 }
239 
240 int hashFile(ref Settings settings, string path)
241 {
242 	version (Trace) trace("path=%s", path);
243 	
244 	try
245 	{
246 		File f;	// Must never be void
247 		// BUG: Using opAssign with LDC2 crashes at runtime
248 		f.open(path, settings.textMode ? "r" : "rb");
249 		
250 		if (f.size())
251 			hashFile(settings, f);
252 		
253 		f.close();
254 		return 0;
255 	}
256 	catch (Exception ex)
257 	{
258 		return printError(ex);
259 	}
260 }
261 int hashFile(ref Settings settings, ref File file)
262 {
263 	try
264 	{
265 		foreach (ubyte[] chunk; file.byChunk(settings.bufferSize))
266 			ddh_compute(settings.ddh, chunk);
267 		
268 		settings.result = ddh_string(settings.ddh);
269 		ddh_reset(settings.ddh);
270 		return 0;
271 	}
272 	catch (Exception ex)
273 	{
274 		return printError(ex);
275 	}
276 }
277 int hashMmfile(ref Settings settings, string path)
278 {
279 	import std.typecons : scoped;
280 	import std.mmfile : MmFile;
281 	
282 	version (Trace) trace("path=%s", path);
283 	
284 	try
285 	{
286 		auto mmfile = scoped!MmFile(path);
287 		ulong flen = mmfile.length;
288 		
289 		if (flen)
290 		{
291 			ulong start;
292 			
293 			if (flen > settings.bufferSize)
294 			{
295 				const ulong climit = flen - settings.bufferSize;
296 				for (; start < climit; start += settings.bufferSize)
297 					ddh_compute(settings.ddh,
298 						cast(ubyte[])mmfile[start..start + settings.bufferSize]);
299 			}
300 			
301 			// Compute remaining
302 			ddh_compute(settings.ddh, cast(ubyte[])mmfile[start..flen]);
303 		}
304 		
305 		settings.result = ddh_string(settings.ddh);
306 		ddh_reset(settings.ddh);
307 		return 0;
308 	}
309 	catch (Exception ex)
310 	{
311 		return printError(ex);
312 	}
313 }
314 int hashStdin(ref Settings settings, string)
315 {
316 	version (Trace) trace("stdin");
317 	return hashFile(settings, stdin);
318 }
319 int hashText(ref Settings settings, string text)
320 {
321 	version (Trace) trace("text='%s'", text);
322 	
323 	try
324 	{
325 		ddh_compute(settings.ddh, cast(ubyte[])text);
326 		settings.result = ddh_string(settings.ddh);
327 		ddh_reset(settings.ddh);
328 		return 0;
329 	}
330 	catch (Exception ex)
331 	{
332 		return printError(ex);
333 	}
334 }
335 
336 int entryFile(ref Settings settings, string path)
337 {
338 	version (Trace) trace("path=%s", path);
339 	
340 	uint count;
341 	string dir  = dirName(path);  // "." if anything
342 	string name = baseName(path); // Glob patterns are kept
343 	const bool same = dir == "."; // same directory name from dirName
344 	foreach (DirEntry entry; dirEntries(dir, name, settings.spanMode, settings.follow))
345 	{
346 		// Because entry will have "./" prefixed to it
347 		string file = same ? entry.name[2..$] : entry.name;
348 		++count;
349 		if (entry.isDir)
350 		{
351 			printError("'%s': Is a directory", file);
352 			continue;
353 		}
354 		if (settings.hash(settings, file))
355 		{
356 			continue;
357 		}
358 		printResult(settings, file);
359 	}
360 	if (count == 0)
361 		printError("'%s': No such file", name);
362 	return 0;
363 }
364 int entryStdin(ref Settings settings)
365 {
366 	version (Trace) trace("stdin");
367 	int e = hashStdin(settings, STDIN_NAME);
368 	if (e == 0)
369 		printResult(settings, STDIN_NAME);
370 	return e;
371 }
372 int entryText(ref Settings settings, string text)
373 {
374 	version (Trace) trace("text='%s'", text);
375 	int e = hashText(settings, text);
376 	if (e == 0)
377 		printResult!`"%s"`(settings, text);
378 	return e;
379 }
380 
381 int entryList(ref Settings settings, string listPath)
382 {
383 	import std.file : readText;
384 	import std..string : lineSplitter;
385 	
386 	version (Trace) trace("list=%s", listPath);
387 	
388 	/// Number of characters the hash string.
389 	size_t hashsize = ddh_digest_size(settings.ddh) << 1;
390 	/// Minimum line length (hash + 1 spaces).
391 	// Example: abcd1234  file.txt
392 	deprecated size_t minsize = hashsize + 1;
393 	uint currentLine, statMismatch, statErrors;
394 	
395 	try
396 	{
397 		string text = readText(listPath);
398 	
399 		if (text.length == 0)
400 			return printError("File '%s' is empty", listPath);
401 		
402 		string file = void, result = void, hash = void, lastHash;
403 		foreach (string line; lineSplitter(text)) // doesn't allocate
404 		{
405 			++currentLine;
406 			
407 			if (line.length == 0) continue; // empty
408 			if (line[0] == '#') continue; // comment
409 			
410 			if (settings.tag)
411 			{
412 				// Tested to work with and without spaces
413 				if (formattedRead(line, "%s(%s) = %s", hash, file, result) != 3)
414 				{
415 					++statErrors;
416 					printError("Formatting error at line %u", currentLine);
417 					continue;
418 				}
419 				
420 				if (hash == lastHash)
421 					goto L_ENTRY_HASH;
422 				
423 				lastHash = hash;
424 				
425 				foreach (DDHInfo info ; meta_info)
426 				{
427 					if (info.tagname == hash)
428 					{
429 						settings.select(info.type);
430 						goto L_ENTRY_HASH;
431 					}
432 				}
433 				
434 				printError("Hash tag not found at line %u", currentLine);
435 				continue;
436 			}
437 			else
438 			{
439 				// Tested to work with one or many spaces
440 				if (formattedRead(line, "%s %s", result, file) != 2)
441 				{
442 					++statErrors;
443 					printError("Formatting error at line %u", currentLine);
444 					continue;
445 				}
446 			}
447 		
448 L_ENTRY_HASH:
449 			if (settings.hash(settings, file))
450 			{
451 				++statErrors;
452 				continue;
453 			}
454 			
455 			version (Trace) trace("r1=%s r2=%s", settings.result, result);
456 			
457 			if (settings.result != result)
458 			{
459 				++statMismatch;
460 				writeln(file, ": FAILED");
461 				continue;
462 			}
463 			
464 			writeln(file, ": OK");
465 		}
466 		
467 	}
468 	catch (Exception ex)
469 	{
470 		return printError(ex);
471 	}
472 	
473 	if (statErrors || statMismatch)
474 		printError("%u mismatches, %u not read", statMismatch, statErrors);
475 	
476 	return 0;
477 }
478 
479 void showPage(string setting)
480 {
481 	import core.stdc.stdlib : exit;
482 	switch (setting)
483 	{
484 	case "ver": writeln(PROJECT_VERSION); break;
485 	case "version": writeln(TEXT_VERSION); break;
486 	case "license": writeln(TEXT_LICENSE); break;
487 	default: assert(0);
488 	}
489 	exit(0);
490 }
491 
492 int main(string[] args)
493 {
494 	const size_t argc = args.length;
495 	Settings settings;	/// CLI arguments
496 	GetoptResult res = void;
497 	try
498 	{
499 		res = getopt(args, config.caseInsensitive, config.passThrough,
500 		"F|file",     "Input mode: Regular file (default)", &settings.setEntryMode,
501 		"b|binary",   "File: Set binary mode (default)", &settings.setFileModeText,
502 		"t|text",     "File: Set text mode", &settings.setFileModeBinary,
503 		"M|mmfile",   "Input mode: Memory-map file", &settings.setEntryMode,
504 		"a|arg",      "Input mode: Command-line argument text, as UTF-8", &settings.setEntryMode,
505 		"c|check",    "Check hashes list in this file", &settings.setEntryMode,
506 		"C|chunk",    "Set buffer size, affects file/mmfile/stdin (default=64K)", &settings.setBufferSize,
507 		"shallow",    "Depth: Same directory (default)", &settings.setSpanMode,
508 		"s|depth",    "Depth: Deepest directories first", &settings.setSpanMode,
509 		"breadth",    "Depth: Sub directories first", &settings.setSpanMode,
510 		"follow",     "Links: Follow symbolic links (default)", &settings.setFollow,
511 		"nofollow",   "Links: Do not follow symbolic links", &settings.setNofollow,
512 		"tag",        "Create or read BSD-style checksums", &settings.tag,
513 		"version",    "Show version page and quit", &showPage,
514 		"ver",        "Show version and quit", &showPage,
515 		"license",    "Show license page and quit", &showPage,
516 		);
517 	}
518 	catch (Exception ex)
519 	{
520 		return printError(ex);
521 	}
522 	
523 	if (res.helpWanted)
524 	{
525 L_HELP:
526 		writeln(TEXT_HELP);
527 		foreach (Option opt; res.options)
528 		{
529 			with (opt) if (optShort)
530 				writefln("%s, %-12s  %s", optShort, optLong, help);
531 			else
532 				writefln("    %-12s  %s", optLong, help);
533 		}
534 		return 0;
535 	}
536 	
537 	if (argc < 2)
538 	{
539 		goto L_HELP;
540 	}
541 	
542 	string action = args[1];
543 	DDHType type = cast(DDHType)-1;
544 	
545 	// Aliases for hashes and checksums
546 	foreach (meta; meta_info)
547 	{
548 		if (meta.basename == action)
549 		{
550 			type = meta.type;
551 			break;
552 		}
553 	}
554 	
555 	// Pages
556 	if (type == -1)
557 	{
558 		switch (action)
559 		{
560 		case "list":
561 			writeln("Alias       Name");
562 			foreach (meta; meta_info)
563 				writefln("%-12s%s", meta.basename, meta.name);
564 			return 0;
565 		case "ver":
566 			showPage("ver");
567 			return 0;
568 		case "help":
569 			goto L_HELP;
570 		case "version":
571 			showPage("version");
572 			return 0;
573 		case "license":
574 			showPage("license");
575 			return 0;
576 		default:
577 			return printError("Unknown action '%s'", action);
578 		}
579 	}
580 	
581 	if (settings.select(type))
582 	{
583 		printError("Couldn't initiate hash module");
584 		return 2;
585 	}
586 	
587 	if (argc < 3)
588 		return entryStdin(settings);
589 	
590 	int function(ref Settings, string) entry = void;
591 	final switch (settings.method) with (EntryMethod)
592 	{
593 	case file: entry = &entryFile; version(Trace) trace("entryFile"); break;
594 	case list: entry = &entryList; version(Trace) trace("entryList"); break;
595 	case text: entry = &entryText; version(Trace) trace("entryText"); break;
596 	}
597 	
598 	foreach (string arg; args[2..$])
599 	{
600 		if (arg == STDIN_NAME) // stdin
601 		{
602 			if (entryStdin(settings))
603 				return 2;
604 			continue;
605 		}
606 		
607 		entry(settings, arg);
608 	}
609 	
610 	return 0;
611 }