1 /// Command-line interface.
2 ///
3 /// Authors: dd86k <dd@dax.moe>
4 /// Copyright: No rights reserved
5 /// License: CC0
6 module main;
7 
8 import std.compiler : version_major, version_minor;
9 import std.file : dirEntries, DirEntry, SpanMode, read;
10 import std.format : format, formattedRead;
11 import std.getopt;
12 import std.path : baseName, dirName;
13 import std.stdio;
14 import core.stdc.stdlib : exit;
15 import blake2d : BLAKE2D_VERSION_STRING;
16 import sha3d : SHA3D_VERSION_STRING;
17 import ddh;
18 import gitinfo;
19 
20 private:
21 
22 alias readAll = read;
23 
24 // GDC isn't happy with int*
25 extern(C) int sscanf(scope const char* s, scope const char* format, scope ...);
26 
27 // Leave GC enabled, but avoid cleanup on exit
28 extern (C) __gshared string[] rt_options = ["cleanup:none"];
29 
30 debug {} else
31 {
32     // Disables the Druntime GC command-line interface
33     // except for debug builds
34     extern (C) __gshared bool rt_cmdline_enabled = false;
35 }
36 
37 enum DEFAULT_READ_SIZE = 4 * 1024;
38 enum TagType
39 {
40     gnu,
41     bsd,
42     sri,
43     plain
44 }
45 
46 debug enum BUILD_TYPE = "+debug";
47 else enum BUILD_TYPE = "";
48 
49 immutable string PAGE_VERSION =
50 `ddh ` ~ GIT_DESCRIPTION ~ BUILD_TYPE ~ ` (built: ` ~ __TIMESTAMP__ ~ `)
51 Using sha3-d ` ~ SHA3D_VERSION_STRING ~ `, blake2-d ` ~ BLAKE2D_VERSION_STRING ~ `
52 No rights reserved
53 License: CC0
54 Homepage: <https://github.com/dd86k/ddh>
55 Compiler: ` ~ __VENDOR__ ~ " v" ~ format("%u.%03u", version_major, version_minor);
56 
57 immutable string PAGE_HELP =
58 `Usage: ddh [options...|--autocheck] [files...|--stdin]
59 
60 Options
61     --            Stop processing options.`;
62 
63 immutable string PAGE_LICENSE =
64 `Creative Commons Legal Code
65 
66 CC0 1.0 Universal
67 
68     CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
69     LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
70     ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
71     INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
72     REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
73     PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
74     THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
75     HEREUNDER.
76 
77 Statement of Purpose
78 
79 The laws of most jurisdictions throughout the world automatically confer
80 exclusive Copyright and Related Rights (defined below) upon the creator
81 and subsequent owner(s) (each and all, an "owner") of an original work of
82 authorship and/or a database (each, a "Work").
83 
84 Certain owners wish to permanently relinquish those rights to a Work for
85 the purpose of contributing to a commons of creative, cultural and
86 scientific works ("Commons") that the public can reliably and without fear
87 of later claims of infringement build upon, modify, incorporate in other
88 works, reuse and redistribute as freely as possible in any form whatsoever
89 and for any purposes, including without limitation commercial purposes.
90 These owners may contribute to the Commons to promote the ideal of a free
91 culture and the further production of creative, cultural and scientific
92 works, or to gain reputation or greater distribution for their Work in
93 part through the use and efforts of others.
94 
95 For these and/or other purposes and motivations, and without any
96 expectation of additional consideration or compensation, the person
97 associating CC0 with a Work (the "Affirmer"), to the extent that he or she
98 is an owner of Copyright and Related Rights in the Work, voluntarily
99 elects to apply CC0 to the Work and publicly distribute the Work under its
100 terms, with knowledge of his or her Copyright and Related Rights in the
101 Work and the meaning and intended legal effect of CC0 on those rights.
102 
103 1. Copyright and Related Rights. A Work made available under CC0 may be
104 protected by copyright and related or neighboring rights ("Copyright and
105 Related Rights"). Copyright and Related Rights include, but are not
106 limited to, the following:
107 
108   i. the right to reproduce, adapt, distribute, perform, display,
109      communicate, and translate a Work;
110  ii. moral rights retained by the original author(s) and/or performer(s);
111 iii. publicity and privacy rights pertaining to a person's image or
112      likeness depicted in a Work;
113  iv. rights protecting against unfair competition in regards to a Work,
114      subject to the limitations in paragraph 4(a), below;
115   v. rights protecting the extraction, dissemination, use and reuse of data
116      in a Work;
117  vi. database rights (such as those arising under Directive 96/9/EC of the
118      European Parliament and of the Council of 11 March 1996 on the legal
119      protection of databases, and under any national implementation
120      thereof, including any amended or successor version of such
121      directive); and
122 vii. other similar, equivalent or corresponding rights throughout the
123      world based on applicable law or treaty, and any national
124      implementations thereof.
125 
126 2. Waiver. To the greatest extent permitted by, but not in contravention
127 of, applicable law, Affirmer hereby overtly, fully, permanently,
128 irrevocably and unconditionally waives, abandons, and surrenders all of
129 Affirmer's Copyright and Related Rights and associated claims and causes
130 of action, whether now known or unknown (including existing as well as
131 future claims and causes of action), in the Work (i) in all territories
132 worldwide, (ii) for the maximum duration provided by applicable law or
133 treaty (including future time extensions), (iii) in any current or future
134 medium and for any number of copies, and (iv) for any purpose whatsoever,
135 including without limitation commercial, advertising or promotional
136 purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
137 member of the public at large and to the detriment of Affirmer's heirs and
138 successors, fully intending that such Waiver shall not be subject to
139 revocation, rescission, cancellation, termination, or any other legal or
140 equitable action to disrupt the quiet enjoyment of the Work by the public
141 as contemplated by Affirmer's express Statement of Purpose.
142 
143 3. Public License Fallback. Should any part of the Waiver for any reason
144 be judged legally invalid or ineffective under applicable law, then the
145 Waiver shall be preserved to the maximum extent permitted taking into
146 account Affirmer's express Statement of Purpose. In addition, to the
147 extent the Waiver is so judged Affirmer hereby grants to each affected
148 person a royalty-free, non transferable, non sublicensable, non exclusive,
149 irrevocable and unconditional license to exercise Affirmer's Copyright and
150 Related Rights in the Work (i) in all territories worldwide, (ii) for the
151 maximum duration provided by applicable law or treaty (including future
152 time extensions), (iii) in any current or future medium and for any number
153 of copies, and (iv) for any purpose whatsoever, including without
154 limitation commercial, advertising or promotional purposes (the
155 "License"). The License shall be deemed effective as of the date CC0 was
156 applied by Affirmer to the Work. Should any part of the License for any
157 reason be judged legally invalid or ineffective under applicable law, such
158 partial invalidity or ineffectiveness shall not invalidate the remainder
159 of the License, and in such case Affirmer hereby affirms that he or she
160 will not (i) exercise any of his or her remaining Copyright and Related
161 Rights in the Work or (ii) assert any associated claims and causes of
162 action with respect to the Work, in either case contrary to Affirmer's
163 express Statement of Purpose.
164 
165 4. Limitations and Disclaimers.
166 
167  a. No trademark or patent rights held by Affirmer are waived, abandoned,
168     surrendered, licensed or otherwise affected by this document.
169  b. Affirmer offers the Work as-is and makes no representations or
170     warranties of any kind concerning the Work, express, implied,
171     statutory or otherwise, including without limitation warranties of
172     title, merchantability, fitness for a particular purpose, non
173     infringement, or the absence of latent or other defects, accuracy, or
174     the present or absence of errors, whether or not discoverable, all to
175     the greatest extent permissible under applicable law.
176  c. Affirmer disclaims responsibility for clearing rights of other persons
177     that may apply to the Work or any use thereof, including without
178     limitation any person's Copyright and Related Rights in the Work.
179     Further, Affirmer disclaims responsibility for obtaining any necessary
180     consents, permissions or other rights required for any use of the
181     Work.
182  d. Affirmer understands and acknowledges that Creative Commons is not a
183     party to this document and has no duty or obligation with respect to
184     this CC0 or use of the Work.`;
185 
186 immutable string PAGE_COFE = q"SECRET
187 
188       ) ) )
189      ( ( (
190     .......
191    _|     |
192   / |     |
193   \_|     |
194     `-----'
195 SECRET";
196 
197 immutable string STDIN_NAME = "-";
198 
199 immutable string FILE_MODE_TEXT = "r";
200 immutable string FILE_MODE_BIN = "rb";
201 
202 immutable string OPT_FILE       = "f|file";
203 immutable string OPT_MMFILE     = "m|mmfile";
204 immutable string OPT_ARG        = "a|arg";
205 immutable string OPT_CHECK      = "c|check";
206 immutable string OPT_TEXT       = "t|text";
207 immutable string OPT_BINARY     = "b|binary";
208 immutable string OPT_BUFFERSIZE = "B|buffersize";
209 immutable string OPT_GNU        = "gnu";
210 immutable string OPT_TAG        = "tag";
211 immutable string OPT_SRI        = "sri";
212 immutable string OPT_PLAIN      = "plain";
213 immutable string OPT_FOLLOW     = "follow";
214 immutable string OPT_NOFOLLOW   = "nofollow";
215 immutable string OPT_DEPTH      = "r|depth";
216 immutable string OPT_SHALLOW    = "shallow";
217 immutable string OPT_BREATH     = "breath";
218 immutable string OPT_KEY        = "key";
219 //immutable string OPT_KEYFILE	= "keyfile";
220 //immutable string OPT_KEYBIN	= "keyhex";
221 immutable string OPT_SEED       = "seed";
222 immutable string OPT_VER        = "ver";
223 immutable string OPT_VERSION    = "version";
224 immutable string OPT_LICENSE    = "license";
225 immutable string OPT_COFE       = "cofe";
226 
227 struct Settings
228 {
229     Ddh hasher;
230     HashType type = InvalidHash;
231     ubyte[] rawHash;
232     size_t bufferSize = DEFAULT_READ_SIZE;
233     SpanMode spanMode;
234     TagType tag;
235     string fileMode = FILE_MODE_BIN;
236     string against; /// Hash to check against (-a/--against)
237     ubyte[] key; /// Key for BLAKE2
238     uint seed; /// Seed for Murmurhash3
239 
240     int function(const(char)[]) hash = &hashFile;
241     // entry processor (file, text, list)
242     int function(const(char)[]) process = &processFile;
243 
244     bool follow = true;
245     bool modeStdin;
246     bool autocheck;
247 }
248 
249 __gshared Settings settings;
250 
251 version (Trace) void trace(string func = __FUNCTION__, A...)(string fmt, A args)
252 {
253     write("TRACE:", func, ": ");
254     writefln(fmt, args);
255 }
256 
257 void logWarn(string func = __FUNCTION__, A...)(string fmt, A args)
258 {
259     stderr.write("warning: ");
260     debug stderr.write("[", func, "] ");
261     stderr.writefln(fmt, args);
262 }
263 
264 void logWarn(Exception ex)
265 {
266     stderr.writefln("warning: %s", ex.msg);
267 }
268 
269 void logError(string func = __FUNCTION__, A...)(int code, string fmt, A args)
270 {
271     stderr.writef("error: (code %d) ", code);
272     debug stderr.write("[", func, "] ");
273     stderr.writefln(fmt, args);
274     exit(code);
275 }
276 
277 void logError(int code, Exception ex)
278 {
279     stderr.writef("error: (code %d) ", code);
280     debug stderr.writeln(ex);
281     else stderr.writeln(ex.msg);
282     exit(code);
283 }
284 
285 void printResult(string fmt = "%s")(in char[] file)
286 {
287     enum fmtgnu = fmt ~ "  %s";
288     enum fmtbsd = "%s(" ~ fmt ~ ")= %s";
289 
290     final switch (settings.tag) with (TagType)
291     {
292     case gnu:
293         writefln(fmtgnu, settings.hasher.toHex, file);
294         break;
295     case bsd:
296         writefln(fmtbsd, settings.hasher.tagName(), file, settings.hasher.toHex);
297         break;
298     case sri:
299         writeln(settings.hasher.aliasName(), '-', settings.hasher.toBase64);
300         break;
301     case plain:
302         writeln(settings.hasher.toHex);
303         break;
304     }
305 }
306 
307 void printStatus(in char[] file, bool match)
308 {
309     if (match)
310         writeln(file, ": OK");
311     else
312         stderr.writeln(file, ": FAILED");
313 }
314 
315 // String to binary size
316 int strtobin(ulong* size, string input)
317 {
318     enum
319     {
320         K = 1024,
321         M = K * 1024,
322         G = M * 1024,
323         T = G * 1024,
324     }
325 
326     float f = void;
327     char c = void;
328     try
329     {
330         if (input.formattedRead!"%f%c"(f, c) != 2)
331             return 1;
332     }
333     catch (Exception ex)
334     {
335         return 2;
336     }
337 
338     if (f <= 0.0f)
339         return 3;
340 
341     ulong u = cast(ulong) f;
342     switch (c)
343     {
344     case 'T', 't': u *= T; break;
345     case 'G', 'g': u *= G; break;
346     case 'M', 'm': u *= M; break;
347     case 'K', 'k': u *= K; break;
348     case 'B', 'b': break;
349     default: return 4;
350     }
351 
352     enum LIMIT = 2 * G; /// Buffer read limit
353     if (u > LIMIT)
354         return 5;
355 
356     *size = u;
357     return 0;
358 }
359 /// 
360 unittest
361 {
362     ulong s = void;
363     assert(strtobin(&s, "1K") == 0);
364     assert(s == 1024);
365     assert(strtobin(&s, "1.1K") == 0);
366     assert(s == 1024 + 102); // 102.4
367 }
368 
369 // unformat any number
370 uint unformat(string input)
371 {
372     //import core.stdc.stdio : sscanf;
373     import std.string : toStringz;
374 
375     int n = void;
376     sscanf(input.toStringz, "%i", &n);
377     return cast(uint) n;
378 }
379 
380 bool compareHash(const(char)[] h1, const(char)[] h2)
381 {
382     import std.digest : secureEqual;
383     import std.uni : asLowerCase;
384 
385     return secureEqual(h1.asLowerCase, h2.asLowerCase);
386 }
387 
388 int hashFile(const(char)[] path)
389 {
390     version (Trace) trace("path=%s", path);
391 
392     try
393     {
394         File f; // Must be init
395         // BUG: Using opAssign with LDC2 crashes at runtime
396         f.open(cast(string)path, settings.fileMode);
397         
398         if (f.size())
399         {
400             int e = hashFile(f);
401             if (e)
402                 return e;
403         }
404         else // Nothing to process, finish digest
405         {
406             settings.rawHash = settings.hasher.finish();
407             settings.hasher.reset();
408         }
409         
410         f.close();
411         return 0;
412     }
413     catch (Exception ex)
414     {
415         logWarn(ex);
416         return 1;
417     }
418 }
419 
420 int hashFile(ref File file)
421 {
422     try
423     {
424         foreach (ubyte[] chunk; file.byChunk(settings.bufferSize))
425             settings.hasher.put(chunk);
426 
427         settings.rawHash = settings.hasher.finish();
428         settings.hasher.reset();
429         return 0;
430     }
431     catch (Exception ex)
432     {
433         logWarn(ex);
434         return 1;
435     }
436 }
437 
438 int hashMmfile(const(char)[] path)
439 {
440     import std.range : chunks;
441     import std.mmfile : MmFile;
442     import std.file : getSize;
443 
444     version (Trace) trace("path=%s", path);
445 
446     try
447     {
448         ulong size = getSize(path);
449         
450         if (size)
451         {
452             scope mmfile = new MmFile(cast(string)path);
453             
454             foreach (chunk; chunks(cast(ubyte[]) mmfile[], settings.bufferSize))
455             {
456                 settings.hasher.put(chunk);
457             }
458         }
459         
460         settings.rawHash = settings.hasher.finish();
461         settings.hasher.reset();
462         return 0;
463     }
464     catch (Exception ex)
465     {
466         logWarn(ex);
467         return 1;
468     }
469 }
470 
471 int hashStdin(string)
472 {
473     version (Trace) trace("stdin");
474     return hashFile(stdin);
475 }
476 
477 int hashText(const(char)[] text)
478 {
479     version (Trace) trace("text='%s'", text);
480 
481     try
482     {
483         settings.hasher.put(cast(ubyte[]) text);
484         settings.rawHash = settings.hasher.finish();
485         settings.hasher.reset();
486         return 0;
487     }
488     catch (Exception ex)
489     {
490         logError(9, "Could not hash text: %s", ex.msg);
491         return 0;
492     }
493 }
494 
495 int processFile(const(char)[] path)
496 {
497     version (Trace) trace("path=%s", path);
498 
499     uint count;
500     string dir  = cast(string)dirName(path); // "." if anything
501     string name = cast(string)baseName(path); // Glob patterns are kept
502     const bool same = dir == "."; // same directory name from dirName
503     foreach (DirEntry entry; dirEntries(dir, name, settings.spanMode, settings.follow))
504     {
505         // Because entry will have "./" prefixed to it
506         string file = same ? entry.name[2 .. $] : entry.name;
507         ++count;
508         if (entry.isDir)
509         {
510             logWarn("'%s': Is a directory", file);
511             continue;
512         }
513 
514         if (settings.hash(file))
515         {
516             continue;
517         }
518 
519         if (settings.against)
520         {
521             bool succ = void;
522             if (settings.tag == TagType.sri)
523             {
524                 const(char)[] type = void, hash = void;
525                 if (readSRILine(settings.against, type, hash))
526                     logError(20, "Could not unformat SRI tag");
527                 
528                 settings.hasher.toBase64;
529                 succ = compareHash(settings.hasher.toBase64, hash);
530             }
531             else
532             {
533                 succ = compareHash(settings.hasher.toHex, settings.against);
534             }
535             printStatus(file, succ);
536             if (succ == false)
537                 return 2;
538         }
539         else
540             printResult(file);
541     }
542     
543     if (count == 0)
544         logError(6, "'%s': No such file", name);
545     
546     return 0;
547 }
548 
549 int processStdin()
550 {
551     version (Trace) trace("stdin");
552     int e = hashStdin(STDIN_NAME);
553     if (e == 0)
554         printResult(STDIN_NAME);
555     return e;
556 }
557 
558 int processText(const(char)[] text)
559 {
560     version (Trace) trace("text='%s'", text);
561     int e = hashText(text);
562     if (e == 0)
563         printResult!`"%s"`(text);
564     return e;
565 }
566 
567 int processList(const(char)[] listPath)
568 {
569     import std.file : readText;
570     import std.string : lineSplitter;
571 
572     version (Trace) trace("list=%s", listPath);
573 
574     uint currentLine, statMismatch, statErrors, statsTotal;
575     
576     if (settings.autocheck)
577     {
578         settings.type = guessHash(listPath);
579         if (settings.type == InvalidHash)
580             logError(5, "Could not determine hash type");
581     }
582 
583     try
584     {
585         string text = readText(listPath);
586 
587         if (text.length == 0)
588             logError(10, "%s: Empty", listPath);
589 
590         const(char)[] file = void, expected = void, type = void, lastType;
591         foreach (string line; lineSplitter(text)) // doesn't allocate
592         {
593             ++currentLine;
594 
595             if (line.length == 0) // empty
596                 continue;
597             if (line[0] == '#') // comment
598                 continue;
599 
600             TAGTYPE: final switch (settings.tag) with (TagType)
601             {
602             case gnu:
603                 if (readGNULine(line, expected, file))
604                 {
605                     ++statErrors;
606                     logWarn("Could not read GNU tag at line %u", currentLine);
607                 }
608                 
609                 if (file[0] == '*')
610                     file = file[1..$];
611                 break;
612             case bsd:
613                 if (readBSDLine(line, type, file, expected))
614                 {
615                     ++statErrors;
616                     logWarn("Could not read BSD tag at line %u", currentLine);
617                     continue;
618                 }
619 
620                 if (type == lastType)
621                     break;
622 
623                 // Find new hash type from tag name
624                 lastType = type;
625                 foreach (HashInfo info; hashInfo)
626                 {
627                     if (type == info.tag)
628                     {
629                         settings.hasher.initiate(info.type);
630                         break TAGTYPE;
631                     }
632                     if (type == info.tag2)
633                     {
634                         settings.hasher.initiate(info.type);
635                         break TAGTYPE;
636                     }
637                 }
638 
639                 logWarn("Unknown '%s' tag at line %u", type, currentLine);
640                 continue;
641             case sri:
642                 logError(11, "SRI hash format is not supported in file checks");
643                 break;
644             case plain:
645                 logError(11, "Plain hash format is not supported in file checks");
646                 break;
647             }
648             
649             ++statsTotal;
650 
651             if (settings.hash(file))
652             {
653                 ++statErrors;
654                 continue;
655             }
656 
657             const(char)[] result = settings.hasher.toHex;
658 
659             version (Trace) trace("r1=%s r2=%s", result, expected);
660 
661             if (compareHash(result, expected) == false)
662             {
663                 ++statMismatch;
664                 printStatus(file, false);
665                 continue;
666             }
667 
668             printStatus(file, true);
669         }
670     }
671     catch (Exception ex)
672     {
673         logError(12, ex);
674     }
675 
676     writefln("%u total: %u mismatches, %u not read",
677         statsTotal, statMismatch, statErrors);
678 
679     return 0;
680 }
681 
682 //TODO: Consider making a foreach-compatible function for this
683 //      popFront returning T[2] (or tuple)
684 /// Compare all file entries against each other.
685 /// O: O(n * log(n)) (according to friend)
686 /// Params: entries: List of files
687 /// Returns: Error code.
688 int processCompare(string[] entries)
689 {
690     const size_t size = entries.length;
691 
692     if (size < 2)
693         logError(15, "Comparison needs 2 or more files");
694 
695     //TODO: Consider an associated array
696     //      Would remove duplicates, but at the same time, this removes
697     //      all user-supplied positions and may confuse people if unordered.
698     string[] hashes = new string[size];
699 
700     for (size_t index; index < size; ++index)
701     {
702         int e = hashFile(entries[index]);
703         if (e)
704             return e;
705 
706         hashes[index] = settings.hasher.toHex.idup;
707     }
708 
709     uint mismatch; /// Number of mismatching files
710 
711     for (size_t distance = 1; distance < size; ++distance)
712     {
713         for (size_t index; index < size; ++index)
714         {
715             size_t index2 = index + distance;
716 
717             if (index2 >= size)
718                 break;
719 
720             if (compareHash(hashes[index], hashes[index2]))
721                 continue;
722 
723             ++mismatch;
724 
725             string entry1 = entries[index];
726             string entry2 = entries[index2];
727 
728             writeln("Files '", entry1, "' and '", entry2, "' are different");
729         }
730     }
731 
732     if (mismatch == 0)
733         writefln("All files identical");
734 
735     return 0;
736 }
737 
738 void printMeta(string baseName, string name, string tag, string tag2)
739 {
740     writefln("%-18s  %-18s  %-18s  %s", baseName, name, tag, tag2);
741 }
742 
743 // special settings that getopts cannot simply set directly
744 void option(string arg)
745 {
746     version (Trace) trace(arg);
747 
748     with (settings) final switch (arg)
749     {
750     // input modes
751     case OPT_ARG:   process = &processText; return;
752     case OPT_CHECK: process = &processList; return;
753     // file input mode
754     case OPT_FILE:      hash = &hashFile; return;
755     case OPT_MMFILE:    hash = &hashMmfile; return;
756     case OPT_TEXT:      fileMode = FILE_MODE_TEXT; return;
757     case OPT_BINARY:    fileMode = FILE_MODE_BIN; return;
758     // hash style
759     case OPT_TAG:   tag = TagType.bsd; return;
760     case OPT_SRI:   tag = TagType.sri; return;
761     case OPT_GNU:   tag = TagType.gnu; return;
762     case OPT_PLAIN: tag = TagType.plain; return;
763     // globber: symlink
764     case OPT_NOFOLLOW:  follow = false; return;
765     case OPT_FOLLOW:    follow = true; return;
766     // globber: directory
767     case OPT_DEPTH:     spanMode = SpanMode.depth; return;
768     case OPT_SHALLOW:   spanMode = SpanMode.shallow; return;
769     case OPT_BREATH:    spanMode = SpanMode.breadth; return;
770     // pages
771     case OPT_VER:       arg = GIT_DESCRIPTION; break;
772     case OPT_VERSION:   arg = PAGE_VERSION; break;
773     case OPT_LICENSE:   arg = PAGE_LICENSE; break;
774     case OPT_COFE:      arg = PAGE_COFE; break;
775     }
776     writeln(arg);
777     exit(0);
778 }
779 
780 void option2(string arg, string val)
781 {
782     with (settings) final switch (arg)
783     {
784     case OPT_BUFFERSIZE:
785         ulong v = void;
786         if (strtobin(&v, val))
787             throw new GetOptException("Couldn't unformat buffer size");
788         
789         if (v >= size_t.max)
790             throw new GetOptException("Buffer size overflows");
791         
792         bufferSize = cast(size_t) v;
793         return;
794     // keying
795     case OPT_KEY:
796         try
797         {
798             settings.key = cast(ubyte[]) readAll(val);
799         }
800         catch (Exception ex)
801         {
802             throw new GetOptException(ex.msg);
803         }
804         return;
805     // seeding
806     case OPT_SEED:
807         settings.seed = unformat(val);
808         return;
809     }
810 }
811 
812 int cliAutoCheck(string[] entries)
813 {
814     foreach (string entry; entries)
815     {
816         version (Trace) trace("entry=%s", entry);
817         
818         settings.type = guessHash(entry);
819         if (settings.type == InvalidHash)
820             logError(7, "Could not determine hash type for: %s", entry);
821         
822         if (settings.hasher.initiate(settings.type))
823         {
824             logError(3, "Couldn't initiate hash module");
825         }
826         
827         processList(entry);
828     }
829     
830     return 0;
831 }
832 
833 void cliHashes()
834 {
835     static immutable string sep = "-----------";
836     printMeta("Alias", "Name", "Tag", "Tag2");
837     printMeta(sep, sep, sep, sep);
838     foreach (info; hashInfo)
839         printMeta(info.alias_, info.fullName, info.tag, info.tag2);
840     exit(0);
841 }
842 
843 void cliHash(string opt)
844 {
845     final switch (opt)
846     {
847     case crc32:     settings.type = HashType.CRC32; return;
848     case crc64iso:  settings.type = HashType.CRC64ISO; return;
849     case crc64ecma: settings.type = HashType.CRC64ECMA; return;
850     case murmur3a:  settings.type = HashType.MurmurHash3_32; return;
851     case murmur3c:  settings.type = HashType.MurmurHash3_128_32; return;
852     case murmur3f:  settings.type = HashType.MurmurHash3_128_64; return;
853     case md5:       settings.type = HashType.MD5; return;
854     case ripemd160: settings.type = HashType.RIPEMD160; return;
855     case sha1:      settings.type = HashType.SHA1; return;
856     case sha224:    settings.type = HashType.SHA224; return;
857     case sha256:    settings.type = HashType.SHA256; return;
858     case sha384:    settings.type = HashType.SHA384; return;
859     case sha512:    settings.type = HashType.SHA512; return;
860     case sha3_224:  settings.type = HashType.SHA3_224; return;
861     case sha3_256:  settings.type = HashType.SHA3_256; return;
862     case sha3_384:  settings.type = HashType.SHA3_384; return;
863     case sha3_512:  settings.type = HashType.SHA3_512; return;
864     case shake128:  settings.type = HashType.SHAKE128; return;
865     case shake256:  settings.type = HashType.SHAKE256; return;
866     case blake2b512:    settings.type = HashType.BLAKE2b512; return;
867     case blake2s256:    settings.type = HashType.BLAKE2s256; return;
868     }
869 }
870 
871 int main(string[] args)
872 {
873     bool compare;
874 
875     GetoptResult res = void;
876     try
877     {
878         //TODO: Array of bool to select multiple hashes?
879         //TODO: Include argument (string,string) for doing batches with X hash?
880         res = getopt(args, config.caseSensitive,
881             OPT_COFE,       "", &option,
882             crc32,          "", &cliHash,
883             crc64iso,       "", &cliHash,
884             crc64ecma,      "", &cliHash,
885             murmur3a,       "", &cliHash,
886             murmur3c,       "", &cliHash,
887             murmur3f,       "", &cliHash,
888             md5,            "", &cliHash,
889             ripemd160,      "", &cliHash,
890             sha1,           "", &cliHash,
891             sha224,         "", &cliHash,
892             sha256,         "", &cliHash,
893             sha384,         "", &cliHash,
894             sha512,         "", &cliHash,
895             sha3_224,       "", &cliHash,
896             sha3_256,       "", &cliHash,
897             sha3_384,       "", &cliHash,
898             sha3_512,       "", &cliHash,
899             shake128,       "", &cliHash,
900             shake256,       "", &cliHash,
901             blake2b512,     "", &cliHash,
902             blake2s256,     "", &cliHash,
903             OPT_FILE,       "Input mode: Regular file (default).", &option,
904             OPT_BINARY,     "File: Set binary mode (default).", &option,
905             OPT_TEXT,       "File: Set text mode.", &option,
906             OPT_MMFILE,     "Input mode: Memory-map file.", &option,
907             OPT_ARG,        "Input mode: Command-line argument is text data (UTF-8).", &option,
908             "stdin",        "Input mode: Standard input (stdin)", &settings.modeStdin,
909             OPT_CHECK,      "Check hashes list in this file.", &option,
910             "autocheck",    "Automatically determine hash type and process list.", &settings.autocheck,
911             "C|compare",    "Compares all file entries.", &compare,
912             "A|against",    "Compare files against hash.", &settings.against,
913             "hashes",       "List supported hashes.", &cliHashes,
914             OPT_BUFFERSIZE, "Set buffer size, affects file/mmfile/stdin (default=4K).", &option2,
915             OPT_SHALLOW,    "Depth: Same directory (default).", &option,
916             OPT_DEPTH,      "Depth: Deepest directories first.", &option,
917             OPT_BREATH,     "Depth: Sub directories first.", &option,
918             OPT_FOLLOW,     "Links: Follow symbolic links (default).", &option,
919             OPT_NOFOLLOW,   "Links: Do not follow symbolic links.", &option,
920             OPT_TAG,        "Create or read BSD-style hashes.", &option,
921             OPT_SRI,        "Create or read SRI-style hashes.", &option,
922             OPT_PLAIN,      "Create or read plain hashes.", &option,
923             OPT_KEY,        "Binary key file for BLAKE2 hashes.", &option2,
924             //"keyhex",       "Hex text key file for supported hash.",  &option2,
925             //"keystr",       "Hex text argument for supported hash.",  &option2,
926             OPT_SEED,       "Seed literal argument for Murmurhash3 hashes.", &option2,
927             OPT_VERSION,    "Show version page and quit.", &option,
928             OPT_VER,        "Show version and quit.", &option,
929             OPT_LICENSE,    "Show license page and quit.", &option,
930         );
931     }
932     catch (Exception ex)
933     {
934         logError(1, ex);
935     }
936 
937     if (res.helpWanted)
938     {
939         writeln(PAGE_HELP);
940         foreach (Option opt; res.options[HashCount + 1..$])
941         {
942             with (opt)
943                 if (optShort)
944                     writefln("%s, %-12s  %s", optShort, optLong, help);
945                 else
946                     writefln("    %-12s  %s", optLong, help);
947         }
948         writeln("\nThis program has actual coffee-making abilities.");
949         return 0;
950     }
951     
952     if (settings.autocheck)
953     {
954         return cliAutoCheck(args[1..$]);
955     }
956 
957     if (settings.type == InvalidHash)
958     {
959         logError(2, "No hashes selected");
960     }
961 
962     if (settings.hasher.initiate(settings.type))
963     {
964         logError(3, "Couldn't initiate hash module");
965     }
966 
967     if (settings.key != settings.key.init)
968     {
969         try
970         {
971             settings.hasher.key(settings.key);
972         }
973         catch (Exception ex)
974         {
975             logError(4, "Failed to set key: %s", ex.msg);
976         }
977     }
978 
979     if (settings.seed)
980     {
981         try
982         {
983             settings.hasher.seed(settings.seed);
984         }
985         catch (Exception ex)
986         {
987             logError(5, "Failed to set seed: %s", ex.msg);
988         }
989     }
990 
991     if (settings.modeStdin)
992     {
993         return processStdin;
994     }
995 
996     string[] entries = args[1 .. $];
997 
998     if (compare)
999         return processCompare(entries);
1000 
1001     if (entries.length == 0)
1002         return processStdin;
1003 
1004     foreach (string entry; entries)
1005     {
1006         settings.process(entry);
1007     }
1008 
1009     return 0;
1010 }