1 /++ 2 Helper functions needed to parse raw IRC event strings into 3 [dialect.defs.IRCEvent|IRCEvent]s. 4 5 Also things that don't belong anywhere else. 6 +/ 7 module dialect.common; 8 9 private: 10 11 import dialect.defs; 12 import dialect.parsing; 13 import lu.string : contains, nom; 14 15 public: 16 17 18 // typenumsOf 19 /++ 20 Returns the `typenums` mapping for a given 21 [dialect.defs.IRCServer.Daemon|IRCServer.Daemon]. 22 23 Example: 24 --- 25 IRCParser parser; 26 IRCServer.Daemon daemon = IRCServer.Daemon.unreal; 27 string daemonstring = "unreal"; 28 29 parser.typenums = getTypenums(daemon); 30 parser.client.daemon = daemon; 31 parser.client.daemonstring = daemonstring; 32 --- 33 34 Params: 35 daemon = The [dialect.defs.IRCServer.Daemon|IRCServer.Daemon] to get 36 the typenums for. 37 38 Returns: 39 A `typenums` array of [dialect.defs.IRCEvent|IRCEvent]s mapped to numerics. 40 +/ 41 auto typenumsOf(const IRCServer.Daemon daemon) pure @safe nothrow @nogc 42 { 43 import lu.meld : MeldingStrategy, meldInto; 44 45 /// https://upload.wikimedia.org/wikipedia/commons/d/d5/IRCd_software_implementations3.svg 46 47 IRCEvent.Type[1024] typenums = Typenums.base; 48 alias strategy = MeldingStrategy.aggressive; 49 50 with (IRCServer.Daemon) 51 final switch (daemon) 52 { 53 case unreal: 54 case mfvx: 55 Typenums.unreal[].meldInto!strategy(typenums); 56 break; 57 58 case solanum: 59 Typenums.solanum[].meldInto!strategy(typenums); 60 break; 61 62 case inspircd: 63 Typenums.inspIRCd[].meldInto!strategy(typenums); 64 break; 65 66 case bahamut: 67 Typenums.bahamut[].meldInto!strategy(typenums); 68 break; 69 70 case ratbox: 71 Typenums.ratBox[].meldInto!strategy(typenums); 72 break; 73 74 case u2: 75 // unknown! 76 break; 77 78 case rizon: 79 // Rizon is hybrid but has some own extras 80 Typenums.hybrid[].meldInto!strategy(typenums); 81 Typenums.rizon[].meldInto!strategy(typenums); 82 break; 83 84 case hybrid: 85 Typenums.hybrid[].meldInto!strategy(typenums); 86 break; 87 88 case ircu: 89 Typenums.ircu[].meldInto!strategy(typenums); 90 break; 91 92 case aircd: 93 Typenums.aircd[].meldInto!strategy(typenums); 94 break; 95 96 case rfc1459: 97 Typenums.rfc1459[].meldInto!strategy(typenums); 98 break; 99 100 case rfc2812: 101 Typenums.rfc2812[].meldInto!strategy(typenums); 102 break; 103 104 case snircd: 105 // snircd is based on ircu 106 Typenums.ircu[].meldInto!strategy(typenums); 107 Typenums.snircd[].meldInto!strategy(typenums); 108 break; 109 110 case nefarious: 111 // Both nefarious and nefarious2 are based on ircu 112 Typenums.ircu[].meldInto!strategy(typenums); 113 Typenums.nefarious[].meldInto!strategy(typenums); 114 break; 115 116 case rusnet: 117 Typenums.rusnet[].meldInto!strategy(typenums); 118 break; 119 120 case austhex: 121 Typenums.austHex[].meldInto!strategy(typenums); 122 break; 123 124 case ircnet: 125 Typenums.ircNet[].meldInto!strategy(typenums); 126 break; 127 128 case ptlink: 129 Typenums.ptlink[].meldInto!strategy(typenums); 130 break; 131 132 case ultimate: 133 Typenums.ultimate[].meldInto!strategy(typenums); 134 break; 135 136 case charybdis: 137 Typenums.charybdis[].meldInto!strategy(typenums); 138 break; 139 140 case ircdseven: 141 // Nei | freenode is based in charybdis which is based on ratbox iirc 142 Typenums.hybrid[].meldInto!strategy(typenums); 143 Typenums.ratBox[].meldInto!strategy(typenums); 144 Typenums.charybdis[].meldInto!strategy(typenums); 145 break; 146 147 case undernet: 148 Typenums.undernet[].meldInto!strategy(typenums); 149 break; 150 151 case anothernet: 152 //Typenums.anothernet[].meldInto!strategy(typenums); 153 break; 154 155 case sorircd: 156 Typenums.charybdis[].meldInto!strategy(typenums); 157 Typenums.sorircd[].meldInto!strategy(typenums); 158 break; 159 160 case bdqircd: 161 //Typenums.bdqIrcD[].meldInto!strategy(typenums); 162 break; 163 164 case chatircd: 165 //Typenums.chatIRCd[].meldInto!strategy(typenums); 166 break; 167 168 case irch: 169 //Typenums.irch[].meldInto!strategy(typenums); 170 break; 171 172 case ithildin: 173 //Typenums.ithildin[].meldInto!strategy(typenums); 174 break; 175 176 case twitch: 177 // do nothing, their events aren't numerical? 178 break; 179 180 case bsdunix: 181 // unsure. 182 break; 183 184 case unknown: 185 case unset: 186 // do nothing... 187 break; 188 } 189 190 return typenums; 191 } 192 193 194 // decodeIRCv3String 195 /++ 196 Decodes an IRCv3 tag string, replacing some characters. 197 198 IRCv3 tags need to be free of spaces, so by necessity they're encoded into 199 `\s`. Likewise; since tags are separated by semicolons, semicolons in tag 200 string are encoded into `\:`, and literal backslashes `\\`. 201 202 Example: 203 --- 204 string encoded = `This\sline\sis\sencoded\:\swith\s\\s`; 205 string decoded = decodeIRCv3String(encoded); 206 assert(decoded == "This line is encoded; with \\s"); 207 --- 208 209 Params: 210 line = Original line to decode. 211 212 Returns: 213 A decoded string without `\s` in it. 214 +/ 215 auto decodeIRCv3String(const string line) pure @safe nothrow 216 { 217 import std.array : Appender; 218 import std.string : representation; 219 220 /++ 221 - http://ircv3.net/specs/core/message-tags-3.2.html 222 223 If a lone \ exists at the end of an escaped value (with no escape 224 character following it), then there SHOULD be no output character. 225 For example, the escaped value test\ should unescape to test. 226 +/ 227 228 if (!line.length) return string.init; 229 230 Appender!(char[]) sink; 231 232 bool escaping; 233 bool dirty; 234 235 foreach (immutable i, immutable c; line.representation) 236 { 237 if (escaping) 238 { 239 if (!dirty) 240 { 241 sink.reserve(line.length); 242 sink.put(line[0..i-1]); 243 dirty = true; 244 } 245 246 switch (c) 247 { 248 case '\\': 249 sink.put('\\'); 250 break; 251 252 case ':': 253 sink.put(';'); 254 break; 255 256 case 's': 257 sink.put(' '); 258 break; 259 260 case 'n': 261 sink.put('\n'); 262 break; 263 264 case 't': 265 sink.put('\t'); 266 break; 267 268 case 'r': 269 sink.put('\r'); 270 break; 271 272 case '0': 273 sink.put('\0'); 274 break; 275 276 default: 277 // Unknown escape 278 sink.put(c); 279 } 280 281 escaping = false; 282 } 283 else 284 { 285 switch (c) 286 { 287 case '\\': 288 escaping = true; 289 break; 290 291 default: 292 if (dirty) sink.put(c); 293 break; 294 } 295 } 296 } 297 298 return dirty ? sink.data : line; 299 } 300 301 /// 302 @safe unittest 303 { 304 immutable s1 = decodeIRCv3String(`kameloso\sjust\ssubscribed\swith\sa\s` ~ 305 `$4.99\ssub.\skameloso\ssubscribed\sfor\s40\smonths\sin\sa\srow!`); 306 assert((s1 == "kameloso just subscribed with a $4.99 sub. " ~ 307 "kameloso subscribed for 40 months in a row!"), s1); 308 309 immutable s2 = decodeIRCv3String(`stop\sspamming\scaps,\sautomated\sby\sNightbot`); 310 assert((s2 == "stop spamming caps, automated by Nightbot"), s2); 311 312 immutable s3 = decodeIRCv3String(`\:__\:`); 313 assert((s3 == ";__;"), s3); 314 315 immutable s4 = decodeIRCv3String(`\\o/ \\o\\ /o/ ~o~`); 316 assert((s4 == `\o/ \o\ /o/ ~o~`), s4); 317 318 immutable s5 = decodeIRCv3String(`This\sis\sa\stest\`); 319 assert((s5 == "This is a test"), s5); 320 321 immutable s6 = decodeIRCv3String(`9\sraiders\sfrom\sVHSGlitch\shave\sjoined\n!`); 322 assert((s6 == "9 raiders from VHSGlitch have joined\n!"), s6); 323 324 { 325 immutable before = `\s\s\s`; 326 immutable after = decodeIRCv3String(before); 327 assert((after == " "), after); 328 assert(before !is after); 329 } 330 { 331 immutable before = `foo bar`; 332 immutable after = decodeIRCv3String(before); 333 assert((after == "foo bar"), after); 334 assert(before is after); 335 } 336 } 337 338 339 // isAuthService 340 /++ 341 Inspects an [dialect.defs.IRCUser|IRCUser] and judges whether or not it is 342 authentication services. 343 344 This is very ad-hoc. 345 346 Example: 347 --- 348 IRCUser user; 349 350 if (user.isAuthService(parser)) 351 { 352 // ... 353 } 354 --- 355 356 Params: 357 sender = [dialect.defs.IRCUser|IRCUser] to examine. 358 parser = Reference to the current [dialect.parsing.IRCParser|IRCParser]. 359 360 Returns: 361 `true` if the `sender` is judged to be from nickname services, `false` if not. 362 +/ 363 auto isAuthService( 364 const IRCUser sender, 365 const ref IRCParser parser) pure @safe 366 { 367 import lu.common : sharedDomains; 368 import lu.string : contains; 369 370 version(TwitchSupport) 371 { 372 if (parser.server.daemon == IRCServer.Daemon.twitch) return false; 373 } 374 375 top: 376 switch (sender.nickname) 377 { 378 case "NickServ": 379 case "SaslServ": 380 switch (sender.ident) 381 { 382 case "NickServ": 383 case "SaslServ": 384 if (sender.address == "services.") return true; // freenode 385 else if (sender.address == "services") return true; // snoonet 386 // Unknown address, drop to after switch 387 break top; 388 389 case "services": 390 switch (sender.address) 391 { 392 case "services.host": // SwiftIRC 393 case "geekshed.net": 394 case "services.irchighway.net": 395 case "services.oftc.net": 396 case "gimpnet-services.gimp.org": 397 return true; 398 399 default: 400 // Unknown address, drop to after switch 401 break top; 402 } 403 404 case "service": 405 switch (sender.address) 406 { 407 case "RusNet": 408 case "dal.net": 409 case "rizon.net": 410 return true; 411 412 default: 413 // Unknown address, drop to after switch 414 break top; 415 } 416 417 default: 418 // Unknown ident, drop to after switch 419 break top; 420 } 421 422 case "Q": 423 // :Q!TheQBot@CServe.quakenet.org NOTICE kameloso :You are now logged in as kameloso. 424 // :Q!services@swiftirc.net NOTICE kameloso^ :[#rot] 4Reign of Terror 425 if ((sender.ident == "TheQBot") && (sender.address == "CServe.quakenet.org")) return true; 426 //else if ((sender.ident == "services") && (sender.address == "swiftirc.net")) return true; 427 break; 428 429 case "AuthServ": 430 case "authserv": 431 // :AuthServ!AuthServ@Services.GameSurge.net NOTICE kameloso :Could not find your account 432 if ((sender.ident == "AuthServ") && (sender.address == "Services.GameSurge.net")) return true; 433 // Unknown ident/address, drop to after switch 434 break; 435 436 case string.init: 437 if (sender.address == "services.") return true; 438 goto default; 439 440 default: 441 // Unknown nickname 442 return false; 443 } 444 445 // We're here if nick nickserv/sasl/etc and unknown ident, or server mismatch 446 // As such, no need to be as strict as isSpecial is 447 448 return (sharedDomains(sender.address, parser.server.address) >= 2) || 449 (sharedDomains(sender.address, parser.server.resolvedAddress) >= 2); 450 } 451 452 /// 453 /*@safe*/ unittest 454 { 455 IRCParser parser; 456 457 { 458 immutable event = parser.toIRCEvent(":Q!TheQBot@CServe.quakenet.org " ~ 459 "NOTICE kameloso :You are now logged in as kameloso."); 460 assert(event.sender.isAuthService(parser)); 461 } 462 { 463 immutable event = parser.toIRCEvent(":NickServ!NickServ@services. " ~ 464 "NOTICE kameloso :This nickname is registered."); 465 assert(event.sender.isAuthService(parser)); 466 } 467 468 parser.server.address = "irc.rizon.net"; 469 parser.server.resolvedAddress = "irc.uworld.se"; 470 471 { 472 immutable event = parser.toIRCEvent(":NickServ!service@rizon.net " ~ 473 "NOTICE kameloso^^ :nick, type /msg NickServ IDENTIFY password. Otherwise,"); 474 assert(event.sender.isAuthService(parser)); 475 } 476 } 477 478 479 // isValidChannel 480 /++ 481 Examines a string and judges whether or not it *looks* like a channel. 482 483 It needs to be passed an [dialect.defs.IRCServer|IRCServer] to know the max 484 channel name length. An alternative would be to change the 485 [dialect.defs.IRCServer|IRCServer] parameter to be an `uint`. 486 487 Example: 488 --- 489 IRCServer server; 490 assert("#channel".isValidChannel(server)); 491 assert("##channel".isValidChannel(server)); 492 assert(!"!channel".isValidChannel(server)); 493 assert(!"#ch#annel".isValidChannel(server)); 494 --- 495 496 Params: 497 channel = String of a potential channel name. 498 server = The current [dialect.defs.IRCServer|IRCServer] with all its settings. 499 500 Returns: 501 `true` if the string content is judged to be a channel, `false` if not. 502 +/ 503 auto isValidChannel( 504 const string channel, 505 const IRCServer server) pure @safe nothrow @nogc 506 { 507 import std.string : representation; 508 509 /+ 510 Channels names are strings (beginning with a '&' or '#' character) of 511 length up to 200 characters. Apart from the requirement that the 512 first character being either '&' or '#'; the only restriction on a 513 channel name is that it may not contain any spaces (' '), a control G 514 (^G or ASCII 7), or a comma (',' which is used as a list item 515 separator by the protocol). 516 517 - https://tools.ietf.org/html/rfc1459.html 518 +/ 519 if ((channel.length < 2) || (channel.length > server.maxChannelLength)) 520 { 521 // Too short or too long a word 522 return false; 523 } 524 525 if (!server.chantypes.contains(channel[0])) return false; 526 527 if (channel.contains(' ') || 528 channel.contains(',') || 529 channel.contains(7)) 530 { 531 // Contains spaces, commas or byte 7 532 return false; 533 } 534 535 if (channel.length == 2) return !server.chantypes.contains(channel[1]); 536 else if (channel.length == 3) return !server.chantypes.contains(channel[2]); 537 else if (channel.length > 3) 538 { 539 // Allow for two ##s (or &&s) in the name but no more 540 foreach (immutable chansign; server.chantypes.representation) 541 { 542 if (channel[2..$].contains(chansign)) return false; 543 } 544 return true; 545 } 546 else 547 { 548 return false; 549 } 550 } 551 552 /// 553 @safe unittest 554 { 555 IRCServer s; 556 s.chantypes = "#&"; 557 558 assert("#channelName".isValidChannel(s)); 559 assert("&otherChannel".isValidChannel(s)); 560 assert("##freenode".isValidChannel(s)); 561 assert(!"###froonode".isValidChannel(s)); 562 assert(!"#not a channel".isValidChannel(s)); 563 assert(!"notAChannelEither".isValidChannel(s)); 564 assert(!"#".isValidChannel(s)); 565 //assert(!"".isValidChannel(s)); 566 assert(!"##".isValidChannel(s)); 567 assert(!"&&".isValidChannel(s)); 568 assert("#d".isValidChannel(s)); 569 assert("#uk".isValidChannel(s)); 570 assert(!"###".isValidChannel(s)); 571 assert(!"#a#".isValidChannel(s)); 572 assert(!"a".isValidChannel(s)); 573 assert(!" ".isValidChannel(s)); 574 //assert(!"".isValidChannel(s)); 575 } 576 577 578 // isValidNickname 579 /++ 580 Examines a string and judges whether or not it *looks* like a nickname. 581 582 It only looks for invalid characters in the name as well as it length. 583 584 Example: 585 --- 586 assert("kameloso".isValidNickname); 587 assert("kameloso^".isValidNickname); 588 assert("kamelåså".isValidNickname); 589 assert(!"#kameloso".isValidNickname); 590 assert(!"k&&me##so".isValidNickname); 591 --- 592 593 Params: 594 nickname = String nickname. 595 server = The current [dialect.defs.IRCServer|IRCServer] with all its settings. 596 597 Returns: 598 `true` if the nickname string is judged to be a nickname, `false` if not. 599 +/ 600 auto isValidNickname( 601 const string nickname, 602 const IRCServer server) pure @safe nothrow @nogc 603 { 604 import std.string : representation; 605 606 if (!nickname.length || (nickname.length > server.maxNickLength)) 607 { 608 return false; 609 } 610 611 immutable rep = nickname.representation; 612 613 if (((rep[0] >= '0') && (rep[0] <= '9')) || (rep[0] == '-')) 614 { 615 return false; 616 } 617 else if (!rep[0].isValidNicknameCharacter) 618 { 619 return false; 620 } 621 622 foreach (immutable c; rep[1..$]) 623 { 624 if (!c.isValidNicknameCharacter) return false; 625 } 626 627 return true; 628 } 629 630 /// 631 @safe unittest 632 { 633 import std.range : repeat; 634 import std.conv : to; 635 636 IRCServer s; 637 638 immutable validNicknames = 639 [ 640 "kameloso", 641 "kameloso^", 642 "zorael-", 643 "hirr{}", 644 "asdf`", 645 "[afk]me", 646 "a-zA-Z0-9", 647 `\`, 648 ]; 649 650 immutable invalidNicknames = 651 [ 652 //"", 653 'X'.repeat(s.maxNickLength+1).to!string, 654 "åäöÅÄÖ", 655 "\n", 656 "¨", 657 "@pelle", 658 "+calvin", 659 "&hobbes", 660 "#channel", 661 "$deity", 662 "0kameloso", 663 "-kameloso", 664 ]; 665 666 foreach (immutable nickname; validNicknames) 667 { 668 assert(nickname.isValidNickname(s), nickname); 669 } 670 671 foreach (immutable nickname; invalidNicknames) 672 { 673 assert(!nickname.isValidNickname(s), nickname); 674 } 675 } 676 677 678 // isValidNicknameCharacter 679 /++ 680 Returns whether or not a passed `char` can be part of a nickname. 681 682 The IRC standard describes nicknames as being a string of any of the 683 following characters: 684 685 `[a-z] [A-Z] [0-9] _-\\[]{}^\`|` 686 687 Example: 688 --- 689 assert('a'.isValidNicknameCharacter); 690 assert('9'.isValidNicknameCharacter); 691 assert('`'.isValidNicknameCharacter); 692 assert(!(' '.isValidNicknameCharacter)); 693 --- 694 695 Params: 696 c = Character to compare with the list of accepted characters in a nickname. 697 698 Returns: 699 `true` if the character is in the list of valid characters for 700 nicknames, `false` if not. 701 +/ 702 auto isValidNicknameCharacter(const ubyte c) pure @safe nothrow @nogc 703 { 704 switch (c) 705 { 706 case 'a': 707 .. 708 case 'z': 709 case 'A': 710 .. 711 case 'Z': 712 case '0': 713 .. 714 case '9': 715 case '_': 716 case '-': 717 case '\\': 718 case '[': 719 case ']': 720 case '{': 721 case '}': 722 case '^': 723 case '`': 724 case '|': 725 return true; 726 default: 727 return false; 728 } 729 } 730 731 /// 732 @safe unittest 733 { 734 import std.string : representation; 735 736 { 737 immutable line = "abcDEFghi0{}29304_[]`\\^|---"; 738 foreach (immutable char c; line.representation) 739 { 740 assert(c.isValidNicknameCharacter, c ~ ""); 741 } 742 } 743 744 assert(!' '.isValidNicknameCharacter); 745 } 746 747 748 // stripModesign 749 /++ 750 Takes a nickname and strips it of any prepended mode signs, like the `@` in 751 `@nickname`. Saves the stripped signs in the ref string `modesigns`. 752 753 Example: 754 --- 755 IRCServer server; 756 immutable signed = "@+kameloso"; 757 string signs; 758 immutable nickname = server.stripModeSign(signed, signs); 759 assert((nickname == "kameloso"), nickname); 760 assert((signs == "@+"), signs); 761 --- 762 763 Params: 764 nickname = String with a signed nickname. 765 server = [dialect.defs.IRCServer|IRCServer], with all its settings. 766 modesigns = Reference string to write the stripped modesigns to. 767 768 Returns: 769 The nickname without any prepended prefix signs. 770 +/ 771 auto stripModesign( 772 const string nickname, 773 const IRCServer server, 774 out string modesigns) pure @safe nothrow @nogc 775 in (nickname.length, "Tried to strip modesigns off an empty nickname") 776 { 777 size_t i; 778 779 for (i = 0; i<nickname.length; ++i) 780 { 781 if (nickname[i] !in server.prefixchars) 782 { 783 break; 784 } 785 } 786 787 modesigns = nickname[0..i]; 788 return nickname[i..$]; 789 } 790 791 /// 792 unittest 793 { 794 IRCServer server; 795 server.prefixchars = 796 [ 797 '@' : 'o', 798 '+' : 'v', 799 '%' : 'h', 800 ]; 801 802 { 803 immutable signed = "@kameloso"; 804 string signs; 805 immutable nickname = signed.stripModesign(server, signs); 806 assert((nickname == "kameloso"), nickname); 807 assert((signs == "@"), signs); 808 } 809 810 { 811 immutable signed = "kameloso"; 812 string signs; 813 immutable nickname = signed.stripModesign(server, signs); 814 assert((nickname == "kameloso"), nickname); 815 assert(!signs.length, signs); 816 } 817 818 { 819 immutable signed = "@+kameloso"; 820 string signs; 821 immutable nickname = signed.stripModesign(server, signs); 822 assert((nickname == "kameloso"), nickname); 823 assert((signs == "@+"), signs); 824 } 825 } 826 827 828 // stripModesign 829 /++ 830 Convenience function to [stripModesign] that doesn't take an out string 831 parameter to store the stripped modesign characters in. 832 833 Example: 834 --- 835 IRCServer server; 836 immutable signed = "@+kameloso"; 837 immutable nickname = server.stripModeSign(signed); 838 assert((nickname == "kameloso"), nickname); 839 assert((signs == "@+"), signs); 840 --- 841 842 Params: 843 nickname = The (potentially) signed nickname to strip the prefix off. 844 server = The [dialect.defs.IRCServer|IRCServer] whose prefix characters to strip. 845 846 Returns: 847 The raw nickname, unsigned. 848 +/ 849 auto stripModesign( 850 const string nickname, 851 const IRCServer server) pure @safe nothrow @nogc 852 { 853 string _; 854 return stripModesign(nickname, server, _); 855 } 856 857 /// 858 @safe unittest 859 { 860 IRCServer server; 861 server.prefixchars = 862 [ 863 '@' : 'o', 864 '+' : 'v', 865 '%' : 'h', 866 ]; 867 868 { 869 immutable signed = "@+kameloso"; 870 immutable nickname = signed.stripModesign(server); 871 assert((nickname == "kameloso"), nickname); 872 } 873 } 874 875 876 // setMode 877 /++ 878 Sets a new or removes a [dialect.defs.IRCChannel.Mode|IRCChannel.Mode]. 879 880 [dialect.defs.IRCChannel.Mode|IRCChannel.Mode]s that are merely a character 881 in `modechars` are simply removed if the *sign* of the mode change is negative, 882 whereas a more elaborate [dialect.defs.IRCChannel.Mode|IRCChannel.Mode] 883 in the `modes` array are only replaced or removed if they match a comparison test. 884 885 Several modes can be specified at once, including modes that take a 886 `data` argument, assuming they are in the proper order (where the 887 `data`-taking modes are at the end of the string). 888 889 Care has to be taken not to have trailing spaces in the arguments. 890 891 Example: 892 --- 893 IRCChannel channel; 894 channel.setMode("+oo zorael!NaN@* kameloso!*@*") 895 assert(channel.modes.length == 2); 896 channel.setMode("-o kameloso!*@*"); 897 assert(channel.modes.length == 1); 898 channel.setMode("-o *!*@*"); 899 assert(!channel.modes.length); 900 --- 901 902 Params: 903 channel = [dialect.defs.IRCChannel] whose modes are being set. 904 signedModestring = String of the raw mode command, including the 905 prefixing sign (+ or -). 906 data = Appendix to the signed modestring; arguments to the modes that 907 are being set. 908 server = The current [dialect.defs.IRCServer|IRCServer] with all its settings. 909 +/ 910 void setMode( 911 ref IRCChannel channel, 912 const string signedModestring, 913 const string data, 914 const IRCServer server) pure @safe 915 { 916 import lu.string : beginsWith; 917 import std.array : array; 918 import std.algorithm.iteration : splitter; 919 import std.range : StoppingPolicy, retro, zip; 920 import std.string : representation; 921 922 if (!signedModestring.length) return; 923 924 struct SignedModechar 925 { 926 char sign; 927 char modechar; 928 } 929 930 char nextSign = '+'; 931 SignedModechar[] modecharArray; 932 933 foreach (immutable c; signedModestring.representation) 934 { 935 if ((c == '+') || (c == '-')) 936 { 937 nextSign = c; 938 } 939 else if (((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z'))) 940 { 941 modecharArray ~= SignedModechar(nextSign, c); 942 } 943 } 944 945 if (!modecharArray.length) return; 946 947 auto datalines = data.splitter(" ").array.retro; 948 auto moderange = modecharArray.retro; 949 auto ziprange = zip(StoppingPolicy.longest, moderange, datalines); 950 951 IRCUser[] carriedExceptions; 952 953 ziploop: 954 foreach (immutable signedModechar, immutable datastring; ziprange) 955 { 956 immutable modechar = signedModechar.modechar; 957 immutable sign = signedModechar.sign; 958 959 if ((sign != '+') && (sign != '-')) 960 { 961 // Ward against stack corruption 962 // immutable(SignedModechar)('ÿ', 'ÿ') 963 break; 964 } 965 966 IRCChannel.Mode newMode; 967 newMode.modechar = modechar; 968 969 if ((modechar == server.exceptsChar) || (modechar == server.invexChar)) 970 { 971 // Exception, carry it to the next aMode 972 carriedExceptions ~= IRCUser(datastring); 973 continue; 974 } 975 976 if (!datastring.beginsWith(server.extbanPrefix) && 977 datastring.contains('!') && datastring.contains('@')) 978 { 979 // Looks like a user and not an extban 980 newMode.user = IRCUser(datastring); 981 } 982 else if (datastring.beginsWith(server.extbanPrefix)) 983 { 984 // extban; https://freenode.net/kb/answer/extbans 985 // https://defs.ircdocs.horse/defs/extbans.html 986 // Does not support a mix of normal and second form bans 987 // e.g. *!*@*$#channel 988 989 /+ extban format: 990 "$a:dannylee$##arguments" 991 "$a:shr000ms" 992 "$a:deadfrogs" 993 "$a:b4b" 994 "$a:terabits$##arguments" 995 // "$x:*0x71*" 996 "$a:DikshitNijjer" 997 "$a:NETGEAR_WNDR3300" 998 "$~a:eir"+/ 999 string slice = datastring[1..$]; // mutable 1000 1001 if (slice[0] == '~') 1002 { 1003 // Negated extban 1004 newMode.negated = true; 1005 slice = slice[1..$]; 1006 } 1007 1008 switch (slice[0]) 1009 { 1010 case 'a': 1011 case 'R': 1012 // Match account 1013 if (slice.contains(':')) 1014 { 1015 // More than one field 1016 slice.nom(':'); 1017 1018 if (slice.contains('$')) 1019 { 1020 // More than one field, first is account 1021 newMode.user.account = slice.nom('$'); 1022 newMode.data = slice; 1023 } 1024 else 1025 { 1026 // Whole slice is an account 1027 newMode.user.account = slice; 1028 } 1029 } 1030 else 1031 { 1032 // "$~a" 1033 // "$R" 1034 // FIXME: Figure out how to express this. 1035 newMode.data = slice.length ? 1036 slice : 1037 datastring; 1038 } 1039 break; 1040 1041 case 'j': 1042 //case 'c': // Conflicts with colour ban 1043 // Match channel 1044 slice.nom(':'); 1045 newMode.channel = slice; 1046 break; 1047 1048 /*case 'r': 1049 // GECOS/Real name, which we aren't saving currently. 1050 // Can be done if there's a use-case for it. 1051 break;*/ 1052 1053 /*case 's': 1054 // Which server the user(s) the mode refers to are connected to 1055 // which we aren't saving either. Can also be fixed. 1056 break;*/ 1057 1058 default: 1059 // Unhandled extban mode 1060 newMode.data = datastring; 1061 break; 1062 } 1063 } 1064 else 1065 { 1066 // Normal, non-user non-extban mode 1067 newMode.data = datastring; 1068 } 1069 1070 if (sign == '+') 1071 { 1072 if (server.prefixes.contains(modechar)) 1073 { 1074 import std.algorithm.searching : canFind; 1075 1076 // Register users with prefix modes (op, halfop, voice, ...) 1077 auto prefixedUsers = newMode.modechar in channel.mods; 1078 if (prefixedUsers && (newMode.data in *prefixedUsers)) 1079 { 1080 continue; 1081 } 1082 1083 channel.mods[newMode.modechar][newMode.data] = true; 1084 continue; 1085 } 1086 else if (server.aModes.contains(modechar)) 1087 { 1088 /++ 1089 A = Mode that adds or removes a nick or address to a 1090 list. Always has a parameter. 1091 +/ 1092 1093 // STACKS. 1094 // If an identical Mode exists, add exceptions and skip 1095 foreach (ref listedMode; channel.modes) 1096 { 1097 if (listedMode == newMode) 1098 { 1099 listedMode.exceptions ~= carriedExceptions; 1100 carriedExceptions.length = 0; 1101 continue ziploop; 1102 } 1103 } 1104 1105 newMode.exceptions ~= carriedExceptions; 1106 carriedExceptions.length = 0; 1107 } 1108 else if (server.bModes.contains(modechar) || server.cModes.contains(modechar)) 1109 { 1110 /++ 1111 B = Mode that changes a setting and always has a 1112 parameter. 1113 1114 C = Mode that changes a setting and only has a 1115 parameter when set. 1116 +/ 1117 1118 // DOES NOT STACK. 1119 // If an identical Mode exists, overwrite 1120 foreach (ref listedMode; channel.modes) 1121 { 1122 if (listedMode.modechar == modechar) 1123 { 1124 listedMode = newMode; 1125 continue ziploop; 1126 } 1127 } 1128 } 1129 else /*if (server.dModes.contains(modechar))*/ 1130 { 1131 // Some clients assume that any mode not listed is of type D 1132 if (!channel.modechars.contains(modechar)) channel.modechars ~= modechar; 1133 continue; 1134 } 1135 1136 channel.modes ~= newMode; 1137 } 1138 else if (sign == '-') 1139 { 1140 import std.algorithm.mutation : SwapStrategy, remove; 1141 1142 if (server.prefixes.contains(modechar)) 1143 { 1144 import std.algorithm.searching : countUntil; 1145 1146 // Remove users with prefix modes (op, halfop, voice, ...) 1147 auto prefixedUsers = newMode.modechar in channel.mods; 1148 if (!prefixedUsers) continue; 1149 1150 (*prefixedUsers).remove(newMode.data); 1151 } 1152 else if (server.aModes.contains(modechar)) 1153 { 1154 /++ 1155 A = Mode that adds or removes a nick or address to a 1156 a list. Always has a parameter. 1157 +/ 1158 1159 // If a comparison matches, remove 1160 channel.modes = channel.modes 1161 .remove!((listed => listed == newMode), SwapStrategy.unstable); 1162 } 1163 else if (server.bModes.contains(modechar) || server.cModes.contains(modechar)) 1164 { 1165 /++ 1166 B = Mode that changes a setting and always has a 1167 parameter. 1168 1169 C = Mode that changes a setting and only has a 1170 parameter when set. 1171 +/ 1172 1173 // If the modechar matches, remove 1174 channel.modes = channel.modes.remove!((listed => 1175 listed.modechar == newMode.modechar), SwapStrategy.unstable); 1176 } 1177 else /*if (server.dModes.contains(modechar))*/ 1178 { 1179 // Some clients assume that any mode not listed is of type D 1180 import std.string : indexOf; 1181 1182 immutable modecharIndex = channel.modechars.indexOf(modechar); 1183 if (modecharIndex != -1) 1184 { 1185 import std.string : representation; 1186 1187 // Remove the char from the modechar string 1188 channel.modechars = cast(string)channel.modechars 1189 .dup 1190 .representation 1191 .remove!(SwapStrategy.unstable)(modecharIndex) 1192 .idup; 1193 } 1194 } 1195 } 1196 else 1197 { 1198 assert(0, "Invalid mode sign: " ~ sign); 1199 } 1200 } 1201 } 1202 1203 /// 1204 @safe unittest 1205 { 1206 import std.conv; 1207 import std.stdio; 1208 1209 IRCServer server; 1210 // Freenode: CHANMODES=eIbq,k,flj,CFLMPQScgimnprstz 1211 with (server) 1212 { 1213 aModes = "eIbq"; 1214 bModes = "k"; 1215 cModes = "flj"; 1216 dModes = "CFLMPQScgimnprstz"; 1217 1218 // SpotChat: PREFIX=(Yqaohv)!~&@%+ 1219 prefixes = "Yaohv"; 1220 prefixchars = 1221 [ 1222 '!' : 'Y', 1223 '&' : 'a', 1224 '@' : 'o', 1225 '%' : 'h', 1226 '+' : 'v', 1227 ]; 1228 1229 extbanPrefix = '$'; 1230 exceptsChar = 'e'; 1231 invexChar = 'I'; 1232 } 1233 1234 { 1235 IRCChannel chan; 1236 1237 chan.topic = "Huerbla"; 1238 1239 chan.setMode("+b", "kameloso!~NaN@aasdf.freenode.org", server); 1240 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1241 //writeln("-------------------------------------"); 1242 assert((chan.modes.length == 1), chan.modes.length.to!string); 1243 1244 chan.setMode("+bbe", "hirrsteff!*@* harblsnarf!ident@* NICK!~IDENT@ADDRESS", server); 1245 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1246 //writeln("-------------------------------------"); 1247 assert((chan.modes.length == 3), chan.modes.length.to!string); 1248 1249 chan.setMode("-b", "*!*@*", server); 1250 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1251 //writeln("-------------------------------------"); 1252 assert((chan.modes.length == 3), chan.modes.length.to!string); 1253 1254 chan.setMode("+i", string.init, server); 1255 assert(chan.modechars == "i", chan.modechars); 1256 1257 chan.setMode("+v", "harbl", server); 1258 assert(chan.modechars == "i", chan.modechars); 1259 1260 chan.setMode("-i", string.init, server); 1261 assert(!chan.modechars.length, chan.modechars); 1262 1263 chan.setMode("+l", "200", server); 1264 IRCChannel.Mode lMode; 1265 lMode.modechar = 'l'; 1266 lMode.data = "200"; 1267 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1268 //writeln("-------------------------------------"); 1269 assert((chan.modes[3] == lMode), chan.modes[3].to!string); 1270 1271 chan.setMode("+l", "100", server); 1272 lMode.modechar = 'l'; 1273 lMode.data = "100"; 1274 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1275 //writeln("-------------------------------------"); 1276 assert((chan.modes[3] == lMode), chan.modes[3].to!string); 1277 } 1278 1279 { 1280 IRCChannel chan; 1281 1282 chan.setMode("+CLPcnprtf", "##linux-overflow", server); 1283 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1284 //writeln("-------------------------------------"); 1285 assert(chan.modes[0].data == "##linux-overflow"); 1286 assert(chan.modes.length == 1); 1287 assert(chan.modechars.length == 8); 1288 1289 chan.setMode("+bee", "mynick!myident@myaddress abc!def@ghi jkl!*@*", server); 1290 //foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1291 //writeln("-------------------------------------"); 1292 assert(chan.modes.length == 2); 1293 assert(chan.modes[1].exceptions.length == 2); 1294 } 1295 1296 { 1297 IRCChannel chan; 1298 1299 chan.setMode("+ns", string.init, server); 1300 foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1301 assert(chan.modes.length == 0); 1302 assert(chan.modechars == "sn", chan.modechars); 1303 1304 chan.setMode("-sn", string.init, server); 1305 foreach (i, mode; chan.modes) writefln("%2d: %s", i, mode); 1306 assert(chan.modes.length == 0); 1307 assert(chan.modechars.length == 0); 1308 } 1309 1310 { 1311 IRCChannel chan; 1312 chan.setMode("+oo", "kameloso zorael", server); 1313 assert(chan.mods['o'].length == 2); 1314 chan.setMode("-o", "kameloso", server); 1315 assert(chan.mods['o'].length == 1); 1316 chan.setMode("-o", "zorael", server); 1317 assert(!chan.mods['o'].length); 1318 } 1319 1320 { 1321 IRCChannel chan; 1322 server.extbanPrefix = '$'; 1323 1324 chan.setMode("+b", "$a:hirrsteff", server); 1325 assert(chan.modes.length); 1326 with (chan.modes[0]) 1327 { 1328 assert((modechar == 'b'), modechar.text); 1329 assert((user.account == "hirrsteff"), user.account); 1330 } 1331 1332 chan.setMode("+q", "$~a:blarf", server); 1333 assert((chan.modes.length == 2), chan.modes.length.text); 1334 with (chan.modes[1]) 1335 { 1336 assert((modechar == 'q'), modechar.text); 1337 assert((user.account == "blarf"), user.account); 1338 assert(negated); 1339 IRCUser blarf; 1340 blarf.nickname = "blarf"; 1341 blarf.account = "blarf"; 1342 assert(blarf.matchesByMask(user)); 1343 } 1344 } 1345 1346 { 1347 IRCChannel chan; 1348 1349 chan.setMode("+t", string.init, server); 1350 assert(!chan.modes.length, chan.modes.length.text); 1351 assert((chan.modechars == "t"), chan.modechars); 1352 1353 chan.setMode("-t+nlk", "42 chankey", server); 1354 assert((chan.modes.length == 2), chan.modes.length.text); 1355 with (chan.modes[0]) 1356 { 1357 assert((modechar == 'k'), modechar.text); 1358 assert((data == "chankey"), data); 1359 } 1360 with (chan.modes[1]) 1361 { 1362 assert((modechar == 'l'), modechar.text); 1363 assert((data == "42"), data); 1364 } 1365 1366 assert((chan.modechars == "n"), chan.modechars); 1367 1368 chan.setMode("-kl", string.init, server); 1369 assert(!chan.modes.length, chan.modes.length.text); 1370 } 1371 } 1372 1373 1374 // IRCParseException 1375 /++ 1376 IRC Parsing Exception, thrown when there were errors parsing. 1377 1378 It is a normal [object.Exception] but with an attached 1379 [dialect.defs.IRCEvent|IRCEvent]. 1380 +/ 1381 final class IRCParseException : Exception 1382 { 1383 /// Bundled [dialect.defs.IRCEvent|IRCEvent], parsing which threw this exception. 1384 IRCEvent event; 1385 1386 /++ 1387 Create a new [IRCParseException], without attaching an 1388 [dialect.defs.IRCEvent|IRCEvent]. 1389 +/ 1390 this(const string message, 1391 const string file = __FILE__, 1392 const size_t line = __LINE__, 1393 Throwable nextInChain = null) pure nothrow @nogc @safe 1394 { 1395 super(message, file, line, nextInChain); 1396 } 1397 1398 /++ 1399 Create a new [IRCParseException], attaching an 1400 [dialect.defs.IRCEvent|IRCEvent] to it. 1401 +/ 1402 this(const string message, 1403 const IRCEvent event, 1404 const string file = __FILE__, 1405 const size_t line = __LINE__, 1406 Throwable nextInChain = null) pure nothrow @nogc @safe 1407 { 1408 this.event = event; 1409 super(message, file, line, nextInChain); 1410 } 1411 } 1412 1413 /// 1414 @safe unittest 1415 { 1416 import std.exception : assertThrown; 1417 1418 IRCEvent event; 1419 1420 assertThrown!IRCParseException((){ throw new IRCParseException("adf"); }()); 1421 1422 assertThrown!IRCParseException(() 1423 { 1424 throw new IRCParseException("adf", event); 1425 }()); 1426 1427 assertThrown!IRCParseException(() 1428 { 1429 throw new IRCParseException("adf", event, "somefile.d"); 1430 }()); 1431 1432 assertThrown!IRCParseException(() 1433 { 1434 throw new IRCParseException("adf", event, "somefile.d", 9999U); 1435 }()); 1436 } 1437 1438 1439 // IRCControlCharacter 1440 /++ 1441 Certain characters that signal specific meaning in an IRC context. 1442 +/ 1443 enum IRCControlCharacter 1444 { 1445 ctcp = 1, /// Client-to-client Protocol marker. 1446 bold = 2, /// Bold text. 1447 colour = 3, /// Colour marker. 1448 reset = 15, /// Colour/formatting reset marker. 1449 invert = 22, /// Inverse text marker. 1450 italics = 29, /// Italics marker. 1451 underlined = 31, /// Underscore marker. 1452 } 1453 1454 1455 // matchesByMask 1456 /++ 1457 Compares this [dialect.defs.IRCUser|IRCUser] with a second one, treating 1458 fields with asterisks as glob wildcards, mimicking `*!*@*` mask matching. 1459 1460 Example: 1461 --- 1462 IRCUser u1; 1463 with (u1) 1464 { 1465 nickname = "foo"; 1466 ident = "NaN"; 1467 address = "asdf.asdf.com"; 1468 } 1469 1470 IRCUser u2; 1471 with (u2) 1472 { 1473 nickname = "*"; 1474 ident = "NaN"; 1475 address = "*"; 1476 } 1477 1478 assert(u1.matchesByMask(u2)); 1479 assert(u1.matchesByMask("f*!NaN@*.com")); 1480 --- 1481 1482 Params: 1483 this_ = [dialect.defs.IRCUser|IRCUser] to compare. 1484 that = [dialect.defs.IRCUser|IRCUser] to compare `this_` with. 1485 caseMapping = [dialect.defs.IRCServer.CaseMapping|IRCServer.CaseMapping] 1486 with which to translate the nicknames in the relevant masks to lowercase. 1487 1488 Returns: 1489 `true` if the [dialect.defs.IRCUser|IRCUser]s are deemed to match, `false` if not. 1490 +/ 1491 auto matchesByMask( 1492 const IRCUser this_, 1493 const IRCUser that, 1494 const IRCServer.CaseMapping caseMapping = IRCServer.CaseMapping.rfc1459) pure @safe nothrow 1495 { 1496 // unpatternedGlobMatch 1497 /++ 1498 Performs a glob match without taking special consideration of 1499 bracketed patterns (with [, ], { and }). 1500 1501 Params: 1502 first = First string. 1503 second = Second expression string to glob match with the first. 1504 1505 Returns: 1506 True if `first` matches the `second` glob mask, false if not. 1507 +/ 1508 static bool unpatternedGlobMatch( 1509 const string first, 1510 const string second) 1511 { 1512 import std.array : replace; 1513 import std.path : CaseSensitive, globMatch; 1514 1515 enum caseSetting = CaseSensitive.no; 1516 1517 enum openBracketSubstitution = "\1"; 1518 enum closedBracketSubstitution = "\2"; 1519 enum openCurlySubstitution = "\3"; 1520 enum closedCurlySubstitution = "\4"; 1521 1522 immutable firstReplaced = first 1523 .replace("[", openBracketSubstitution) 1524 .replace("]", closedBracketSubstitution) 1525 .replace("{", openCurlySubstitution) 1526 .replace("}", closedCurlySubstitution); 1527 1528 immutable secondReplaced = second 1529 .replace("[", openBracketSubstitution) 1530 .replace("]", closedBracketSubstitution) 1531 .replace("{", openCurlySubstitution) 1532 .replace("}", closedCurlySubstitution); 1533 1534 return firstReplaced.globMatch!caseSetting(secondReplaced); 1535 } 1536 1537 // Only ever compare nicknames case-insensitive 1538 immutable ourLower = this_.nickname.toLowerCase(caseMapping); 1539 immutable theirLower = that.nickname.toLowerCase(caseMapping); 1540 1541 // (unpatterned) globMatch in both directions 1542 // If no match and either is empty, that means they're * 1543 1544 immutable matchNick = ( 1545 (ourLower == theirLower) || 1546 !this_.nickname.length || 1547 !that.nickname.length || 1548 unpatternedGlobMatch(ourLower, theirLower) || 1549 unpatternedGlobMatch(theirLower, ourLower)); 1550 if (!matchNick) return false; 1551 1552 immutable matchIdent = ( 1553 (this_.ident == that.ident) || 1554 !this_.ident.length || 1555 !that.ident.length || 1556 unpatternedGlobMatch(this_.ident, that.ident) || 1557 unpatternedGlobMatch(that.ident, this_.ident)); 1558 if (!matchIdent) return false; 1559 1560 immutable matchAddress = ( 1561 (this_.address == that.address) || 1562 !this_.address.length || 1563 !that.address.length || 1564 unpatternedGlobMatch(this_.address, that.address) || 1565 unpatternedGlobMatch(that.address, this_.address)); 1566 if (!matchAddress) return false; 1567 1568 return true; 1569 } 1570 1571 /// 1572 @safe unittest 1573 { 1574 IRCUser first = IRCUser("kameloso!NaN@wopkfoewopk.com"); 1575 1576 IRCUser second = IRCUser("*!*@*"); 1577 assert(first.matchesByMask(second)); 1578 1579 IRCUser third = IRCUser("kame*!*@*.com"); 1580 assert(first.matchesByMask(third)); 1581 1582 IRCUser fourth = IRCUser("*loso!*@wop*"); 1583 assert(first.matchesByMask(fourth)); 1584 1585 assert(second.matchesByMask(first)); 1586 assert(third.matchesByMask(first)); 1587 assert(fourth.matchesByMask(first)); 1588 1589 IRCUser fifth = IRCUser("kameloso!*@*"); 1590 IRCUser sixth = IRCUser("KAMELOSO!ident@address.com"); 1591 assert(fifth.matchesByMask(sixth)); 1592 1593 IRCUser seventh = IRCUser("^[0V0]^!ID@ADD"); 1594 IRCUser eight = IRCUser("~{0v0}~!id@add"); 1595 assert(seventh.matchesByMask(eight, IRCServer.CaseMapping.rfc1459)); 1596 assert(!seventh.matchesByMask(eight, IRCServer.CaseMapping.strict_rfc1459)); 1597 1598 IRCUser ninth = IRCUser("*!*@170.233.40.144]"); // Accidental trailing ] 1599 IRCUser tenth = IRCUser("Joe!Shmoe@*"); 1600 assert(ninth.matchesByMask(tenth, IRCServer.CaseMapping.rfc1459)); 1601 1602 IRCUser eleventh = IRCUser("abc]!*@*"); 1603 IRCUser twelfth = IRCUser("abc}!abc}@abc}"); 1604 assert(eleventh.matchesByMask(twelfth, IRCServer.CaseMapping.rfc1459)); 1605 } 1606 1607 1608 // isUpper 1609 /++ 1610 Checks whether the passed `char` is in uppercase as per the supplied case mappings. 1611 1612 Params: 1613 c = Character to examine. 1614 caseMapping = Server case mapping; maps uppercase to lowercase characters. 1615 1616 Returns: 1617 `true` if the passed `c` is in uppercase, `false` if not. 1618 +/ 1619 auto isUpper( 1620 const char c, 1621 const IRCServer.CaseMapping caseMapping) pure @safe nothrow @nogc 1622 { 1623 import std.ascii : isUpper; 1624 1625 if ((caseMapping == IRCServer.CaseMapping.rfc1459) || 1626 (caseMapping == IRCServer.CaseMapping.strict_rfc1459)) 1627 { 1628 switch (c) 1629 { 1630 case '[': 1631 case ']': 1632 case '\\': 1633 return true; 1634 case '^': 1635 return (caseMapping == IRCServer.CaseMapping.rfc1459); 1636 1637 default: 1638 break; 1639 } 1640 } 1641 1642 return c.isUpper; 1643 } 1644 1645 1646 // toLower 1647 /++ 1648 Produces the passed `char` in lowercase as per the supplied case mappings. 1649 1650 Params: 1651 c = Character to translate into lowercase. 1652 caseMapping = Server case mapping; maps uppercase to lowercase characters. 1653 1654 Returns: 1655 The passed `c` in lowercase as per the case mappings. 1656 +/ 1657 auto toLower( 1658 const char c, 1659 const IRCServer.CaseMapping caseMapping) pure @safe nothrow @nogc 1660 { 1661 import std.ascii : toLower; 1662 1663 if ((caseMapping == IRCServer.CaseMapping.rfc1459) || 1664 (caseMapping == IRCServer.CaseMapping.strict_rfc1459)) 1665 { 1666 switch (c) 1667 { 1668 case '[': 1669 return '{'; 1670 case ']': 1671 return '}'; 1672 case '\\': 1673 return '|'; 1674 case '^': 1675 if (caseMapping == IRCServer.CaseMapping.rfc1459) 1676 { 1677 return '~'; 1678 } 1679 break; 1680 1681 default: 1682 break; 1683 } 1684 } 1685 1686 return c.toLower; 1687 } 1688 1689 1690 // toLowerCase 1691 /++ 1692 Produces the passed string in lowercase as per the supplied case mappings. 1693 1694 This function is `@trusted` to be able to cast the internal `output` char 1695 array to string. [std.array.Appender|Appender] does this with its 1696 [std.array.Appender.data|data]/[std.array.Appender.opSlice|opSlice] methods. 1697 1698 --- 1699 @property inout(ElementEncodingType!A)[] opSlice() inout @trusted pure nothrow 1700 { 1701 /* @trusted operation: 1702 * casting Unqual!T[] to inout(T)[] 1703 */ 1704 return cast(typeof(return))(_data ? _data.arr : null); 1705 } 1706 --- 1707 1708 So just do the same. 1709 1710 Params: 1711 name = String to parse into lowercase. 1712 caseMapping = Server case mapping; maps uppercase to lowercase characters. 1713 1714 Returns: 1715 The passed `name` string with uppercase characters replaced as per 1716 the case mappings. 1717 +/ 1718 auto toLowerCase( 1719 const string name, 1720 const IRCServer.CaseMapping caseMapping) pure nothrow @trusted 1721 { 1722 import std.string : representation; 1723 1724 char[] output; // mutable 1725 bool dirty; 1726 1727 foreach (immutable i, immutable c; name.representation) 1728 { 1729 if (c.isUpper(caseMapping)) 1730 { 1731 if (!dirty) 1732 { 1733 output.length = name.length; 1734 output[0..i] = name[0..i]; 1735 dirty = true; 1736 } 1737 1738 output[i] = name[i].toLower(caseMapping); 1739 } 1740 else if (dirty) 1741 { 1742 output[i] = name[i]; 1743 } 1744 } 1745 1746 return dirty ? cast(string)output : name; 1747 } 1748 1749 /// 1750 @safe unittest 1751 { 1752 IRCServer.CaseMapping m = IRCServer.CaseMapping.rfc1459; 1753 1754 { 1755 immutable before = "ABCDEF"; 1756 immutable lowercase = toLowerCase(before, m); 1757 assert((lowercase == "abcdef"), lowercase); 1758 } 1759 { 1760 immutable before = "123"; 1761 immutable lowercase = toLowerCase(before, m); 1762 assert((lowercase == "123"), lowercase); 1763 assert(before is lowercase); 1764 assert(before.ptr == lowercase.ptr); 1765 } 1766 { 1767 immutable lowercase = toLowerCase("^[0v0]^", m); 1768 assert((lowercase == "~{0v0}~"), lowercase); 1769 } 1770 { 1771 immutable lowercase = toLowerCase(`A|\|`, m); 1772 assert((lowercase == "a|||"), lowercase); 1773 } 1774 1775 m = IRCServer.caseMapping.ascii; 1776 1777 { 1778 immutable before = "^[0v0]^"; 1779 immutable lowercase = toLowerCase(before, m); 1780 assert((lowercase == "^[0v0]^"), lowercase); 1781 assert(before is lowercase); 1782 assert(before.ptr == lowercase.ptr); 1783 } 1784 { 1785 immutable lowercase = toLowerCase(`A|\|`, m); 1786 assert((lowercase == `a|\|`), lowercase); 1787 } 1788 1789 m = IRCServer.CaseMapping.strict_rfc1459; 1790 1791 { 1792 immutable lowercase = toLowerCase("^[0v0]^", m); 1793 assert((lowercase == "^{0v0}^"), lowercase); 1794 } 1795 } 1796 1797 1798 // opEqualsCaseInsensitive 1799 /++ 1800 Compares two strings to see if they match if case is ignored. 1801 1802 Only works with ASCII. 1803 1804 Params: 1805 lhs = Left-hand side of the comparison. 1806 rhs = Right-hand side of the comparison. 1807 mapping = The server case mapping to apply. 1808 1809 Returns: 1810 `true` if `lhs` and `rhs` are deemed to be case-insensitively equal; 1811 `false` if not. 1812 +/ 1813 auto opEqualsCaseInsensitive( 1814 const string lhs, 1815 const string rhs, 1816 const IRCServer.CaseMapping mapping) pure @safe nothrow @nogc 1817 { 1818 if (lhs.length != rhs.length) return false; 1819 if (lhs is rhs) return true; 1820 1821 foreach (immutable i; 0..lhs.length) 1822 { 1823 immutable c = lhs[i]; 1824 immutable rc = rhs[i]; 1825 1826 if (c == rc) continue; 1827 1828 with (IRCServer.CaseMapping) 1829 switch (c) 1830 { 1831 case 'A': 1832 .. 1833 case 'Z': 1834 if (rc == c+32) continue; 1835 return false; 1836 1837 case 'a': 1838 .. 1839 case 'z': 1840 if (rc == c-32) continue; 1841 return false; 1842 1843 case '[': 1844 if (((mapping == rfc1459) || (mapping == strict_rfc1459)) && 1845 (rc == '{')) 1846 { 1847 continue; 1848 } 1849 return false; 1850 1851 case ']': 1852 if (((mapping == rfc1459) || (mapping == strict_rfc1459)) && 1853 (rc == '}')) 1854 { 1855 continue; 1856 } 1857 return false; 1858 1859 case '\\': 1860 if (((mapping == rfc1459) || (mapping == strict_rfc1459)) && 1861 (rc == '|')) 1862 { 1863 continue; 1864 } 1865 return false; 1866 1867 case '^': 1868 if ((mapping == rfc1459) && (rc == '~')) continue; 1869 return false; 1870 1871 default: 1872 return false; 1873 } 1874 } 1875 1876 return true; 1877 } 1878 1879 /// 1880 @safe unittest 1881 { 1882 immutable c = IRCServer.CaseMapping.rfc1459; 1883 1884 assert("joe".opEqualsCaseInsensitive("JOE", c)); 1885 assert("joe".opEqualsCaseInsensitive("joe", c)); 1886 assert(!"joe".opEqualsCaseInsensitive("Bengt", c)); 1887 assert(!"joe".opEqualsCaseInsensitive("", c)); 1888 assert("^o^".opEqualsCaseInsensitive("~o~", c)); 1889 assert("[derp]FACE".opEqualsCaseInsensitive("{DERP]face", c)); 1890 assert("C:\\".opEqualsCaseInsensitive("c:|", c)); 1891 } 1892 1893 1894 // isValidHostmask 1895 /++ 1896 Makes a cursory verification of a hostmask, ensuring that it doesn't contain 1897 invalid characters. May very well have false positives. 1898 1899 Params: 1900 hostmask = Hostmask string to examine. 1901 server = The current [dialect.defs.IRCServer|IRCServer] with its 1902 [dialect.defs.IRCServer.CaseMapping|IRCServer.CaseMapping]. 1903 1904 Returns: 1905 `true` if the hostmask seems to be valid, `false` if it obviously is not. 1906 +/ 1907 auto isValidHostmask( 1908 const string hostmask, 1909 const IRCServer server) pure @safe nothrow @nogc 1910 { 1911 import std.string : indexOf, representation; 1912 import std.typecons : Flag, No, Yes; 1913 1914 string slice = hostmask; // mutable 1915 1916 static bool isValidIdentOrAddressCharacter(const char c, const Flag!"address" address) 1917 { 1918 switch (c) 1919 { 1920 case 'A': 1921 .. 1922 case 'Z': 1923 case 'a': 1924 .. 1925 case 'z': 1926 case '0': 1927 .. 1928 case '9': 1929 case '-': 1930 case '_': 1931 case '*': 1932 break; 1933 1934 case ':': 1935 case '.': 1936 if (address) 1937 { 1938 break; 1939 } 1940 else 1941 { 1942 goto default; 1943 } 1944 1945 default: 1946 return false; 1947 } 1948 1949 return true; 1950 } 1951 1952 static bool isValidIdent(const string ident) 1953 { 1954 import std.string : representation; 1955 1956 if (!ident.length) return false; 1957 1958 foreach (immutable c; ident.representation) 1959 { 1960 if (!isValidIdentOrAddressCharacter(c, No.address)) return false; 1961 } 1962 1963 return true; 1964 } 1965 1966 static bool isValidAddress(const string address) 1967 { 1968 import std.string : representation; 1969 1970 if (!address.length) return false; 1971 1972 foreach (immutable c; address.representation) 1973 { 1974 if (!isValidIdentOrAddressCharacter(c, Yes.address)) return false; 1975 } 1976 1977 return true; 1978 } 1979 1980 static bool isValidNicknameGlob(const string nickname, const IRCServer server) 1981 { 1982 import std.string : representation; 1983 1984 if (nickname.length > server.maxNickLength) 1985 { 1986 return false; 1987 } 1988 1989 foreach (immutable c; nickname.representation) 1990 { 1991 if (!c.isValidNicknameCharacter && (c != '*')) return false; 1992 } 1993 1994 return true; 1995 } 1996 1997 immutable bangPos = slice.indexOf('!'); 1998 if (bangPos == -1) return false; 1999 immutable nickname = slice[0..bangPos]; 2000 if (!isValidNicknameGlob(nickname, server)) return false; 2001 slice = slice[bangPos+1..$]; 2002 if (!slice.length) return false; 2003 2004 if (slice[0] == '~') slice = slice[1..$]; 2005 immutable atPos = slice.indexOf('@'); 2006 if (atPos == -1) return false; 2007 immutable ident = slice[0..atPos]; 2008 if ((ident != "*") && !isValidIdent(ident)) return false; 2009 slice = slice[atPos+1..$]; 2010 2011 immutable address = slice; 2012 if (!address.length) return false; 2013 return (address == "*") || isValidAddress(address); 2014 } 2015 2016 /// 2017 @safe unittest 2018 { 2019 IRCServer server; 2020 2021 { 2022 immutable hostmask = "*!*@*"; 2023 assert(hostmask.isValidHostmask(server)); 2024 } 2025 { 2026 immutable hostmask = "nick123`!*@*"; 2027 assert(hostmask.isValidHostmask(server)); 2028 } 2029 { 2030 immutable hostmask = "*!~ident0-9_@*"; 2031 assert(hostmask.isValidHostmask(server)); 2032 } 2033 { 2034 immutable hostmask = "*!ident0-9_@*"; 2035 assert(hostmask.isValidHostmask(server)); 2036 } 2037 { 2038 immutable hostmask = "*!~~ident0-9_@*"; 2039 assert(!hostmask.isValidHostmask(server)); 2040 } 2041 { 2042 immutable hostmask = "*!*@address.tld.net"; 2043 assert(hostmask.isValidHostmask(server)); 2044 } 2045 { 2046 immutable hostmask = "*!*@~address.tld.net"; 2047 assert(!hostmask.isValidHostmask(server)); 2048 } 2049 { 2050 immutable hostmask = "*!*@2001::ff:09:ff"; 2051 assert(hostmask.isValidHostmask(server)); 2052 } 2053 { 2054 immutable hostmask = "kameloso!~kameloso@2001*"; 2055 assert(hostmask.isValidHostmask(server)); 2056 } 2057 { 2058 immutable hostmask = "harbl*!~dolmen@*"; 2059 assert(hostmask.isValidHostmask(server)); 2060 } 2061 }