···11+## Ignore Visual Studio temporary files, build results, and
22+## files generated by popular Visual Studio add-ons.
33+##
44+## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
55+66+# User-specific files
77+*.rsuser
88+*.suo
99+*.user
1010+*.userosscache
1111+*.sln.docstates
1212+1313+# User-specific files (MonoDevelop/Xamarin Studio)
1414+*.userprefs
1515+1616+# Mono auto generated files
1717+mono_crash.*
1818+1919+# Build results
2020+[Dd]ebug/
2121+[Dd]ebugPublic/
2222+[Rr]elease/
2323+[Rr]eleases/
2424+x64/
2525+x86/
2626+[Ww][Ii][Nn]32/
2727+[Aa][Rr][Mm]/
2828+[Aa][Rr][Mm]64/
2929+bld/
3030+[Bb]in/
3131+[Oo]bj/
3232+[Ll]og/
3333+[Ll]ogs/
3434+3535+# Visual Studio 2015/2017 cache/options directory
3636+.vs/
3737+# Uncomment if you have tasks that create the project's static files in wwwroot
3838+#wwwroot/
3939+4040+# Visual Studio 2017 auto generated files
4141+Generated\ Files/
4242+4343+# MSTest test Results
4444+[Tt]est[Rr]esult*/
4545+[Bb]uild[Ll]og.*
4646+4747+# NUnit
4848+*.VisualState.xml
4949+TestResult.xml
5050+nunit-*.xml
5151+5252+# Build Results of an ATL Project
5353+[Dd]ebugPS/
5454+[Rr]eleasePS/
5555+dlldata.c
5656+5757+# Benchmark Results
5858+BenchmarkDotNet.Artifacts/
5959+6060+# .NET Core
6161+project.lock.json
6262+project.fragment.lock.json
6363+artifacts/
6464+6565+# ASP.NET Scaffolding
6666+ScaffoldingReadMe.txt
6767+6868+# StyleCop
6969+StyleCopReport.xml
7070+7171+# Files built by Visual Studio
7272+*_i.c
7373+*_p.c
7474+*_h.h
7575+*.ilk
7676+*.meta
7777+*.obj
7878+*.iobj
7979+*.pch
8080+*.pdb
8181+*.ipdb
8282+*.pgc
8383+*.pgd
8484+*.rsp
8585+*.sbr
8686+*.tlb
8787+*.tli
8888+*.tlh
8989+*.tmp
9090+*.tmp_proj
9191+*_wpftmp.csproj
9292+*.log
9393+*.vspscc
9494+*.vssscc
9595+.builds
9696+*.pidb
9797+*.svclog
9898+*.scc
9999+100100+# Chutzpah Test files
101101+_Chutzpah*
102102+103103+# Visual C++ cache files
104104+ipch/
105105+*.aps
106106+*.ncb
107107+*.opendb
108108+*.opensdf
109109+*.sdf
110110+*.cachefile
111111+*.VC.db
112112+*.VC.VC.opendb
113113+114114+# Visual Studio profiler
115115+*.psess
116116+*.vsp
117117+*.vspx
118118+*.sap
119119+120120+# Visual Studio Trace Files
121121+*.e2e
122122+123123+# TFS 2012 Local Workspace
124124+$tf/
125125+126126+# Guidance Automation Toolkit
127127+*.gpState
128128+129129+# ReSharper is a .NET coding add-in
130130+_ReSharper*/
131131+*.[Rr]e[Ss]harper
132132+*.DotSettings.user
133133+134134+# TeamCity is a build add-in
135135+_TeamCity*
136136+137137+# DotCover is a Code Coverage Tool
138138+*.dotCover
139139+140140+# AxoCover is a Code Coverage Tool
141141+.axoCover/*
142142+!.axoCover/settings.json
143143+144144+# Coverlet is a free, cross platform Code Coverage Tool
145145+coverage*[.json, .xml, .info]
146146+147147+# Visual Studio code coverage results
148148+*.coverage
149149+*.coveragexml
150150+151151+# NCrunch
152152+_NCrunch_*
153153+.*crunch*.local.xml
154154+nCrunchTemp_*
155155+156156+# MightyMoose
157157+*.mm.*
158158+AutoTest.Net/
159159+160160+# Web workbench (sass)
161161+.sass-cache/
162162+163163+# Installshield output folder
164164+[Ee]xpress/
165165+166166+# DocProject is a documentation generator add-in
167167+DocProject/buildhelp/
168168+DocProject/Help/*.HxT
169169+DocProject/Help/*.HxC
170170+DocProject/Help/*.hhc
171171+DocProject/Help/*.hhk
172172+DocProject/Help/*.hhp
173173+DocProject/Help/Html2
174174+DocProject/Help/html
175175+176176+# Click-Once directory
177177+publish/
178178+179179+# Publish Web Output
180180+*.[Pp]ublish.xml
181181+*.azurePubxml
182182+# Note: Comment the next line if you want to checkin your web deploy settings,
183183+# but database connection strings (with potential passwords) will be unencrypted
184184+*.pubxml
185185+*.publishproj
186186+187187+# Microsoft Azure Web App publish settings. Comment the next line if you want to
188188+# checkin your Azure Web App publish settings, but sensitive information contained
189189+# in these scripts will be unencrypted
190190+PublishScripts/
191191+192192+# NuGet Packages
193193+*.nupkg
194194+# NuGet Symbol Packages
195195+*.snupkg
196196+# The packages folder can be ignored because of Package Restore
197197+**/[Pp]ackages/*
198198+# except build/, which is used as an MSBuild target.
199199+!**/[Pp]ackages/build/
200200+# Uncomment if necessary however generally it will be regenerated when needed
201201+#!**/[Pp]ackages/repositories.config
202202+# NuGet v3's project.json files produces more ignorable files
203203+*.nuget.props
204204+*.nuget.targets
205205+206206+# Microsoft Azure Build Output
207207+csx/
208208+*.build.csdef
209209+210210+# Microsoft Azure Emulator
211211+ecf/
212212+rcf/
213213+214214+# Windows Store app package directories and files
215215+AppPackages/
216216+BundleArtifacts/
217217+Package.StoreAssociation.xml
218218+_pkginfo.txt
219219+*.appx
220220+*.appxbundle
221221+*.appxupload
222222+223223+# Visual Studio cache files
224224+# files ending in .cache can be ignored
225225+*.[Cc]ache
226226+# but keep track of directories ending in .cache
227227+!?*.[Cc]ache/
228228+229229+# Others
230230+ClientBin/
231231+~$*
232232+*~
233233+*.dbmdl
234234+*.dbproj.schemaview
235235+*.jfm
236236+*.pfx
237237+*.publishsettings
238238+orleans.codegen.cs
239239+240240+# Including strong name files can present a security risk
241241+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
242242+#*.snk
243243+244244+# Since there are multiple workflows, uncomment next line to ignore bower_components
245245+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
246246+#bower_components/
247247+248248+# RIA/Silverlight projects
249249+Generated_Code/
250250+251251+# Backup & report files from converting an old project file
252252+# to a newer Visual Studio version. Backup files are not needed,
253253+# because we have git ;-)
254254+_UpgradeReport_Files/
255255+Backup*/
256256+UpgradeLog*.XML
257257+UpgradeLog*.htm
258258+ServiceFabricBackup/
259259+*.rptproj.bak
260260+261261+# SQL Server files
262262+*.mdf
263263+*.ldf
264264+*.ndf
265265+266266+# Business Intelligence projects
267267+*.rdl.data
268268+*.bim.layout
269269+*.bim_*.settings
270270+*.rptproj.rsuser
271271+*- [Bb]ackup.rdl
272272+*- [Bb]ackup ([0-9]).rdl
273273+*- [Bb]ackup ([0-9][0-9]).rdl
274274+275275+# Microsoft Fakes
276276+FakesAssemblies/
277277+278278+# GhostDoc plugin setting file
279279+*.GhostDoc.xml
280280+281281+# Node.js Tools for Visual Studio
282282+.ntvs_analysis.dat
283283+node_modules/
284284+285285+# Visual Studio 6 build log
286286+*.plg
287287+288288+# Visual Studio 6 workspace options file
289289+*.opt
290290+291291+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
292292+*.vbw
293293+294294+# Visual Studio LightSwitch build output
295295+**/*.HTMLClient/GeneratedArtifacts
296296+**/*.DesktopClient/GeneratedArtifacts
297297+**/*.DesktopClient/ModelManifest.xml
298298+**/*.Server/GeneratedArtifacts
299299+**/*.Server/ModelManifest.xml
300300+_Pvt_Extensions
301301+302302+# Paket dependency manager
303303+.paket/paket.exe
304304+paket-files/
305305+306306+# FAKE - F# Make
307307+.fake/
308308+309309+# CodeRush personal settings
310310+.cr/personal
311311+312312+# Python Tools for Visual Studio (PTVS)
313313+__pycache__/
314314+*.pyc
315315+316316+# Cake - Uncomment if you are using it
317317+# tools/**
318318+# !tools/packages.config
319319+320320+# Tabs Studio
321321+*.tss
322322+323323+# Telerik's JustMock configuration file
324324+*.jmconfig
325325+326326+# BizTalk build output
327327+*.btp.cs
328328+*.btm.cs
329329+*.odx.cs
330330+*.xsd.cs
331331+332332+# OpenCover UI analysis results
333333+OpenCover/
334334+335335+# Azure Stream Analytics local run output
336336+ASALocalRun/
337337+338338+# MSBuild Binary and Structured Log
339339+*.binlog
340340+341341+# NVidia Nsight GPU debugger configuration file
342342+*.nvuser
343343+344344+# MFractors (Xamarin productivity tool) working folder
345345+.mfractor/
346346+347347+# Local History for Visual Studio
348348+.localhistory/
349349+350350+# BeatPulse healthcheck temp database
351351+healthchecksdb
352352+353353+# Backup folder for Package Reference Convert tool in Visual Studio 2017
354354+MigrationBackup/
355355+356356+# Ionide (cross platform F# VS Code tools) working folder
357357+.ionide/
358358+
+25
IrcTokens.sln
···11+
22+Microsoft Visual Studio Solution File, Format Version 12.00
33+# Visual Studio Version 16
44+VisualStudioVersion = 16.0.30011.22
55+MinimumVisualStudioVersion = 10.0.40219.1
66+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IrcTokens", "IrcTokens\IrcTokens.csproj", "{9E812F45-B2CD-42D2-8378-EBEBF8697905}"
77+EndProject
88+Global
99+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
1010+ Debug|Any CPU = Debug|Any CPU
1111+ Release|Any CPU = Release|Any CPU
1212+ EndGlobalSection
1313+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
1414+ {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
1515+ {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Debug|Any CPU.Build.0 = Debug|Any CPU
1616+ {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Release|Any CPU.ActiveCfg = Release|Any CPU
1717+ {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Release|Any CPU.Build.0 = Release|Any CPU
1818+ EndGlobalSection
1919+ GlobalSection(SolutionProperties) = preSolution
2020+ HideSolutionNode = FALSE
2121+ EndGlobalSection
2222+ GlobalSection(ExtensibilityGlobals) = postSolution
2323+ SolutionGuid = {0B91F0EA-8564-4318-8EEC-ED0640475141}
2424+ EndGlobalSection
2525+EndGlobal
+42
IrcTokens/Hostmask.cs
···11+namespace IrcTokens
22+{
33+ /// <summary>
44+ /// Represents the three parts of a hostmask. Parse with the constructor.
55+ /// </summary>
66+ public class Hostmask
77+ {
88+ public string NickName { get; set; }
99+ public string UserName { get; set; }
1010+ public string HostName { get; set; }
1111+1212+ public override string ToString() => _source;
1313+1414+ private readonly string _source;
1515+1616+ public Hostmask(string source)
1717+ {
1818+ if (source == null) return;
1919+2020+ _source = source;
2121+2222+ if (source.Contains('@'))
2323+ {
2424+ var split = source.Split('@');
2525+2626+ NickName = split[0];
2727+ HostName = split[1];
2828+ }
2929+ else
3030+ {
3131+ NickName = source;
3232+ }
3333+3434+ if (NickName.Contains('!'))
3535+ {
3636+ var userSplit = NickName.Split('!');
3737+ NickName = userSplit[0];
3838+ UserName = userSplit[1];
3939+ }
4040+ }
4141+ }
4242+}
···11+using System;
22+using System.Collections.Generic;
33+using System.Linq;
44+55+namespace IrcTokens
66+{
77+ /// <summary>
88+ /// Tools to represent, parse, and format IRC lines
99+ /// </summary>
1010+ public class Line
1111+ {
1212+ public Dictionary<string, string> Tags { get; set; }
1313+ public string Source { get; set; }
1414+ public string Command { get; set; }
1515+ public List<string> Params { get; set; }
1616+1717+ private Hostmask _hostmask;
1818+ private string _rawLine;
1919+2020+ public override string ToString() =>
2121+ $"Line(tags={string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}, params={string.Join(",", Params)})";
2222+2323+ public Hostmask Hostmask =>
2424+ _hostmask ??= new Hostmask(Source);
2525+2626+ public Line() { }
2727+2828+ /// <summary>
2929+ /// Build new <see cref="Line"/> object parsed from <param name="line">a string</param>. Analogous to irctokens.tokenise()
3030+ /// </summary>
3131+ /// <param name="line">irc line to parse</param>
3232+ public Line(string line)
3333+ {
3434+ _rawLine = line;
3535+ string[] split;
3636+3737+ if (line.StartsWith('@'))
3838+ {
3939+ Tags = new Dictionary<string, string>();
4040+4141+ split = line.Split(" ");
4242+ var messageTags = split[0];
4343+ line = string.Join(" ", split.Skip(1));
4444+4545+ foreach (var part in messageTags.Substring(1).Split(';'))
4646+ {
4747+ if (part.Contains('='))
4848+ {
4949+ split = part.Split('=');
5050+ Tags[split[0]] = Protocol.UnescapeTag(split[1]);
5151+ }
5252+ else
5353+ {
5454+ Tags[part] = string.Empty;
5555+ }
5656+ }
5757+ }
5858+5959+ string trailing;
6060+ if (line.Contains(" :"))
6161+ {
6262+ split = line.Split(" :");
6363+ line = split[0];
6464+ trailing = string.Join(" :", split.Skip(1));
6565+ }
6666+ else
6767+ {
6868+ trailing = null;
6969+ }
7070+7171+ Params = line.Contains(' ')
7272+ ? line.Split(' ').Where(p => !string.IsNullOrWhiteSpace(p)).ToList()
7373+ : new List<string> {line};
7474+7575+ if (Params[0].StartsWith(':'))
7676+ {
7777+ Source = Params[0].Substring(1);
7878+ Params.RemoveAt(0);
7979+ }
8080+8181+ if (Params.Count > 0)
8282+ {
8383+ Command = Params[0].ToUpper();
8484+ Params.RemoveAt(0);
8585+ }
8686+8787+ if (trailing != null)
8888+ {
8989+ Params.Add(trailing);
9090+ }
9191+ }
9292+9393+ /// <summary>
9494+ /// Format a <see cref="Line"/> as a standards-compliant IRC line
9595+ /// </summary>
9696+ /// <returns>formatted irc line</returns>
9797+ public string Format()
9898+ {
9999+ var outs = new List<string>();
100100+101101+ if (Tags != null && Tags.Any())
102102+ {
103103+ var tags = Tags.Keys
104104+ .Select(key => string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={Protocol.EscapeTag(Tags[key])}")
105105+ .ToList();
106106+107107+ outs.Add($"@{string.Join(";", tags)}");
108108+ }
109109+110110+ if (Source != null)
111111+ {
112112+ outs.Add($":{Source}");
113113+ }
114114+115115+ outs.Add(Command);
116116+117117+ if (Params != null && Params.Any())
118118+ {
119119+ var last = Params[^1];
120120+ Params.RemoveAt(Params.Count - 1);
121121+122122+ foreach (var p in Params)
123123+ {
124124+ if (p.Contains(' '))
125125+ throw new ArgumentException("non-last parameters cannot have spaces");
126126+ if (p.StartsWith(':'))
127127+ throw new ArgumentException("non-last parameters cannot start with colon");
128128+ }
129129+ outs.AddRange(Params);
130130+131131+ if (last == null || string.IsNullOrWhiteSpace(last) || last.Contains(' ') || last.StartsWith(':'))
132132+ last = $":{last}";
133133+ outs.Add(last);
134134+ }
135135+136136+ return string.Join(" ", outs);
137137+ }
138138+ }
139139+}
+74
IrcTokens/Protocol.cs
···11+using System;
22+using System.Globalization;
33+using System.Linq;
44+using System.Text;
55+66+namespace IrcTokens
77+{
88+ public class Protocol
99+ {
1010+ private static readonly string[] TagUnescaped = new []
1111+ {
1212+ "\\", " ", ";", "\r", "\n"
1313+ };
1414+1515+ private static readonly string[] TagEscaped = new []
1616+ {
1717+ "\\\\", "\\s", "\\:", "\\r", "\\n"
1818+ };
1919+2020+ /// <summary>
2121+ /// Unescape ircv3 tag
2222+ /// </summary>
2323+ /// <param name="val">escaped string</param>
2424+ /// <returns>unescaped string</returns>
2525+ public static string UnescapeTag(string val)
2626+ {
2727+ var unescaped = new StringBuilder();
2828+2929+ var graphemeIterator = StringInfo.GetTextElementEnumerator(val);
3030+ graphemeIterator.Reset();
3131+3232+ while (graphemeIterator.MoveNext())
3333+ {
3434+ var current = graphemeIterator.GetTextElement();
3535+3636+ if (current == @"\")
3737+ {
3838+ try
3939+ {
4040+ graphemeIterator.MoveNext();
4141+ var next = graphemeIterator.GetTextElement();
4242+ var pair = current + next;
4343+ unescaped.Append(TagEscaped.Contains(pair)
4444+ ? TagUnescaped[Array.IndexOf(TagEscaped, pair)]
4545+ : next);
4646+ }
4747+ catch (InvalidOperationException)
4848+ {
4949+ // ignored
5050+ }
5151+ }
5252+ else
5353+ unescaped.Append(current);
5454+ }
5555+5656+ return unescaped.ToString();
5757+ }
5858+5959+ /// <summary>
6060+ /// Escape strings for use in ircv3 tags
6161+ /// </summary>
6262+ /// <param name="val">string to escape</param>
6363+ /// <returns>escaped string</returns>
6464+ public static string EscapeTag(string val)
6565+ {
6666+ for (var i = 0; i < TagUnescaped.Length; ++i)
6767+ {
6868+ val = val.Replace(TagUnescaped[i], TagEscaped[i]);
6969+ }
7070+7171+ return val;
7272+ }
7373+ }
7474+}
+31
IrcTokens/Tests/Data/JoinModel.cs
···11+using System.Collections.Generic;
22+using YamlDotNet.Serialization;
33+44+namespace IrcTokens.Tests.Data
55+{
66+ public class JoinModel
77+ {
88+ public List<Test> Tests { get; set; }
99+1010+ public class Test
1111+ {
1212+ [YamlMember(Alias = "desc")]
1313+ public string Description { get; set; }
1414+1515+ public Atoms Atoms { get; set; }
1616+1717+ public List<string> Matches { get; set; }
1818+ }
1919+2020+ public class Atoms
2121+ {
2222+ public Dictionary<string, string> Tags { get; set; }
2323+2424+ public string Source { get; set; }
2525+2626+ public string Verb { get; set; }
2727+2828+ public List<string> Params { get; set; }
2929+ }
3030+ }
3131+}
+15
IrcTokens/Tests/Data/SplitModel.cs
···11+using System.Collections.Generic;
22+33+namespace IrcTokens.Tests.Data
44+{
55+ public class SplitModel
66+ {
77+ public List<Test> Tests { get; set; }
88+99+ public class Test
1010+ {
1111+ public string Input { get; set; }
1212+ public JoinModel.Atoms Atoms { get; set; }
1313+ }
1414+ }
1515+}
+221
IrcTokens/Tests/Data/msg-join.yaml
···11+# IRC parser tests
22+# joining atoms into sendable messages
33+44+# Written in 2015 by Daniel Oaks <daniel@danieloaks.net>
55+#
66+# To the extent possible under law, the author(s) have dedicated all copyright
77+# and related and neighboring rights to this software to the public domain
88+# worldwide. This software is distributed without any warranty.
99+#
1010+# You should have received a copy of the CC0 Public Domain Dedication along
1111+# with this software. If not, see
1212+# <http://creativecommons.org/publicdomain/zero/1.0/>.
1313+1414+# some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed
1515+# https://github.com/grawity/code/tree/master/lib/tests
1616+# some of the tests here originate from Mozilla's test vectors, which is public domain
1717+# https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js
1818+# some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here
1919+# https://github.com/SaberUK/ircparser/tree/master/test
2020+2121+tests:
2222+ # the desc string holds a description of the test, if it exists
2323+2424+ # the atoms dict has the keys:
2525+ # * tags: tags dict
2626+ # tags with no value are an empty string
2727+ # * source: source string, without single leading colon
2828+ # * verb: verb string
2929+ # * params: params split up as a list
3030+ # if the params key does not exist, assume it is empty
3131+ # if any other keys do no exist, assume they are null
3232+ # a key that is null does not exist or is not specified with the
3333+ # given input string
3434+3535+ # matches is a list of messages that match
3636+3737+ # simple tests
3838+ - desc: Simple test with verb and params.
3939+ atoms:
4040+ verb: "foo"
4141+ params:
4242+ - "bar"
4343+ - "baz"
4444+ - "asdf"
4545+ matches:
4646+ - "foo bar baz asdf"
4747+ - "foo bar baz :asdf"
4848+4949+ # with no regular params
5050+ - desc: Simple test with source and no params.
5151+ atoms:
5252+ source: "src"
5353+ verb: "AWAY"
5454+ matches:
5555+ - ":src AWAY"
5656+5757+ - desc: Simple test with source and empty trailing param.
5858+ atoms:
5959+ source: "src"
6060+ verb: "AWAY"
6161+ params:
6262+ - ""
6363+ matches:
6464+ - ":src AWAY :"
6565+6666+ # with source
6767+ - desc: Simple test with source.
6868+ atoms:
6969+ source: "coolguy"
7070+ verb: "foo"
7171+ params:
7272+ - "bar"
7373+ - "baz"
7474+ - "asdf"
7575+ matches:
7676+ - ":coolguy foo bar baz asdf"
7777+ - ":coolguy foo bar baz :asdf"
7878+7979+ # with trailing param
8080+ - desc: Simple test with trailing param.
8181+ atoms:
8282+ verb: "foo"
8383+ params:
8484+ - "bar"
8585+ - "baz"
8686+ - "asdf quux"
8787+ matches:
8888+ - "foo bar baz :asdf quux"
8989+9090+ - desc: Simple test with empty trailing param.
9191+ atoms:
9292+ verb: "foo"
9393+ params:
9494+ - "bar"
9595+ - "baz"
9696+ - ""
9797+ matches:
9898+ - "foo bar baz :"
9999+100100+ - desc: Simple test with trailing param containing colon.
101101+ atoms:
102102+ verb: "foo"
103103+ params:
104104+ - "bar"
105105+ - "baz"
106106+ - ":asdf"
107107+ matches:
108108+ - "foo bar baz ::asdf"
109109+110110+ # with source and trailing param
111111+ - desc: Test with source and trailing param.
112112+ atoms:
113113+ source: "coolguy"
114114+ verb: "foo"
115115+ params:
116116+ - "bar"
117117+ - "baz"
118118+ - "asdf quux"
119119+ matches:
120120+ - ":coolguy foo bar baz :asdf quux"
121121+122122+ - desc: Test with trailing containing beginning+end whitespace.
123123+ atoms:
124124+ source: "coolguy"
125125+ verb: "foo"
126126+ params:
127127+ - "bar"
128128+ - "baz"
129129+ - " asdf quux "
130130+ matches:
131131+ - ":coolguy foo bar baz : asdf quux "
132132+133133+ - desc: Test with trailing containing what looks like another trailing param.
134134+ atoms:
135135+ source: "coolguy"
136136+ verb: "PRIVMSG"
137137+ params:
138138+ - "bar"
139139+ - "lol :) "
140140+ matches:
141141+ - ":coolguy PRIVMSG bar :lol :) "
142142+143143+ - desc: Simple test with source and empty trailing.
144144+ atoms:
145145+ source: "coolguy"
146146+ verb: "foo"
147147+ params:
148148+ - "bar"
149149+ - "baz"
150150+ - ""
151151+ matches:
152152+ - ":coolguy foo bar baz :"
153153+154154+ - desc: Trailing contains only spaces.
155155+ atoms:
156156+ source: "coolguy"
157157+ verb: "foo"
158158+ params:
159159+ - "bar"
160160+ - "baz"
161161+ - " "
162162+ matches:
163163+ - ":coolguy foo bar baz : "
164164+165165+ - desc: Param containing tab (tab is not considered SPACE for message splitting).
166166+ atoms:
167167+ source: "coolguy"
168168+ verb: "foo"
169169+ params:
170170+ - "b\tar"
171171+ - "baz"
172172+ matches:
173173+ - ":coolguy foo b\tar baz"
174174+ - ":coolguy foo b\tar :baz"
175175+176176+ # with tags
177177+ - desc: Tag with no value and space-filled trailing.
178178+ atoms:
179179+ tags:
180180+ "asd": ""
181181+ source: "coolguy"
182182+ verb: "foo"
183183+ params:
184184+ - "bar"
185185+ - "baz"
186186+ - " "
187187+ matches:
188188+ - "@asd :coolguy foo bar baz : "
189189+190190+ - desc: Tags with escaped values.
191191+ atoms:
192192+ verb: "foo"
193193+ tags:
194194+ "a": "b\\and\nk"
195195+ "d": "gh;764"
196196+ matches:
197197+ - "@a=b\\\\and\\nk;d=gh\\:764 foo"
198198+ - "@d=gh\\:764;a=b\\\\and\\nk foo"
199199+200200+ - desc: Tags with escaped values and params.
201201+ atoms:
202202+ verb: "foo"
203203+ tags:
204204+ "a": "b\\and\nk"
205205+ "d": "gh;764"
206206+ params:
207207+ - "par1"
208208+ - "par2"
209209+ matches:
210210+ - "@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2"
211211+ - "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2"
212212+ - "@d=gh\\:764;a=b\\\\and\\nk foo par1 par2"
213213+ - "@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2"
214214+215215+ - desc: Tag with long, strange values (including LF and newline).
216216+ atoms:
217217+ tags:
218218+ foo: "\\\\;\\s \r\n"
219219+ verb: "COMMAND"
220220+ matches:
221221+ - "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
+343
IrcTokens/Tests/Data/msg-split.yaml
···11+# IRC parser tests
22+# splitting messages into usable atoms
33+44+# Written in 2015 by Daniel Oaks <daniel@danieloaks.net>
55+#
66+# To the extent possible under law, the author(s) have dedicated all copyright
77+# and related and neighboring rights to this software to the public domain
88+# worldwide. This software is distributed without any warranty.
99+#
1010+# You should have received a copy of the CC0 Public Domain Dedication along
1111+# with this software. If not, see
1212+# <http://creativecommons.org/publicdomain/zero/1.0/>.
1313+1414+# some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed
1515+# https://github.com/grawity/code/tree/master/lib/tests
1616+# some of the tests here originate from Mozilla's test vectors, which is public domain
1717+# https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js
1818+# some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here
1919+# https://github.com/SaberUK/ircparser/tree/master/test
2020+2121+# we follow RFC1459 with regards to multiple ascii spaces splitting atoms:
2222+# The prefix, command, and all parameters are
2323+# separated by one (or more) ASCII space character(s) (0x20).
2424+# because doing it as RFC2812 says (strictly as a single ascii space) isn't sane
2525+2626+tests:
2727+ # input is the string coming directly from the server to parse
2828+2929+ # the atoms dict has the keys:
3030+ # * tags: tags dict
3131+ # tags with no value are an empty string
3232+ # * source: source string, without single leading colon
3333+ # * verb: verb string
3434+ # * params: params split up as a list
3535+ # if the params key does not exist, assume it is empty
3636+ # if any other keys do no exist, assume they are null
3737+ # a key that is null does not exist or is not specified with the
3838+ # given input string
3939+4040+ # simple
4141+ - input: "foo bar baz asdf"
4242+ atoms:
4343+ verb: "foo"
4444+ params:
4545+ - "bar"
4646+ - "baz"
4747+ - "asdf"
4848+4949+ # with source
5050+ - input: ":coolguy foo bar baz asdf"
5151+ atoms:
5252+ source: "coolguy"
5353+ verb: "foo"
5454+ params:
5555+ - "bar"
5656+ - "baz"
5757+ - "asdf"
5858+5959+ # with trailing param
6060+ - input: "foo bar baz :asdf quux"
6161+ atoms:
6262+ verb: "foo"
6363+ params:
6464+ - "bar"
6565+ - "baz"
6666+ - "asdf quux"
6767+6868+ - input: "foo bar baz :"
6969+ atoms:
7070+ verb: "foo"
7171+ params:
7272+ - "bar"
7373+ - "baz"
7474+ - ""
7575+7676+ - input: "foo bar baz ::asdf"
7777+ atoms:
7878+ verb: "foo"
7979+ params:
8080+ - "bar"
8181+ - "baz"
8282+ - ":asdf"
8383+8484+ # with source and trailing param
8585+ - input: ":coolguy foo bar baz :asdf quux"
8686+ atoms:
8787+ source: "coolguy"
8888+ verb: "foo"
8989+ params:
9090+ - "bar"
9191+ - "baz"
9292+ - "asdf quux"
9393+9494+ - input: ":coolguy foo bar baz : asdf quux "
9595+ atoms:
9696+ source: "coolguy"
9797+ verb: "foo"
9898+ params:
9999+ - "bar"
100100+ - "baz"
101101+ - " asdf quux "
102102+103103+ - input: ":coolguy PRIVMSG bar :lol :) "
104104+ atoms:
105105+ source: "coolguy"
106106+ verb: "PRIVMSG"
107107+ params:
108108+ - "bar"
109109+ - "lol :) "
110110+111111+ - input: ":coolguy foo bar baz :"
112112+ atoms:
113113+ source: "coolguy"
114114+ verb: "foo"
115115+ params:
116116+ - "bar"
117117+ - "baz"
118118+ - ""
119119+120120+ - input: ":coolguy foo bar baz : "
121121+ atoms:
122122+ source: "coolguy"
123123+ verb: "foo"
124124+ params:
125125+ - "bar"
126126+ - "baz"
127127+ - " "
128128+129129+ # with tags
130130+ - input: "@a=b;c=32;k;rt=ql7 foo"
131131+ atoms:
132132+ verb: "foo"
133133+ tags:
134134+ "a": "b"
135135+ "c": "32"
136136+ "k": ""
137137+ "rt": "ql7"
138138+139139+ # with escaped tags
140140+ - input: "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo"
141141+ atoms:
142142+ verb: "foo"
143143+ tags:
144144+ "a": "b\\and\nk"
145145+ "c": "72 45"
146146+ "d": "gh;764"
147147+148148+ # with tags and source
149149+ - input: "@c;h=;a=b :quux ab cd"
150150+ atoms:
151151+ tags:
152152+ "c": ""
153153+ "h": ""
154154+ "a": "b"
155155+ source: "quux"
156156+ verb: "ab"
157157+ params:
158158+ - "cd"
159159+160160+ # different forms of last param
161161+ - input: ":src JOIN #chan"
162162+ atoms:
163163+ source: "src"
164164+ verb: "JOIN"
165165+ params:
166166+ - "#chan"
167167+168168+ - input: ":src JOIN :#chan"
169169+ atoms:
170170+ source: "src"
171171+ verb: "JOIN"
172172+ params:
173173+ - "#chan"
174174+175175+ # with and without last param
176176+ - input: ":src AWAY"
177177+ atoms:
178178+ source: "src"
179179+ verb: "AWAY"
180180+181181+ - input: ":src AWAY "
182182+ atoms:
183183+ source: "src"
184184+ verb: "AWAY"
185185+186186+ # tab is not considered <SPACE>
187187+ - input: ":cool\tguy foo bar baz"
188188+ atoms:
189189+ source: "cool\tguy"
190190+ verb: "foo"
191191+ params:
192192+ - "bar"
193193+ - "baz"
194194+195195+ # with weird control codes in the source
196196+ - input: ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz"
197197+ atoms:
198198+ source: "coolguy!ag@net\x035w\x03ork.admin"
199199+ verb: "PRIVMSG"
200200+ params:
201201+ - "foo"
202202+ - "bar baz"
203203+204204+ - input: ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz"
205205+ atoms:
206206+ source: "coolguy!~ag@n\x02et\x0305w\x0fork.admin"
207207+ verb: "PRIVMSG"
208208+ params:
209209+ - "foo"
210210+ - "bar baz"
211211+212212+ - input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3"
213213+ atoms:
214214+ tags:
215215+ tag1: "value1"
216216+ tag2: ""
217217+ vendor1/tag3: "value2"
218218+ vendor2/tag4: ""
219219+ source: "irc.example.com"
220220+ verb: "COMMAND"
221221+ params:
222222+ - "param1"
223223+ - "param2"
224224+ - "param3 param3"
225225+226226+ - input: ":irc.example.com COMMAND param1 param2 :param3 param3"
227227+ atoms:
228228+ source: "irc.example.com"
229229+ verb: "COMMAND"
230230+ params:
231231+ - "param1"
232232+ - "param2"
233233+ - "param3 param3"
234234+235235+ - input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3"
236236+ atoms:
237237+ tags:
238238+ tag1: "value1"
239239+ tag2: ""
240240+ vendor1/tag3: "value2"
241241+ vendor2/tag4: ""
242242+ verb: "COMMAND"
243243+ params:
244244+ - "param1"
245245+ - "param2"
246246+ - "param3 param3"
247247+248248+ - input: "COMMAND"
249249+ atoms:
250250+ verb: "COMMAND"
251251+252252+ # yaml encoding + slashes is fun
253253+ - input: "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
254254+ atoms:
255255+ tags:
256256+ foo: "\\\\;\\s \r\n"
257257+ verb: "COMMAND"
258258+259259+ # broken messages from unreal
260260+ - input: ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters"
261261+ atoms:
262262+ source: "gravel.mozilla.org"
263263+ verb: "432"
264264+ params:
265265+ - "#momo"
266266+ - "Erroneous Nickname: Illegal characters"
267267+268268+ - input: ":gravel.mozilla.org MODE #tckk +n "
269269+ atoms:
270270+ source: "gravel.mozilla.org"
271271+ verb: "MODE"
272272+ params:
273273+ - "#tckk"
274274+ - "+n"
275275+276276+ - input: ":services.esper.net MODE #foo-bar +o foobar "
277277+ atoms:
278278+ source: "services.esper.net"
279279+ verb: "MODE"
280280+ params:
281281+ - "#foo-bar"
282282+ - "+o"
283283+ - "foobar"
284284+285285+ # tag values should be parsed char-at-a-time to prevent wayward replacements.
286286+ - input: "@tag1=value\\\\ntest COMMAND"
287287+ atoms:
288288+ tags:
289289+ tag1: "value\\ntest"
290290+ verb: "COMMAND"
291291+292292+ # If a tag value has a slash followed by a character which doesn't need
293293+ # to be escaped, the slash should be dropped.
294294+ - input: "@tag1=value\\1 COMMAND"
295295+ atoms:
296296+ tags:
297297+ tag1: "value1"
298298+ verb: "COMMAND"
299299+300300+ # A slash at the end of a tag value should be dropped
301301+ - input: "@tag1=value1\\ COMMAND"
302302+ atoms:
303303+ tags:
304304+ tag1: "value1"
305305+ verb: "COMMAND"
306306+307307+ # Duplicate tags: Parsers SHOULD disregard all but the final occurence
308308+ - input: "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND"
309309+ atoms:
310310+ tags:
311311+ tag1: "5"
312312+ tag2: "3"
313313+ tag3: "4"
314314+ verb: "COMMAND"
315315+316316+ # vendored tags can have the same name as a non-vendored tag
317317+ - input: "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND"
318318+ atoms:
319319+ tags:
320320+ tag1: "5"
321321+ tag2: "3"
322322+ tag3: "4"
323323+ vendor/tag2: "8"
324324+ verb: "COMMAND"
325325+326326+ # Some parsers handle /MODE in a special way, make sure they do it right
327327+ - input: ":SomeOp MODE #channel :+i"
328328+ atoms:
329329+ source: "SomeOp"
330330+ verb: "MODE"
331331+ params:
332332+ - "#channel"
333333+ - "+i"
334334+335335+ - input: ":SomeOp MODE #channel +oo SomeUser :AnotherUser"
336336+ atoms:
337337+ source: "SomeOp"
338338+ verb: "MODE"
339339+ params:
340340+ - "#channel"
341341+ - "+oo"
342342+ - "SomeUser"
343343+ - "AnotherUser"
+150
IrcTokens/Tests/FormatTests.cs
···11+using Microsoft.VisualStudio.TestTools.UnitTesting;
22+using System;
33+using System.Collections.Generic;
44+55+namespace IrcTokens.Tests
66+{
77+ [TestClass]
88+ public class FormatTests
99+ {
1010+ [TestMethod]
1111+ public void TestTags()
1212+ {
1313+ var line = new Line
1414+ {
1515+ Command = "PRIVMSG",
1616+ Params = new List<string> {"#channel", "hello"},
1717+ Tags = new Dictionary<string, string> {{"id", "\\" + " " + ";" + "\r\n"}}
1818+ }.Format();
1919+2020+ Assert.AreEqual("@id=\\\\\\s\\:\\r\\n PRIVMSG #channel hello", line);
2121+ }
2222+2323+ [TestMethod]
2424+ public void TestMissingTag()
2525+ {
2626+ var line = new Line
2727+ {
2828+ Command = "PRIVMSG",
2929+ Params = new List<string> {"#channel", "hello"}
3030+ }.Format();
3131+3232+ Assert.AreEqual("PRIVMSG #channel hello", line);
3333+ }
3434+3535+ [TestMethod]
3636+ public void TestNullTag()
3737+ {
3838+ var line = new Line
3939+ {
4040+ Command = "PRIVMSG",
4141+ Params = new List<string> {"#channel", "hello"},
4242+ Tags = new Dictionary<string, string> {{"a", null}}
4343+ }.Format();
4444+4545+ Assert.AreEqual("@a PRIVMSG #channel hello", line);
4646+ }
4747+4848+ [TestMethod]
4949+ public void TestEmptyTag()
5050+ {
5151+ var line = new Line
5252+ {
5353+ Command = "PRIVMSG",
5454+ Params = new List<string> {"#channel", "hello"},
5555+ Tags = new Dictionary<string, string> {{"a", ""}}
5656+ }.Format();
5757+5858+ Assert.AreEqual("@a PRIVMSG #channel hello", line);
5959+ }
6060+6161+ [TestMethod]
6262+ public void TestSource()
6363+ {
6464+ var line = new Line
6565+ {
6666+ Command = "PRIVMSG",
6767+ Params = new List<string> {"#channel", "hello"},
6868+ Source = "nick!user@host"
6969+ }.Format();
7070+7171+ Assert.AreEqual(":nick!user@host PRIVMSG #channel hello", line);
7272+ }
7373+7474+ [TestMethod]
7575+ public void TestCommandLowercase()
7676+ {
7777+ var line = new Line {Command = "privmsg"}.Format();
7878+ Assert.AreEqual("privmsg", line);
7979+ }
8080+8181+ [TestMethod]
8282+ public void TestCommandUppercase()
8383+ {
8484+ var line = new Line {Command = "PRIVMSG"}.Format();
8585+ Assert.AreEqual("PRIVMSG", line);
8686+ }
8787+8888+ [TestMethod]
8989+ public void TestTrailingSpace()
9090+ {
9191+ var line = new Line
9292+ {
9393+ Command = "PRIVMSG",
9494+ Params = new List<string> {"#channel", "hello world"}
9595+ }.Format();
9696+9797+ Assert.AreEqual("PRIVMSG #channel :hello world", line);
9898+ }
9999+100100+ [TestMethod]
101101+ public void TestTrailingNoSpace()
102102+ {
103103+ var line = new Line
104104+ {
105105+ Command = "PRIVMSG",
106106+ Params = new List<string> {"#channel", "helloworld"}
107107+ }.Format();
108108+109109+ Assert.AreEqual("PRIVMSG #channel helloworld", line);
110110+ }
111111+112112+ [TestMethod]
113113+ public void TestTrailingDoubleColon()
114114+ {
115115+ var line = new Line
116116+ {
117117+ Command = "PRIVMSG",
118118+ Params = new List<string> {"#channel", ":helloworld"}
119119+ }.Format();
120120+121121+ Assert.AreEqual("PRIVMSG #channel ::helloworld", line);
122122+ }
123123+124124+ [TestMethod]
125125+ public void TestInvalidNonLastSpace()
126126+ {
127127+ Assert.ThrowsException<ArgumentException>(() =>
128128+ {
129129+ new Line
130130+ {
131131+ Command = "USER",
132132+ Params = new List<string> {"user", "0 *", "real name"}
133133+ }.Format();
134134+ });
135135+ }
136136+137137+ [TestMethod]
138138+ public void TestInvalidNonLastColon()
139139+ {
140140+ Assert.ThrowsException<ArgumentException>(() =>
141141+ {
142142+ new Line
143143+ {
144144+ Command = "PRIVMSG",
145145+ Params = new List<string> {":#channel", "hello"}
146146+ }.Format();
147147+ });
148148+ }
149149+ }
150150+}
+64
IrcTokens/Tests/HostmaskTests.cs
···11+using Microsoft.VisualStudio.TestTools.UnitTesting;
22+33+namespace IrcTokens.Tests
44+{
55+ [TestClass]
66+ public class HostmaskTests
77+ {
88+ [TestMethod]
99+ public void TestHostmask()
1010+ {
1111+ var hostmask = new Hostmask("nick!user@host");
1212+ Assert.AreEqual("nick", hostmask.NickName);
1313+ Assert.AreEqual("user", hostmask.UserName);
1414+ Assert.AreEqual("host", hostmask.HostName);
1515+ }
1616+1717+ [TestMethod]
1818+ public void TestNoHostName()
1919+ {
2020+ var hostmask = new Hostmask("nick!user");
2121+ Assert.AreEqual("nick", hostmask.NickName);
2222+ Assert.AreEqual("user", hostmask.UserName);
2323+ Assert.IsNull(hostmask.HostName);
2424+ }
2525+2626+ [TestMethod]
2727+ public void TestNoUserName()
2828+ {
2929+ var hostmask = new Hostmask("nick@host");
3030+ Assert.AreEqual("nick", hostmask.NickName);
3131+ Assert.IsNull(hostmask.UserName);
3232+ Assert.AreEqual("host", hostmask.HostName);
3333+ }
3434+3535+ [TestMethod]
3636+ public void TestOnlyNickName()
3737+ {
3838+ var hostmask = new Hostmask("nick");
3939+ Assert.AreEqual("nick", hostmask.NickName);
4040+ Assert.IsNull(hostmask.UserName);
4141+ Assert.IsNull(hostmask.HostName);
4242+ }
4343+4444+ [TestMethod]
4545+ public void TestHostmaskFromLine()
4646+ {
4747+ var line = new Line(":nick!user@host PRIVMSG #channel hello");
4848+ var hostmask = new Hostmask("nick!user@host");
4949+ Assert.AreEqual(hostmask.ToString(), line.Hostmask.ToString());
5050+ Assert.AreEqual("nick", line.Hostmask.NickName);
5151+ Assert.AreEqual("user", line.Hostmask.UserName);
5252+ Assert.AreEqual("host", line.Hostmask.HostName);
5353+ }
5454+5555+ [TestMethod]
5656+ public void TestEmptyHostmaskFromLine()
5757+ {
5858+ var line = new Line("PRIVMSG #channel hello");
5959+ Assert.IsNull(line.Hostmask.HostName);
6060+ Assert.IsNull(line.Hostmask.UserName);
6161+ Assert.IsNull(line.Hostmask.NickName);
6262+ }
6363+ }
6464+}
+55
IrcTokens/Tests/ParserTests.cs
···11+using System.Collections.Generic;
22+using System.IO;
33+using IrcTokens.Tests.Data;
44+using Microsoft.VisualStudio.TestTools.UnitTesting;
55+using YamlDotNet.Serialization;
66+using YamlDotNet.Serialization.NamingConventions;
77+88+namespace IrcTokens.Tests
99+{
1010+ [TestClass]
1111+ public class ParserTests
1212+ {
1313+ private static T LoadYaml<T>(string path)
1414+ {
1515+ var deserializer = new DeserializerBuilder()
1616+ .WithNamingConvention(CamelCaseNamingConvention.Instance)
1717+ .Build();
1818+1919+ return deserializer.Deserialize<T>(File.ReadAllText(path));
2020+ }
2121+2222+ [TestMethod]
2323+ public void TestSplit()
2424+ {
2525+ foreach (var test in LoadYaml<SplitModel>("Tests/Data/msg-split.yaml").Tests)
2626+ {
2727+ var tokens = new Line(test.Input);
2828+ var atoms = test.Atoms;
2929+3030+ Assert.AreEqual(atoms.Verb.ToUpper(), tokens.Command, $"command failed on: '{test.Input}'");
3131+ Assert.AreEqual(atoms.Source, tokens.Source, $"source failed on: '{test.Input}'");
3232+ CollectionAssert.AreEqual(atoms.Tags, tokens.Tags, $"tags failed on: '{test.Input}'");
3333+ CollectionAssert.AreEqual(atoms.Params ?? new List<string>(), tokens.Params, $"params failed on: '{test.Input}'");
3434+ }
3535+ }
3636+3737+ [TestMethod]
3838+ public void TestJoin()
3939+ {
4040+ foreach (var test in LoadYaml<JoinModel>("Tests/Data/msg-join.yaml").Tests)
4141+ {
4242+ var atoms = test.Atoms;
4343+ var line = new Line
4444+ {
4545+ Command = atoms.Verb,
4646+ Params = atoms.Params,
4747+ Source = atoms.Source ?? null,
4848+ Tags = atoms.Tags
4949+ }.Format();
5050+5151+ Assert.IsTrue(test.Matches.Contains(line), test.Description);
5252+ }
5353+ }
5454+ }
5555+}
+118
IrcTokens/Tests/TokenizationTests.cs
···11+using System.Collections.Generic;
22+using Microsoft.VisualStudio.TestTools.UnitTesting;
33+44+namespace IrcTokens.Tests
55+{
66+ [TestClass]
77+ public class TokenizationTests
88+ {
99+ [TestMethod]
1010+ public void TestTagsMissing()
1111+ {
1212+ var line = new Line("PRIVMSG #channel");
1313+ Assert.IsNull(line.Tags);
1414+ }
1515+1616+ [TestMethod]
1717+ public void TestTagsMissingValue()
1818+ {
1919+ var line = new Line("@id= PRIVMSG #channel");
2020+ Assert.AreEqual(string.Empty, line.Tags["id"]);
2121+ }
2222+2323+ [TestMethod]
2424+ public void TestTagsMissingEqual()
2525+ {
2626+ var line = new Line("@id PRIVMSG #channel");
2727+ Assert.AreEqual(string.Empty, line.Tags["id"]);
2828+ }
2929+3030+ [TestMethod]
3131+ public void TestTagsUnescape()
3232+ {
3333+ var line = new Line(@"@id=1\\\:\r\n\s2 PRIVMSG #channel");
3434+ Assert.AreEqual("1\\;\r\n 2", line.Tags["id"]);
3535+ }
3636+3737+ [TestMethod]
3838+ public void TestTagsOverlap()
3939+ {
4040+ var line = new Line(@"@id=1\\\s\\s PRIVMSG #channel");
4141+ Assert.AreEqual("1\\ \\s", line.Tags["id"]);
4242+ }
4343+4444+ [TestMethod]
4545+ public void TestTagsLoneEndSlash()
4646+ {
4747+ var line = new Line("@id=1\\ PRIVMSG #channel");
4848+ Assert.AreEqual("1", line.Tags["id"]);
4949+ }
5050+5151+ [TestMethod]
5252+ public void TestSourceWithoutTags()
5353+ {
5454+ var line = new Line(":nick!user@host PRIVMSG #channel");
5555+ Assert.AreEqual("nick!user@host", line.Source);
5656+ }
5757+5858+ [TestMethod]
5959+ public void TestSourceWithTags()
6060+ {
6161+ var line = new Line("@id=123 :nick!user@host PRIVMSG #channel");
6262+ Assert.AreEqual("nick!user@host", line.Source);
6363+ }
6464+6565+ [TestMethod]
6666+ public void TestSourceMissingWithoutTags()
6767+ {
6868+ var line = new Line("PRIVMSG #channel");
6969+ Assert.IsNull(line.Source);
7070+ }
7171+7272+ [TestMethod]
7373+ public void TestSourceMissingWithTags()
7474+ {
7575+ var line = new Line("@id=123 PRIVMSG #channel");
7676+ Assert.IsNull(line.Source);
7777+ }
7878+7979+ [TestMethod]
8080+ public void TestCommand()
8181+ {
8282+ var line = new Line("privmsg #channel");
8383+ Assert.AreEqual("PRIVMSG", line.Command);
8484+ }
8585+8686+ [TestMethod]
8787+ public void TestParamsTrailing()
8888+ {
8989+ var line = new Line("PRIVMSG #channel :hello world");
9090+ CollectionAssert.AreEqual(new List<string> {"#channel", "hello world"}, line.Params);
9191+ }
9292+9393+ [TestMethod]
9494+ public void TestParamsOnlyTrailing()
9595+ {
9696+ var line = new Line("PRIVMSG :hello world");
9797+ CollectionAssert.AreEqual(new List<string> {"hello world"}, line.Params);
9898+ }
9999+100100+ [TestMethod]
101101+ public void TestParamsMissing()
102102+ {
103103+ var line = new Line("PRIVMSG");
104104+ Assert.AreEqual("PRIVMSG", line.Command);
105105+ CollectionAssert.AreEqual(new List<string>(), line.Params);
106106+ }
107107+108108+ [TestMethod]
109109+ public void TestAllTokens()
110110+ {
111111+ var line = new Line("@id=123 :nick!user@host PRIVMSG #channel :hello world");
112112+ CollectionAssert.AreEqual(new Dictionary<string, string> {{"id", "123"}}, line.Tags);
113113+ Assert.AreEqual("nick!user@host", line.Source);
114114+ Assert.AreEqual("PRIVMSG", line.Command);
115115+ CollectionAssert.AreEqual(new List<string> {"#channel", "hello world"}, line.Params);
116116+ }
117117+ }
118118+}
+22
LICENSE
···11+MIT License
22+33+Copyright (c) 2020 Ben Harris
44+55+Permission is hereby granted, free of charge, to any person obtaining a copy
66+of this software and associated documentation files (the "Software"), to deal
77+in the Software without restriction, including without limitation the rights
88+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
99+copies of the Software, and to permit persons to whom the Software is
1010+furnished to do so, subject to the following conditions:
1111+1212+The above copyright notice and this permission notice shall be included in all
1313+copies or substantial portions of the Software.
1414+1515+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1616+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1717+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1818+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1919+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
2020+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2121+SOFTWARE.
2222+
+5
README.md
···11+# irctokens
22+33+this is a c\# port of jesopo's [irctokens](
44+https://github.com/jesopo/irctokens)
55+