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 }