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