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 }