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 }