1 module config.util; 2 3 import std.range.primitives; 4 import std.traits : isSomeString, isSomeChar, Unqual; 5 6 7 /// down cast of a reference to a child class reference 8 /// runtime check is disabled in release build 9 U unsafeCast(U, T)(T obj) 10 if ((is(T==class) || is(T==interface)) && 11 (is(U==class) || is(U==interface)) && 12 is(U : T)) 13 in { 14 assert(obj); 15 } 16 body { 17 debug { 18 auto uObj = cast(U)obj; 19 assert(uObj, "unsafeCast from "~T.stringof~" to "~U.stringof~" failed"); 20 return uObj; 21 } 22 else { 23 static if (is(T == interface) && is(U == class)) { 24 return cast(U)(cast(void*)(cast(Object)obj)); 25 } 26 else { 27 return cast(U)(cast(void*)obj); 28 } 29 } 30 } 31 32 /// ditto 33 const(U) unsafeCast(U, T)(const(T) obj) 34 if ((is(T==class) || is(T==interface)) && 35 (is(U==class) || is(U==interface)) && 36 is(U : T)) 37 in { 38 assert(obj); 39 } 40 body { 41 debug { 42 auto uObj = cast(const(U))obj; 43 assert(uObj, "unsafeCast from "~T.stringof~" to "~U.stringof~" failed"); 44 return uObj; 45 } 46 else { 47 static if (is(T == interface) && is(U == class)) { 48 return cast(const(U))(cast(const(void*))(cast(const(Object))obj)); 49 } 50 else { 51 return cast(const(U))(cast(const(void*))obj); 52 } 53 } 54 } 55 56 57 auto findSplitAmong(alias pred="a == b", R1, R2)(R1 seq, R2 choices) 58 if (isForwardRange!R1 && isForwardRange!R2) 59 { 60 static struct Result(S1, S2, S3) 61 if (isForwardRange!S1 && isForwardRange!S2 && isForwardRange!S3) 62 { 63 import std.typecons : Tuple; 64 65 this(S1 pre, S2 separator, S3 post) 66 { 67 asTuple = typeof(asTuple)(pre, separator, post); 68 } 69 70 bool opCast(T : bool)() 71 { 72 return !asTuple[1].empty; 73 } 74 75 alias asTuple this; 76 Tuple!(S1, S2, S3) asTuple; 77 } 78 79 auto makeResult(S1, S2, S3)(S1 s1, S2 s2, S3 s3) { 80 return Result!(S1, S2, S3)(s1, s2, s3); 81 } 82 83 static if (isSomeString!R1 && isSomeString!R2 || isRandomAccessRange!R1 && hasLength!R2) 84 { 85 import std.algorithm : findAmong; 86 87 immutable balance = findAmong!pred(seq, choices); 88 immutable pos1 = seq.length - balance.length; 89 immutable pos2 = balance.empty ? pos1 : pos1 + 1; 90 return makeResult(seq[0 .. pos1], seq[pos1 .. pos2], seq[pos2 .. $]); 91 } 92 else 93 { 94 import std.range : takeExactly, takeOne; 95 import std.functional : binaryFun; 96 97 auto original = seq.save; 98 size_t pos; 99 100 seqLoop: while (!seq.empty) { 101 auto sf = seq.front; 102 auto c = choices.save; 103 while(!c.empty) { 104 if (binaryFun!pred(sf, c.front)) { 105 break seqLoop; 106 } 107 c.popFront(); 108 } 109 seq.popFront(); 110 ++pos; 111 } 112 113 auto right = seq.save; 114 if (!right.empty) right.popFront(); 115 return makeResult ( takeExactly(original, pos), takeOne(seq), right ); 116 } 117 } 118 119 120 unittest { 121 { 122 auto name = "name1.name2"; 123 124 auto split1 = name.findSplitAmong(":./"); 125 assert(split1[0] == "name1"); 126 assert(split1[1] == "."); 127 assert(split1[2] == "name2"); 128 129 auto split2 = name.findSplitAmong(":-/"); 130 assert(split2[0] == "name1.name2"); 131 assert(split2[1] == ""); 132 assert(split2[2] == ""); 133 } 134 { 135 import std.algorithm : equal; 136 137 static struct StrFwdRange { 138 string s; 139 @property auto empty() const { return s.length == 0; } 140 @property auto front() const { return s[0]; } 141 void popFront() { s = s[1 .. $]; } 142 @property auto save() const { return StrFwdRange(s); } 143 } 144 static assert(isForwardRange!StrFwdRange); 145 146 auto name = StrFwdRange("name1.name2"); 147 148 auto split1 = name.findSplitAmong(StrFwdRange(":./")); 149 assert(equal(split1[0], "name1")); 150 assert(equal(split1[1], ".")); 151 assert(equal(split1[2], "name2")); 152 153 auto split2 = name.findSplitAmong(":-/"); 154 assert(equal(split2[0], "name1.name2")); 155 assert(equal(split2[1], "")); 156 assert(equal(split2[2], "")); 157 } 158 } 159 160 161 /// strip libconfig comments on a char by char basis 162 auto stripComments(R)(R input) 163 if (isInputRange!R && isSomeChar!(ElementType!R)) 164 { 165 struct Result 166 { 167 R _input; 168 ElementType!R[] _lookAhead; 169 170 this(R input) 171 { 172 _input = input; 173 check(); 174 } 175 176 private this (R input, ElementType!R[] lookAhead) 177 { 178 _input = input; 179 _lookAhead = lookAhead; 180 } 181 182 @property auto save() 183 { 184 return Result(_input.save, _lookAhead.dup); 185 } 186 187 @property bool empty() 188 { 189 return _lookAhead.empty && _input.empty; 190 } 191 192 @property auto ref front() 193 { 194 if (_lookAhead.length) return _lookAhead[0]; 195 else return _input.front; 196 } 197 198 void popFront() 199 { 200 if (_lookAhead.empty) 201 { 202 _input.popFront(); 203 check(); 204 } 205 else _lookAhead = _lookAhead[1 .. $]; 206 } 207 208 private void check() 209 { 210 assert(_lookAhead.empty); 211 212 if (_input.empty) return; 213 else if (_input.front == '#') popUntilEol(); 214 else if (_input.front == '/') 215 { 216 immutable keep = _input.front; 217 _input.popFront(); 218 219 if (_input.empty) _lookAhead = [keep]; 220 else if (_input.front == '/') popUntilEol(); 221 else if (_input.front == '*') popUntilEndBlock(); 222 else _lookAhead = [keep]; 223 } 224 } 225 226 private void popUntilEol() 227 { 228 do { 229 _input.popFront(); 230 } 231 while (!_input.empty && !(_input.front == '\r' || _input.front == '\n')); 232 } 233 234 private void popUntilEndBlock() 235 { 236 while (1) { 237 do { 238 _input.popFront(); 239 if (_input.empty) { 240 throw new Exception("comment block not closed"); 241 } 242 } 243 while (_input.front != '*'); 244 245 _input.popFront(); 246 if (_input.empty) { 247 throw new Exception("comment block not closed"); 248 } 249 if (_input.front == '/') { 250 _input.popFront(); 251 break; 252 } 253 } 254 } 255 } 256 257 return Result(input); 258 } 259 260 261 262 /// strip libconfig comments on a line by line basis 263 auto stripComments(R)(R input) 264 if (isInputRange!R && isSomeString!(ElementType!R)) 265 { 266 alias StringT = ElementType!R; 267 alias CharT = Unqual!(typeof(StringT.init[0])); 268 269 270 struct Result 271 { 272 R _input; 273 StringT _buf; 274 bool _inBlock; 275 int count; 276 277 this (R input) 278 { 279 _input = input; 280 fetch(); 281 } 282 283 this (R input, StringT buf, bool inBlock) 284 { 285 _input = input; 286 _buf = buf; 287 _inBlock = inBlock; 288 } 289 290 static if (isForwardRange!R) 291 { 292 @property auto save() 293 { 294 return Result(_input.save, (_buf is null) ? null : _buf.dup, _inBlock); 295 } 296 } 297 298 @property bool empty() 299 { 300 return _buf is null; 301 } 302 303 @property auto front() 304 { 305 return _buf; 306 } 307 308 void popFront() 309 { 310 _buf = null; 311 if (!_input.empty) { 312 _input.popFront(); 313 fetch(); 314 } 315 } 316 317 private void fetch() 318 { 319 import std.algorithm : find, canFind; 320 321 if (_input.empty) return; 322 323 auto line = _input.front; 324 assert(!line.canFind("\n"), "pass stripComments on a range of lines"); 325 typeof(line) left; 326 typeof(line) right = line[]; 327 328 while (1) { 329 immutable len = line.length; 330 331 if (_inBlock) { 332 immutable end = right.find("*/"); 333 if (!end.empty) { 334 line = left ~ end[2 .. $]; 335 _inBlock = false; 336 } 337 else 338 { 339 line = left[]; 340 } 341 } 342 else { 343 immutable cmtHash = line.find("#"); 344 immutable cmtCpp = line.find("//"); 345 immutable cmtLine = cmtHash.length > cmtCpp.length ? cmtHash : cmtCpp; 346 immutable cmtBlock = line.find("/*"); 347 348 if (!cmtLine.empty || !cmtBlock.empty) 349 { 350 if (cmtBlock.length > cmtLine.length) 351 { 352 line = line[0 .. $-cmtBlock.length]; 353 right = cmtBlock[]; 354 _inBlock = true; 355 } 356 else 357 { 358 line = line[0 .. $-cmtLine.length]; 359 } 360 } 361 left = line[]; 362 } 363 364 if (len == line.length) break; 365 } 366 367 _buf ~= line; 368 369 if (_inBlock) { 370 _input.popFront(); 371 if (_input.empty) throw new Exception("comment block not closed"); 372 else fetch(); 373 } 374 } 375 } 376 377 return Result(input); 378 } 379 380 381 382 /// 383 unittest { 384 // couples of input and expected result 385 immutable text = [[ 386 "some text\n" 387 "more text", 388 389 "some text\n" 390 "more text" 391 ], 392 [ 393 "// start with comments\n" 394 "some text // other comment\n" 395 "more text", 396 397 "\n" 398 "some text \n" 399 "more text" 400 ], 401 [ 402 "/ almost a comment but text\n" 403 "some text // an actual comment\n" 404 "more text", 405 406 "/ almost a comment but text\n" 407 "some text \n" 408 "more text" 409 ], 410 [ 411 "some text // comment\n" 412 "more text", 413 414 "some text \n" 415 "more text" 416 ], 417 [ 418 "some text # comment\n" 419 "more text", 420 421 "some text \n" 422 "more text" 423 ], 424 [ 425 "some text /* inlined comment */ more text", 426 427 "some text more text" 428 ], 429 [ 430 "some text /* comment with // inlined\n" 431 "over 2 lines */ more text", 432 433 "some text more text" 434 ], 435 [ 436 "some text // comment with /* inlined\n" 437 "more text", 438 439 "some text \n" 440 "more text" 441 ], 442 [ 443 "some text # comment with */ inlined\n" 444 "more text", 445 446 "some text \n" 447 "more text" 448 ], 449 [ 450 "some text /* multiline comment\n" 451 "still in comment\n" 452 "again comment */ more text", 453 454 "some text more text" 455 ]]; 456 457 foreach (t; text) { 458 import std.algorithm : equal; 459 import std.format : format; 460 import std.conv : to; 461 462 string input = t[0]; 463 string result = t[0].stripComments().to!string; 464 string expected = t[1]; 465 466 assert(equal(expected, result), format( 467 "stripComments test failed.\ninput:\n%s\nresult:\n%s\nexpected:\n%s\n", 468 input, result, expected 469 )); 470 } 471 472 foreach (t; text) { 473 import std.algorithm : equal; 474 import std.string : lineSplitter; 475 import std.format : format; 476 477 string [] input; 478 string [] result; 479 string [] expected; 480 foreach (s; t[0].lineSplitter) input ~= s; 481 foreach (s; t[0].lineSplitter.stripComments) result ~= s; 482 foreach (s; t[1].lineSplitter) expected ~= s; 483 484 assert(equal(expected, result), format( 485 "lineSplitter.stripComments test failed.\ninput:\n%s\nresult:\n%s\nexpected:\n%s\n", 486 input, result, expected 487 )); 488 } 489 } 490 491 /// Transforms the given input range by replacing lines that have @include directive 492 /// with the content of the included file. Filenames are looked for in the list 493 /// of directories includeDirs. 494 /// input must be a range of lines (as given e.g. by std.string.lineSplitter) 495 auto handleIncludeDirs(R)(R input, in string[] includeDirs=[]) 496 if (isInputRange!R && isSomeString!(ElementType!R)) 497 { 498 return HandleIncludeDirsResult!R(input, includeDirs); 499 } 500 501 private struct HandleIncludeDirsResult(R) 502 { 503 IncludeHandler!(ElementType!R) hdler; 504 505 this (R input, in string[] includeDirs) 506 { 507 hdler = makeIncludeHandler(input, includeDirs); 508 } 509 510 @property bool empty() 511 { 512 return hdler.empty; 513 } 514 @property auto front() 515 { 516 return hdler.front; 517 } 518 void popFront() 519 { 520 hdler.popFront(); 521 } 522 } 523 524 private auto makeIncludeHandler(R) (R input, in string[] includeDirs) 525 { 526 alias StringT = typeof(input.front); 527 return cast(IncludeHandler!StringT)(new IncludeHandlerImpl!R(input, includeDirs)); 528 } 529 530 private interface IncludeHandler(StringT) 531 { 532 @property bool empty(); 533 @property IncludeHandler!StringT save(); 534 @property StringT front(); 535 void popFront(); 536 } 537 538 private class IncludeHandlerImpl(R) : IncludeHandler!(ElementType!R) 539 { 540 alias StringT = ElementType!R; 541 542 R _input; 543 const(string[]) _includeDirs; 544 IncludeHandler!StringT _dir; 545 bool _fstCheck; 546 547 548 this (R input, in string[] includeDirs) 549 { 550 _input = input; 551 _includeDirs = includeDirs; 552 } 553 554 private this (R input, in string[] includeDirs, IncludeHandler!StringT dir, bool fstCheck) 555 { 556 _input = input; 557 _includeDirs = includeDirs; 558 _dir = dir; 559 _fstCheck = fstCheck; 560 } 561 562 @property IncludeHandler!StringT save() 563 { 564 static if (isForwardRange!R) 565 { 566 return new IncludeHandlerImpl!R(_input.save, _includeDirs, _dir ? _dir.save : null, _fstCheck); 567 } 568 else 569 { 570 assert(false, "not implementable with input of type "~R.stringof); 571 } 572 } 573 574 @property bool empty() 575 { 576 if (_dir) { 577 return _dir.empty && _input.empty; 578 } 579 else { 580 return _input.empty; 581 } 582 } 583 584 @property StringT front() 585 { 586 if (!_fstCheck) check(); 587 if (_dir && !_dir.empty) return _dir.front; 588 return _input.front; 589 } 590 591 void popFront() 592 { 593 if (_dir && !_dir.empty) 594 { 595 _dir.popFront(); 596 if (_dir.empty) { 597 _dir = null; 598 if (!_input.empty) 599 { 600 _input.popFront(); 601 check(); 602 } 603 } 604 } 605 else { 606 _input.popFront(); 607 check(); 608 } 609 } 610 611 private void check() 612 { 613 import std.algorithm : canFind; 614 import std.path : chainPath; 615 import std.file : exists, read; 616 import std.string : lineSplitter; 617 import std.regex : ctRegex, matchFirst; 618 import std.conv : to; 619 620 _fstCheck = true; 621 622 if (_input.empty) return; 623 auto line = _input.front; 624 assert(!line.canFind('\n'), "handleIncludeDirs must be passed a line range"); 625 626 auto re = ctRegex!("^\\s*@include\\s*\"(.*)\"\\s*$"); 627 auto m = matchFirst(line, re); 628 629 if(m) { 630 assert(m.length >= 2); 631 auto fname = m[1]; 632 633 assert(!_dir); 634 foreach(d; _includeDirs) 635 { 636 auto fpath = chainPath(d, fname).to!string; 637 if (exists(fpath)) { 638 // TODO: handle escaped chars in fname 639 auto content = cast(StringT)read(fpath); 640 _dir = makeIncludeHandler(content.lineSplitter, _includeDirs); 641 } 642 } 643 if(!_dir) { 644 throw new Exception("unresolved include directive: "~fname); 645 } 646 } 647 } 648 } 649 650 /// 651 unittest 652 { 653 import std.file : write, readText, remove; 654 655 scope(exit) { 656 remove("test.cfg"); 657 } 658 write("test.cfg", "name=12"); 659 660 // couples of input and expected result 661 auto text = [[ 662 "foo=10\nbar:30", 663 "foo=10\nbar:30" 664 ], 665 [ 666 "@include \"test.cfg\"", 667 "name=12" 668 ], 669 [ 670 "foo=10\n @include \"test.cfg\" \nbar:30", 671 "foo=10\nname=12\nbar:30" 672 ], 673 [ 674 "foo=10\n@include\"test.cfg\"\nbar:30", 675 "foo=10\nname=12\nbar:30" 676 ], 677 [ 678 "foo=10\n@include \"test.cfg\"\nbar:30", 679 "foo=10\nname=12\nbar:30" 680 ]]; 681 682 683 foreach (t; text) { 684 import std.algorithm : equal; 685 import std.string : lineSplitter; 686 import std.format : format; 687 688 string [] input; 689 string [] result; 690 string [] expected; 691 foreach (s; t[0].lineSplitter) input ~= s; 692 foreach (s; t[0].lineSplitter.handleIncludeDirs(["."])) result ~= s; 693 foreach (s; t[1].lineSplitter) expected ~= s; 694 695 assert(equal(expected, result), 696 format("lineSplitter.handleIncludeDirs test failed.\ninput:\n%s\nresult:\n%s\nexpected:\n%s\n", 697 input, result, expected)); 698 } 699 700 }