IRC parsing, tokenization, and state handling in C#

init

+1685
+358
.gitignore
··· 1 + ## Ignore Visual Studio temporary files, build results, and 2 + ## files generated by popular Visual Studio add-ons. 3 + ## 4 + ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 + 6 + # User-specific files 7 + *.rsuser 8 + *.suo 9 + *.user 10 + *.userosscache 11 + *.sln.docstates 12 + 13 + # User-specific files (MonoDevelop/Xamarin Studio) 14 + *.userprefs 15 + 16 + # Mono auto generated files 17 + mono_crash.* 18 + 19 + # Build results 20 + [Dd]ebug/ 21 + [Dd]ebugPublic/ 22 + [Rr]elease/ 23 + [Rr]eleases/ 24 + x64/ 25 + x86/ 26 + [Ww][Ii][Nn]32/ 27 + [Aa][Rr][Mm]/ 28 + [Aa][Rr][Mm]64/ 29 + bld/ 30 + [Bb]in/ 31 + [Oo]bj/ 32 + [Ll]og/ 33 + [Ll]ogs/ 34 + 35 + # Visual Studio 2015/2017 cache/options directory 36 + .vs/ 37 + # Uncomment if you have tasks that create the project's static files in wwwroot 38 + #wwwroot/ 39 + 40 + # Visual Studio 2017 auto generated files 41 + Generated\ Files/ 42 + 43 + # MSTest test Results 44 + [Tt]est[Rr]esult*/ 45 + [Bb]uild[Ll]og.* 46 + 47 + # NUnit 48 + *.VisualState.xml 49 + TestResult.xml 50 + nunit-*.xml 51 + 52 + # Build Results of an ATL Project 53 + [Dd]ebugPS/ 54 + [Rr]eleasePS/ 55 + dlldata.c 56 + 57 + # Benchmark Results 58 + BenchmarkDotNet.Artifacts/ 59 + 60 + # .NET Core 61 + project.lock.json 62 + project.fragment.lock.json 63 + artifacts/ 64 + 65 + # ASP.NET Scaffolding 66 + ScaffoldingReadMe.txt 67 + 68 + # StyleCop 69 + StyleCopReport.xml 70 + 71 + # Files built by Visual Studio 72 + *_i.c 73 + *_p.c 74 + *_h.h 75 + *.ilk 76 + *.meta 77 + *.obj 78 + *.iobj 79 + *.pch 80 + *.pdb 81 + *.ipdb 82 + *.pgc 83 + *.pgd 84 + *.rsp 85 + *.sbr 86 + *.tlb 87 + *.tli 88 + *.tlh 89 + *.tmp 90 + *.tmp_proj 91 + *_wpftmp.csproj 92 + *.log 93 + *.vspscc 94 + *.vssscc 95 + .builds 96 + *.pidb 97 + *.svclog 98 + *.scc 99 + 100 + # Chutzpah Test files 101 + _Chutzpah* 102 + 103 + # Visual C++ cache files 104 + ipch/ 105 + *.aps 106 + *.ncb 107 + *.opendb 108 + *.opensdf 109 + *.sdf 110 + *.cachefile 111 + *.VC.db 112 + *.VC.VC.opendb 113 + 114 + # Visual Studio profiler 115 + *.psess 116 + *.vsp 117 + *.vspx 118 + *.sap 119 + 120 + # Visual Studio Trace Files 121 + *.e2e 122 + 123 + # TFS 2012 Local Workspace 124 + $tf/ 125 + 126 + # Guidance Automation Toolkit 127 + *.gpState 128 + 129 + # ReSharper is a .NET coding add-in 130 + _ReSharper*/ 131 + *.[Rr]e[Ss]harper 132 + *.DotSettings.user 133 + 134 + # TeamCity is a build add-in 135 + _TeamCity* 136 + 137 + # DotCover is a Code Coverage Tool 138 + *.dotCover 139 + 140 + # AxoCover is a Code Coverage Tool 141 + .axoCover/* 142 + !.axoCover/settings.json 143 + 144 + # Coverlet is a free, cross platform Code Coverage Tool 145 + coverage*[.json, .xml, .info] 146 + 147 + # Visual Studio code coverage results 148 + *.coverage 149 + *.coveragexml 150 + 151 + # NCrunch 152 + _NCrunch_* 153 + .*crunch*.local.xml 154 + nCrunchTemp_* 155 + 156 + # MightyMoose 157 + *.mm.* 158 + AutoTest.Net/ 159 + 160 + # Web workbench (sass) 161 + .sass-cache/ 162 + 163 + # Installshield output folder 164 + [Ee]xpress/ 165 + 166 + # DocProject is a documentation generator add-in 167 + DocProject/buildhelp/ 168 + DocProject/Help/*.HxT 169 + DocProject/Help/*.HxC 170 + DocProject/Help/*.hhc 171 + DocProject/Help/*.hhk 172 + DocProject/Help/*.hhp 173 + DocProject/Help/Html2 174 + DocProject/Help/html 175 + 176 + # Click-Once directory 177 + publish/ 178 + 179 + # Publish Web Output 180 + *.[Pp]ublish.xml 181 + *.azurePubxml 182 + # Note: Comment the next line if you want to checkin your web deploy settings, 183 + # but database connection strings (with potential passwords) will be unencrypted 184 + *.pubxml 185 + *.publishproj 186 + 187 + # Microsoft Azure Web App publish settings. Comment the next line if you want to 188 + # checkin your Azure Web App publish settings, but sensitive information contained 189 + # in these scripts will be unencrypted 190 + PublishScripts/ 191 + 192 + # NuGet Packages 193 + *.nupkg 194 + # NuGet Symbol Packages 195 + *.snupkg 196 + # The packages folder can be ignored because of Package Restore 197 + **/[Pp]ackages/* 198 + # except build/, which is used as an MSBuild target. 199 + !**/[Pp]ackages/build/ 200 + # Uncomment if necessary however generally it will be regenerated when needed 201 + #!**/[Pp]ackages/repositories.config 202 + # NuGet v3's project.json files produces more ignorable files 203 + *.nuget.props 204 + *.nuget.targets 205 + 206 + # Microsoft Azure Build Output 207 + csx/ 208 + *.build.csdef 209 + 210 + # Microsoft Azure Emulator 211 + ecf/ 212 + rcf/ 213 + 214 + # Windows Store app package directories and files 215 + AppPackages/ 216 + BundleArtifacts/ 217 + Package.StoreAssociation.xml 218 + _pkginfo.txt 219 + *.appx 220 + *.appxbundle 221 + *.appxupload 222 + 223 + # Visual Studio cache files 224 + # files ending in .cache can be ignored 225 + *.[Cc]ache 226 + # but keep track of directories ending in .cache 227 + !?*.[Cc]ache/ 228 + 229 + # Others 230 + ClientBin/ 231 + ~$* 232 + *~ 233 + *.dbmdl 234 + *.dbproj.schemaview 235 + *.jfm 236 + *.pfx 237 + *.publishsettings 238 + orleans.codegen.cs 239 + 240 + # Including strong name files can present a security risk 241 + # (https://github.com/github/gitignore/pull/2483#issue-259490424) 242 + #*.snk 243 + 244 + # Since there are multiple workflows, uncomment next line to ignore bower_components 245 + # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 246 + #bower_components/ 247 + 248 + # RIA/Silverlight projects 249 + Generated_Code/ 250 + 251 + # Backup & report files from converting an old project file 252 + # to a newer Visual Studio version. Backup files are not needed, 253 + # because we have git ;-) 254 + _UpgradeReport_Files/ 255 + Backup*/ 256 + UpgradeLog*.XML 257 + UpgradeLog*.htm 258 + ServiceFabricBackup/ 259 + *.rptproj.bak 260 + 261 + # SQL Server files 262 + *.mdf 263 + *.ldf 264 + *.ndf 265 + 266 + # Business Intelligence projects 267 + *.rdl.data 268 + *.bim.layout 269 + *.bim_*.settings 270 + *.rptproj.rsuser 271 + *- [Bb]ackup.rdl 272 + *- [Bb]ackup ([0-9]).rdl 273 + *- [Bb]ackup ([0-9][0-9]).rdl 274 + 275 + # Microsoft Fakes 276 + FakesAssemblies/ 277 + 278 + # GhostDoc plugin setting file 279 + *.GhostDoc.xml 280 + 281 + # Node.js Tools for Visual Studio 282 + .ntvs_analysis.dat 283 + node_modules/ 284 + 285 + # Visual Studio 6 build log 286 + *.plg 287 + 288 + # Visual Studio 6 workspace options file 289 + *.opt 290 + 291 + # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 292 + *.vbw 293 + 294 + # Visual Studio LightSwitch build output 295 + **/*.HTMLClient/GeneratedArtifacts 296 + **/*.DesktopClient/GeneratedArtifacts 297 + **/*.DesktopClient/ModelManifest.xml 298 + **/*.Server/GeneratedArtifacts 299 + **/*.Server/ModelManifest.xml 300 + _Pvt_Extensions 301 + 302 + # Paket dependency manager 303 + .paket/paket.exe 304 + paket-files/ 305 + 306 + # FAKE - F# Make 307 + .fake/ 308 + 309 + # CodeRush personal settings 310 + .cr/personal 311 + 312 + # Python Tools for Visual Studio (PTVS) 313 + __pycache__/ 314 + *.pyc 315 + 316 + # Cake - Uncomment if you are using it 317 + # tools/** 318 + # !tools/packages.config 319 + 320 + # Tabs Studio 321 + *.tss 322 + 323 + # Telerik's JustMock configuration file 324 + *.jmconfig 325 + 326 + # BizTalk build output 327 + *.btp.cs 328 + *.btm.cs 329 + *.odx.cs 330 + *.xsd.cs 331 + 332 + # OpenCover UI analysis results 333 + OpenCover/ 334 + 335 + # Azure Stream Analytics local run output 336 + ASALocalRun/ 337 + 338 + # MSBuild Binary and Structured Log 339 + *.binlog 340 + 341 + # NVidia Nsight GPU debugger configuration file 342 + *.nvuser 343 + 344 + # MFractors (Xamarin productivity tool) working folder 345 + .mfractor/ 346 + 347 + # Local History for Visual Studio 348 + .localhistory/ 349 + 350 + # BeatPulse healthcheck temp database 351 + healthchecksdb 352 + 353 + # Backup folder for Package Reference Convert tool in Visual Studio 2017 354 + MigrationBackup/ 355 + 356 + # Ionide (cross platform F# VS Code tools) working folder 357 + .ionide/ 358 +
+25
IrcTokens.sln
··· 1 +  2 + Microsoft Visual Studio Solution File, Format Version 12.00 3 + # Visual Studio Version 16 4 + VisualStudioVersion = 16.0.30011.22 5 + MinimumVisualStudioVersion = 10.0.40219.1 6 + Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IrcTokens", "IrcTokens\IrcTokens.csproj", "{9E812F45-B2CD-42D2-8378-EBEBF8697905}" 7 + EndProject 8 + Global 9 + GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 + Debug|Any CPU = Debug|Any CPU 11 + Release|Any CPU = Release|Any CPU 12 + EndGlobalSection 13 + GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 + {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 + {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 + {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 + {9E812F45-B2CD-42D2-8378-EBEBF8697905}.Release|Any CPU.Build.0 = Release|Any CPU 18 + EndGlobalSection 19 + GlobalSection(SolutionProperties) = preSolution 20 + HideSolutionNode = FALSE 21 + EndGlobalSection 22 + GlobalSection(ExtensibilityGlobals) = postSolution 23 + SolutionGuid = {0B91F0EA-8564-4318-8EEC-ED0640475141} 24 + EndGlobalSection 25 + EndGlobal
+42
IrcTokens/Hostmask.cs
··· 1 + namespace IrcTokens 2 + { 3 + /// <summary> 4 + /// Represents the three parts of a hostmask. Parse with the constructor. 5 + /// </summary> 6 + public class Hostmask 7 + { 8 + public string NickName { get; set; } 9 + public string UserName { get; set; } 10 + public string HostName { get; set; } 11 + 12 + public override string ToString() => _source; 13 + 14 + private readonly string _source; 15 + 16 + public Hostmask(string source) 17 + { 18 + if (source == null) return; 19 + 20 + _source = source; 21 + 22 + if (source.Contains('@')) 23 + { 24 + var split = source.Split('@'); 25 + 26 + NickName = split[0]; 27 + HostName = split[1]; 28 + } 29 + else 30 + { 31 + NickName = source; 32 + } 33 + 34 + if (NickName.Contains('!')) 35 + { 36 + var userSplit = NickName.Split('!'); 37 + NickName = userSplit[0]; 38 + UserName = userSplit[1]; 39 + } 40 + } 41 + } 42 + }
+23
IrcTokens/IrcTokens.csproj
··· 1 + <Project Sdk="Microsoft.NET.Sdk"> 2 + 3 + <PropertyGroup> 4 + <TargetFramework>netcoreapp3.1</TargetFramework> 5 + </PropertyGroup> 6 + 7 + <ItemGroup> 8 + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> 9 + <PackageReference Include="MSTest.TestAdapter" Version="2.1.1" /> 10 + <PackageReference Include="MSTest.TestFramework" Version="2.1.1" /> 11 + <PackageReference Include="YamlDotNet" Version="8.1.0" /> 12 + </ItemGroup> 13 + 14 + <ItemGroup> 15 + <None Update="Tests\Data\msg-join.yaml"> 16 + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> 17 + </None> 18 + <None Update="Tests\Data\msg-split.yaml"> 19 + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> 20 + </None> 21 + </ItemGroup> 22 + 23 + </Project>
+139
IrcTokens/Line.cs
··· 1 + using System; 2 + using System.Collections.Generic; 3 + using System.Linq; 4 + 5 + namespace IrcTokens 6 + { 7 + /// <summary> 8 + /// Tools to represent, parse, and format IRC lines 9 + /// </summary> 10 + public class Line 11 + { 12 + public Dictionary<string, string> Tags { get; set; } 13 + public string Source { get; set; } 14 + public string Command { get; set; } 15 + public List<string> Params { get; set; } 16 + 17 + private Hostmask _hostmask; 18 + private string _rawLine; 19 + 20 + public override string ToString() => 21 + $"Line(tags={string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}, params={string.Join(",", Params)})"; 22 + 23 + public Hostmask Hostmask => 24 + _hostmask ??= new Hostmask(Source); 25 + 26 + public Line() { } 27 + 28 + /// <summary> 29 + /// Build new <see cref="Line"/> object parsed from <param name="line">a string</param>. Analogous to irctokens.tokenise() 30 + /// </summary> 31 + /// <param name="line">irc line to parse</param> 32 + public Line(string line) 33 + { 34 + _rawLine = line; 35 + string[] split; 36 + 37 + if (line.StartsWith('@')) 38 + { 39 + Tags = new Dictionary<string, string>(); 40 + 41 + split = line.Split(" "); 42 + var messageTags = split[0]; 43 + line = string.Join(" ", split.Skip(1)); 44 + 45 + foreach (var part in messageTags.Substring(1).Split(';')) 46 + { 47 + if (part.Contains('=')) 48 + { 49 + split = part.Split('='); 50 + Tags[split[0]] = Protocol.UnescapeTag(split[1]); 51 + } 52 + else 53 + { 54 + Tags[part] = string.Empty; 55 + } 56 + } 57 + } 58 + 59 + string trailing; 60 + if (line.Contains(" :")) 61 + { 62 + split = line.Split(" :"); 63 + line = split[0]; 64 + trailing = string.Join(" :", split.Skip(1)); 65 + } 66 + else 67 + { 68 + trailing = null; 69 + } 70 + 71 + Params = line.Contains(' ') 72 + ? line.Split(' ').Where(p => !string.IsNullOrWhiteSpace(p)).ToList() 73 + : new List<string> {line}; 74 + 75 + if (Params[0].StartsWith(':')) 76 + { 77 + Source = Params[0].Substring(1); 78 + Params.RemoveAt(0); 79 + } 80 + 81 + if (Params.Count > 0) 82 + { 83 + Command = Params[0].ToUpper(); 84 + Params.RemoveAt(0); 85 + } 86 + 87 + if (trailing != null) 88 + { 89 + Params.Add(trailing); 90 + } 91 + } 92 + 93 + /// <summary> 94 + /// Format a <see cref="Line"/> as a standards-compliant IRC line 95 + /// </summary> 96 + /// <returns>formatted irc line</returns> 97 + public string Format() 98 + { 99 + var outs = new List<string>(); 100 + 101 + if (Tags != null && Tags.Any()) 102 + { 103 + var tags = Tags.Keys 104 + .Select(key => string.IsNullOrWhiteSpace(Tags[key]) ? key : $"{key}={Protocol.EscapeTag(Tags[key])}") 105 + .ToList(); 106 + 107 + outs.Add($"@{string.Join(";", tags)}"); 108 + } 109 + 110 + if (Source != null) 111 + { 112 + outs.Add($":{Source}"); 113 + } 114 + 115 + outs.Add(Command); 116 + 117 + if (Params != null && Params.Any()) 118 + { 119 + var last = Params[^1]; 120 + Params.RemoveAt(Params.Count - 1); 121 + 122 + foreach (var p in Params) 123 + { 124 + if (p.Contains(' ')) 125 + throw new ArgumentException("non-last parameters cannot have spaces"); 126 + if (p.StartsWith(':')) 127 + throw new ArgumentException("non-last parameters cannot start with colon"); 128 + } 129 + outs.AddRange(Params); 130 + 131 + if (last == null || string.IsNullOrWhiteSpace(last) || last.Contains(' ') || last.StartsWith(':')) 132 + last = $":{last}"; 133 + outs.Add(last); 134 + } 135 + 136 + return string.Join(" ", outs); 137 + } 138 + } 139 + }
+74
IrcTokens/Protocol.cs
··· 1 + using System; 2 + using System.Globalization; 3 + using System.Linq; 4 + using System.Text; 5 + 6 + namespace IrcTokens 7 + { 8 + public class Protocol 9 + { 10 + private static readonly string[] TagUnescaped = new [] 11 + { 12 + "\\", " ", ";", "\r", "\n" 13 + }; 14 + 15 + private static readonly string[] TagEscaped = new [] 16 + { 17 + "\\\\", "\\s", "\\:", "\\r", "\\n" 18 + }; 19 + 20 + /// <summary> 21 + /// Unescape ircv3 tag 22 + /// </summary> 23 + /// <param name="val">escaped string</param> 24 + /// <returns>unescaped string</returns> 25 + public static string UnescapeTag(string val) 26 + { 27 + var unescaped = new StringBuilder(); 28 + 29 + var graphemeIterator = StringInfo.GetTextElementEnumerator(val); 30 + graphemeIterator.Reset(); 31 + 32 + while (graphemeIterator.MoveNext()) 33 + { 34 + var current = graphemeIterator.GetTextElement(); 35 + 36 + if (current == @"\") 37 + { 38 + try 39 + { 40 + graphemeIterator.MoveNext(); 41 + var next = graphemeIterator.GetTextElement(); 42 + var pair = current + next; 43 + unescaped.Append(TagEscaped.Contains(pair) 44 + ? TagUnescaped[Array.IndexOf(TagEscaped, pair)] 45 + : next); 46 + } 47 + catch (InvalidOperationException) 48 + { 49 + // ignored 50 + } 51 + } 52 + else 53 + unescaped.Append(current); 54 + } 55 + 56 + return unescaped.ToString(); 57 + } 58 + 59 + /// <summary> 60 + /// Escape strings for use in ircv3 tags 61 + /// </summary> 62 + /// <param name="val">string to escape</param> 63 + /// <returns>escaped string</returns> 64 + public static string EscapeTag(string val) 65 + { 66 + for (var i = 0; i < TagUnescaped.Length; ++i) 67 + { 68 + val = val.Replace(TagUnescaped[i], TagEscaped[i]); 69 + } 70 + 71 + return val; 72 + } 73 + } 74 + }
+31
IrcTokens/Tests/Data/JoinModel.cs
··· 1 + using System.Collections.Generic; 2 + using YamlDotNet.Serialization; 3 + 4 + namespace IrcTokens.Tests.Data 5 + { 6 + public class JoinModel 7 + { 8 + public List<Test> Tests { get; set; } 9 + 10 + public class Test 11 + { 12 + [YamlMember(Alias = "desc")] 13 + public string Description { get; set; } 14 + 15 + public Atoms Atoms { get; set; } 16 + 17 + public List<string> Matches { get; set; } 18 + } 19 + 20 + public class Atoms 21 + { 22 + public Dictionary<string, string> Tags { get; set; } 23 + 24 + public string Source { get; set; } 25 + 26 + public string Verb { get; set; } 27 + 28 + public List<string> Params { get; set; } 29 + } 30 + } 31 + }
+15
IrcTokens/Tests/Data/SplitModel.cs
··· 1 + using System.Collections.Generic; 2 + 3 + namespace IrcTokens.Tests.Data 4 + { 5 + public class SplitModel 6 + { 7 + public List<Test> Tests { get; set; } 8 + 9 + public class Test 10 + { 11 + public string Input { get; set; } 12 + public JoinModel.Atoms Atoms { get; set; } 13 + } 14 + } 15 + }
+221
IrcTokens/Tests/Data/msg-join.yaml
··· 1 + # IRC parser tests 2 + # joining atoms into sendable messages 3 + 4 + # Written in 2015 by Daniel Oaks <daniel@danieloaks.net> 5 + # 6 + # To the extent possible under law, the author(s) have dedicated all copyright 7 + # and related and neighboring rights to this software to the public domain 8 + # worldwide. This software is distributed without any warranty. 9 + # 10 + # You should have received a copy of the CC0 Public Domain Dedication along 11 + # with this software. If not, see 12 + # <http://creativecommons.org/publicdomain/zero/1.0/>. 13 + 14 + # some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed 15 + # https://github.com/grawity/code/tree/master/lib/tests 16 + # some of the tests here originate from Mozilla's test vectors, which is public domain 17 + # https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js 18 + # some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here 19 + # https://github.com/SaberUK/ircparser/tree/master/test 20 + 21 + tests: 22 + # the desc string holds a description of the test, if it exists 23 + 24 + # the atoms dict has the keys: 25 + # * tags: tags dict 26 + # tags with no value are an empty string 27 + # * source: source string, without single leading colon 28 + # * verb: verb string 29 + # * params: params split up as a list 30 + # if the params key does not exist, assume it is empty 31 + # if any other keys do no exist, assume they are null 32 + # a key that is null does not exist or is not specified with the 33 + # given input string 34 + 35 + # matches is a list of messages that match 36 + 37 + # simple tests 38 + - desc: Simple test with verb and params. 39 + atoms: 40 + verb: "foo" 41 + params: 42 + - "bar" 43 + - "baz" 44 + - "asdf" 45 + matches: 46 + - "foo bar baz asdf" 47 + - "foo bar baz :asdf" 48 + 49 + # with no regular params 50 + - desc: Simple test with source and no params. 51 + atoms: 52 + source: "src" 53 + verb: "AWAY" 54 + matches: 55 + - ":src AWAY" 56 + 57 + - desc: Simple test with source and empty trailing param. 58 + atoms: 59 + source: "src" 60 + verb: "AWAY" 61 + params: 62 + - "" 63 + matches: 64 + - ":src AWAY :" 65 + 66 + # with source 67 + - desc: Simple test with source. 68 + atoms: 69 + source: "coolguy" 70 + verb: "foo" 71 + params: 72 + - "bar" 73 + - "baz" 74 + - "asdf" 75 + matches: 76 + - ":coolguy foo bar baz asdf" 77 + - ":coolguy foo bar baz :asdf" 78 + 79 + # with trailing param 80 + - desc: Simple test with trailing param. 81 + atoms: 82 + verb: "foo" 83 + params: 84 + - "bar" 85 + - "baz" 86 + - "asdf quux" 87 + matches: 88 + - "foo bar baz :asdf quux" 89 + 90 + - desc: Simple test with empty trailing param. 91 + atoms: 92 + verb: "foo" 93 + params: 94 + - "bar" 95 + - "baz" 96 + - "" 97 + matches: 98 + - "foo bar baz :" 99 + 100 + - desc: Simple test with trailing param containing colon. 101 + atoms: 102 + verb: "foo" 103 + params: 104 + - "bar" 105 + - "baz" 106 + - ":asdf" 107 + matches: 108 + - "foo bar baz ::asdf" 109 + 110 + # with source and trailing param 111 + - desc: Test with source and trailing param. 112 + atoms: 113 + source: "coolguy" 114 + verb: "foo" 115 + params: 116 + - "bar" 117 + - "baz" 118 + - "asdf quux" 119 + matches: 120 + - ":coolguy foo bar baz :asdf quux" 121 + 122 + - desc: Test with trailing containing beginning+end whitespace. 123 + atoms: 124 + source: "coolguy" 125 + verb: "foo" 126 + params: 127 + - "bar" 128 + - "baz" 129 + - " asdf quux " 130 + matches: 131 + - ":coolguy foo bar baz : asdf quux " 132 + 133 + - desc: Test with trailing containing what looks like another trailing param. 134 + atoms: 135 + source: "coolguy" 136 + verb: "PRIVMSG" 137 + params: 138 + - "bar" 139 + - "lol :) " 140 + matches: 141 + - ":coolguy PRIVMSG bar :lol :) " 142 + 143 + - desc: Simple test with source and empty trailing. 144 + atoms: 145 + source: "coolguy" 146 + verb: "foo" 147 + params: 148 + - "bar" 149 + - "baz" 150 + - "" 151 + matches: 152 + - ":coolguy foo bar baz :" 153 + 154 + - desc: Trailing contains only spaces. 155 + atoms: 156 + source: "coolguy" 157 + verb: "foo" 158 + params: 159 + - "bar" 160 + - "baz" 161 + - " " 162 + matches: 163 + - ":coolguy foo bar baz : " 164 + 165 + - desc: Param containing tab (tab is not considered SPACE for message splitting). 166 + atoms: 167 + source: "coolguy" 168 + verb: "foo" 169 + params: 170 + - "b\tar" 171 + - "baz" 172 + matches: 173 + - ":coolguy foo b\tar baz" 174 + - ":coolguy foo b\tar :baz" 175 + 176 + # with tags 177 + - desc: Tag with no value and space-filled trailing. 178 + atoms: 179 + tags: 180 + "asd": "" 181 + source: "coolguy" 182 + verb: "foo" 183 + params: 184 + - "bar" 185 + - "baz" 186 + - " " 187 + matches: 188 + - "@asd :coolguy foo bar baz : " 189 + 190 + - desc: Tags with escaped values. 191 + atoms: 192 + verb: "foo" 193 + tags: 194 + "a": "b\\and\nk" 195 + "d": "gh;764" 196 + matches: 197 + - "@a=b\\\\and\\nk;d=gh\\:764 foo" 198 + - "@d=gh\\:764;a=b\\\\and\\nk foo" 199 + 200 + - desc: Tags with escaped values and params. 201 + atoms: 202 + verb: "foo" 203 + tags: 204 + "a": "b\\and\nk" 205 + "d": "gh;764" 206 + params: 207 + - "par1" 208 + - "par2" 209 + matches: 210 + - "@a=b\\\\and\\nk;d=gh\\:764 foo par1 par2" 211 + - "@a=b\\\\and\\nk;d=gh\\:764 foo par1 :par2" 212 + - "@d=gh\\:764;a=b\\\\and\\nk foo par1 par2" 213 + - "@d=gh\\:764;a=b\\\\and\\nk foo par1 :par2" 214 + 215 + - desc: Tag with long, strange values (including LF and newline). 216 + atoms: 217 + tags: 218 + foo: "\\\\;\\s \r\n" 219 + verb: "COMMAND" 220 + matches: 221 + - "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
+343
IrcTokens/Tests/Data/msg-split.yaml
··· 1 + # IRC parser tests 2 + # splitting messages into usable atoms 3 + 4 + # Written in 2015 by Daniel Oaks <daniel@danieloaks.net> 5 + # 6 + # To the extent possible under law, the author(s) have dedicated all copyright 7 + # and related and neighboring rights to this software to the public domain 8 + # worldwide. This software is distributed without any warranty. 9 + # 10 + # You should have received a copy of the CC0 Public Domain Dedication along 11 + # with this software. If not, see 12 + # <http://creativecommons.org/publicdomain/zero/1.0/>. 13 + 14 + # some of the tests here originate from grawity's test vectors, which is WTFPL v2 licensed 15 + # https://github.com/grawity/code/tree/master/lib/tests 16 + # some of the tests here originate from Mozilla's test vectors, which is public domain 17 + # https://dxr.mozilla.org/comm-central/source/chat/protocols/irc/test/test_ircMessage.js 18 + # some of the tests here originate from SaberUK's test vectors, which he's indicated I am free to include here 19 + # https://github.com/SaberUK/ircparser/tree/master/test 20 + 21 + # we follow RFC1459 with regards to multiple ascii spaces splitting atoms: 22 + # The prefix, command, and all parameters are 23 + # separated by one (or more) ASCII space character(s) (0x20). 24 + # because doing it as RFC2812 says (strictly as a single ascii space) isn't sane 25 + 26 + tests: 27 + # input is the string coming directly from the server to parse 28 + 29 + # the atoms dict has the keys: 30 + # * tags: tags dict 31 + # tags with no value are an empty string 32 + # * source: source string, without single leading colon 33 + # * verb: verb string 34 + # * params: params split up as a list 35 + # if the params key does not exist, assume it is empty 36 + # if any other keys do no exist, assume they are null 37 + # a key that is null does not exist or is not specified with the 38 + # given input string 39 + 40 + # simple 41 + - input: "foo bar baz asdf" 42 + atoms: 43 + verb: "foo" 44 + params: 45 + - "bar" 46 + - "baz" 47 + - "asdf" 48 + 49 + # with source 50 + - input: ":coolguy foo bar baz asdf" 51 + atoms: 52 + source: "coolguy" 53 + verb: "foo" 54 + params: 55 + - "bar" 56 + - "baz" 57 + - "asdf" 58 + 59 + # with trailing param 60 + - input: "foo bar baz :asdf quux" 61 + atoms: 62 + verb: "foo" 63 + params: 64 + - "bar" 65 + - "baz" 66 + - "asdf quux" 67 + 68 + - input: "foo bar baz :" 69 + atoms: 70 + verb: "foo" 71 + params: 72 + - "bar" 73 + - "baz" 74 + - "" 75 + 76 + - input: "foo bar baz ::asdf" 77 + atoms: 78 + verb: "foo" 79 + params: 80 + - "bar" 81 + - "baz" 82 + - ":asdf" 83 + 84 + # with source and trailing param 85 + - input: ":coolguy foo bar baz :asdf quux" 86 + atoms: 87 + source: "coolguy" 88 + verb: "foo" 89 + params: 90 + - "bar" 91 + - "baz" 92 + - "asdf quux" 93 + 94 + - input: ":coolguy foo bar baz : asdf quux " 95 + atoms: 96 + source: "coolguy" 97 + verb: "foo" 98 + params: 99 + - "bar" 100 + - "baz" 101 + - " asdf quux " 102 + 103 + - input: ":coolguy PRIVMSG bar :lol :) " 104 + atoms: 105 + source: "coolguy" 106 + verb: "PRIVMSG" 107 + params: 108 + - "bar" 109 + - "lol :) " 110 + 111 + - input: ":coolguy foo bar baz :" 112 + atoms: 113 + source: "coolguy" 114 + verb: "foo" 115 + params: 116 + - "bar" 117 + - "baz" 118 + - "" 119 + 120 + - input: ":coolguy foo bar baz : " 121 + atoms: 122 + source: "coolguy" 123 + verb: "foo" 124 + params: 125 + - "bar" 126 + - "baz" 127 + - " " 128 + 129 + # with tags 130 + - input: "@a=b;c=32;k;rt=ql7 foo" 131 + atoms: 132 + verb: "foo" 133 + tags: 134 + "a": "b" 135 + "c": "32" 136 + "k": "" 137 + "rt": "ql7" 138 + 139 + # with escaped tags 140 + - input: "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo" 141 + atoms: 142 + verb: "foo" 143 + tags: 144 + "a": "b\\and\nk" 145 + "c": "72 45" 146 + "d": "gh;764" 147 + 148 + # with tags and source 149 + - input: "@c;h=;a=b :quux ab cd" 150 + atoms: 151 + tags: 152 + "c": "" 153 + "h": "" 154 + "a": "b" 155 + source: "quux" 156 + verb: "ab" 157 + params: 158 + - "cd" 159 + 160 + # different forms of last param 161 + - input: ":src JOIN #chan" 162 + atoms: 163 + source: "src" 164 + verb: "JOIN" 165 + params: 166 + - "#chan" 167 + 168 + - input: ":src JOIN :#chan" 169 + atoms: 170 + source: "src" 171 + verb: "JOIN" 172 + params: 173 + - "#chan" 174 + 175 + # with and without last param 176 + - input: ":src AWAY" 177 + atoms: 178 + source: "src" 179 + verb: "AWAY" 180 + 181 + - input: ":src AWAY " 182 + atoms: 183 + source: "src" 184 + verb: "AWAY" 185 + 186 + # tab is not considered <SPACE> 187 + - input: ":cool\tguy foo bar baz" 188 + atoms: 189 + source: "cool\tguy" 190 + verb: "foo" 191 + params: 192 + - "bar" 193 + - "baz" 194 + 195 + # with weird control codes in the source 196 + - input: ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz" 197 + atoms: 198 + source: "coolguy!ag@net\x035w\x03ork.admin" 199 + verb: "PRIVMSG" 200 + params: 201 + - "foo" 202 + - "bar baz" 203 + 204 + - input: ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz" 205 + atoms: 206 + source: "coolguy!~ag@n\x02et\x0305w\x0fork.admin" 207 + verb: "PRIVMSG" 208 + params: 209 + - "foo" 210 + - "bar baz" 211 + 212 + - input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3" 213 + atoms: 214 + tags: 215 + tag1: "value1" 216 + tag2: "" 217 + vendor1/tag3: "value2" 218 + vendor2/tag4: "" 219 + source: "irc.example.com" 220 + verb: "COMMAND" 221 + params: 222 + - "param1" 223 + - "param2" 224 + - "param3 param3" 225 + 226 + - input: ":irc.example.com COMMAND param1 param2 :param3 param3" 227 + atoms: 228 + source: "irc.example.com" 229 + verb: "COMMAND" 230 + params: 231 + - "param1" 232 + - "param2" 233 + - "param3 param3" 234 + 235 + - input: "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 COMMAND param1 param2 :param3 param3" 236 + atoms: 237 + tags: 238 + tag1: "value1" 239 + tag2: "" 240 + vendor1/tag3: "value2" 241 + vendor2/tag4: "" 242 + verb: "COMMAND" 243 + params: 244 + - "param1" 245 + - "param2" 246 + - "param3 param3" 247 + 248 + - input: "COMMAND" 249 + atoms: 250 + verb: "COMMAND" 251 + 252 + # yaml encoding + slashes is fun 253 + - input: "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND" 254 + atoms: 255 + tags: 256 + foo: "\\\\;\\s \r\n" 257 + verb: "COMMAND" 258 + 259 + # broken messages from unreal 260 + - input: ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters" 261 + atoms: 262 + source: "gravel.mozilla.org" 263 + verb: "432" 264 + params: 265 + - "#momo" 266 + - "Erroneous Nickname: Illegal characters" 267 + 268 + - input: ":gravel.mozilla.org MODE #tckk +n " 269 + atoms: 270 + source: "gravel.mozilla.org" 271 + verb: "MODE" 272 + params: 273 + - "#tckk" 274 + - "+n" 275 + 276 + - input: ":services.esper.net MODE #foo-bar +o foobar " 277 + atoms: 278 + source: "services.esper.net" 279 + verb: "MODE" 280 + params: 281 + - "#foo-bar" 282 + - "+o" 283 + - "foobar" 284 + 285 + # tag values should be parsed char-at-a-time to prevent wayward replacements. 286 + - input: "@tag1=value\\\\ntest COMMAND" 287 + atoms: 288 + tags: 289 + tag1: "value\\ntest" 290 + verb: "COMMAND" 291 + 292 + # If a tag value has a slash followed by a character which doesn't need 293 + # to be escaped, the slash should be dropped. 294 + - input: "@tag1=value\\1 COMMAND" 295 + atoms: 296 + tags: 297 + tag1: "value1" 298 + verb: "COMMAND" 299 + 300 + # A slash at the end of a tag value should be dropped 301 + - input: "@tag1=value1\\ COMMAND" 302 + atoms: 303 + tags: 304 + tag1: "value1" 305 + verb: "COMMAND" 306 + 307 + # Duplicate tags: Parsers SHOULD disregard all but the final occurence 308 + - input: "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND" 309 + atoms: 310 + tags: 311 + tag1: "5" 312 + tag2: "3" 313 + tag3: "4" 314 + verb: "COMMAND" 315 + 316 + # vendored tags can have the same name as a non-vendored tag 317 + - input: "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND" 318 + atoms: 319 + tags: 320 + tag1: "5" 321 + tag2: "3" 322 + tag3: "4" 323 + vendor/tag2: "8" 324 + verb: "COMMAND" 325 + 326 + # Some parsers handle /MODE in a special way, make sure they do it right 327 + - input: ":SomeOp MODE #channel :+i" 328 + atoms: 329 + source: "SomeOp" 330 + verb: "MODE" 331 + params: 332 + - "#channel" 333 + - "+i" 334 + 335 + - input: ":SomeOp MODE #channel +oo SomeUser :AnotherUser" 336 + atoms: 337 + source: "SomeOp" 338 + verb: "MODE" 339 + params: 340 + - "#channel" 341 + - "+oo" 342 + - "SomeUser" 343 + - "AnotherUser"
+150
IrcTokens/Tests/FormatTests.cs
··· 1 + using Microsoft.VisualStudio.TestTools.UnitTesting; 2 + using System; 3 + using System.Collections.Generic; 4 + 5 + namespace IrcTokens.Tests 6 + { 7 + [TestClass] 8 + public class FormatTests 9 + { 10 + [TestMethod] 11 + public void TestTags() 12 + { 13 + var line = new Line 14 + { 15 + Command = "PRIVMSG", 16 + Params = new List<string> {"#channel", "hello"}, 17 + Tags = new Dictionary<string, string> {{"id", "\\" + " " + ";" + "\r\n"}} 18 + }.Format(); 19 + 20 + Assert.AreEqual("@id=\\\\\\s\\:\\r\\n PRIVMSG #channel hello", line); 21 + } 22 + 23 + [TestMethod] 24 + public void TestMissingTag() 25 + { 26 + var line = new Line 27 + { 28 + Command = "PRIVMSG", 29 + Params = new List<string> {"#channel", "hello"} 30 + }.Format(); 31 + 32 + Assert.AreEqual("PRIVMSG #channel hello", line); 33 + } 34 + 35 + [TestMethod] 36 + public void TestNullTag() 37 + { 38 + var line = new Line 39 + { 40 + Command = "PRIVMSG", 41 + Params = new List<string> {"#channel", "hello"}, 42 + Tags = new Dictionary<string, string> {{"a", null}} 43 + }.Format(); 44 + 45 + Assert.AreEqual("@a PRIVMSG #channel hello", line); 46 + } 47 + 48 + [TestMethod] 49 + public void TestEmptyTag() 50 + { 51 + var line = new Line 52 + { 53 + Command = "PRIVMSG", 54 + Params = new List<string> {"#channel", "hello"}, 55 + Tags = new Dictionary<string, string> {{"a", ""}} 56 + }.Format(); 57 + 58 + Assert.AreEqual("@a PRIVMSG #channel hello", line); 59 + } 60 + 61 + [TestMethod] 62 + public void TestSource() 63 + { 64 + var line = new Line 65 + { 66 + Command = "PRIVMSG", 67 + Params = new List<string> {"#channel", "hello"}, 68 + Source = "nick!user@host" 69 + }.Format(); 70 + 71 + Assert.AreEqual(":nick!user@host PRIVMSG #channel hello", line); 72 + } 73 + 74 + [TestMethod] 75 + public void TestCommandLowercase() 76 + { 77 + var line = new Line {Command = "privmsg"}.Format(); 78 + Assert.AreEqual("privmsg", line); 79 + } 80 + 81 + [TestMethod] 82 + public void TestCommandUppercase() 83 + { 84 + var line = new Line {Command = "PRIVMSG"}.Format(); 85 + Assert.AreEqual("PRIVMSG", line); 86 + } 87 + 88 + [TestMethod] 89 + public void TestTrailingSpace() 90 + { 91 + var line = new Line 92 + { 93 + Command = "PRIVMSG", 94 + Params = new List<string> {"#channel", "hello world"} 95 + }.Format(); 96 + 97 + Assert.AreEqual("PRIVMSG #channel :hello world", line); 98 + } 99 + 100 + [TestMethod] 101 + public void TestTrailingNoSpace() 102 + { 103 + var line = new Line 104 + { 105 + Command = "PRIVMSG", 106 + Params = new List<string> {"#channel", "helloworld"} 107 + }.Format(); 108 + 109 + Assert.AreEqual("PRIVMSG #channel helloworld", line); 110 + } 111 + 112 + [TestMethod] 113 + public void TestTrailingDoubleColon() 114 + { 115 + var line = new Line 116 + { 117 + Command = "PRIVMSG", 118 + Params = new List<string> {"#channel", ":helloworld"} 119 + }.Format(); 120 + 121 + Assert.AreEqual("PRIVMSG #channel ::helloworld", line); 122 + } 123 + 124 + [TestMethod] 125 + public void TestInvalidNonLastSpace() 126 + { 127 + Assert.ThrowsException<ArgumentException>(() => 128 + { 129 + new Line 130 + { 131 + Command = "USER", 132 + Params = new List<string> {"user", "0 *", "real name"} 133 + }.Format(); 134 + }); 135 + } 136 + 137 + [TestMethod] 138 + public void TestInvalidNonLastColon() 139 + { 140 + Assert.ThrowsException<ArgumentException>(() => 141 + { 142 + new Line 143 + { 144 + Command = "PRIVMSG", 145 + Params = new List<string> {":#channel", "hello"} 146 + }.Format(); 147 + }); 148 + } 149 + } 150 + }
+64
IrcTokens/Tests/HostmaskTests.cs
··· 1 + using Microsoft.VisualStudio.TestTools.UnitTesting; 2 + 3 + namespace IrcTokens.Tests 4 + { 5 + [TestClass] 6 + public class HostmaskTests 7 + { 8 + [TestMethod] 9 + public void TestHostmask() 10 + { 11 + var hostmask = new Hostmask("nick!user@host"); 12 + Assert.AreEqual("nick", hostmask.NickName); 13 + Assert.AreEqual("user", hostmask.UserName); 14 + Assert.AreEqual("host", hostmask.HostName); 15 + } 16 + 17 + [TestMethod] 18 + public void TestNoHostName() 19 + { 20 + var hostmask = new Hostmask("nick!user"); 21 + Assert.AreEqual("nick", hostmask.NickName); 22 + Assert.AreEqual("user", hostmask.UserName); 23 + Assert.IsNull(hostmask.HostName); 24 + } 25 + 26 + [TestMethod] 27 + public void TestNoUserName() 28 + { 29 + var hostmask = new Hostmask("nick@host"); 30 + Assert.AreEqual("nick", hostmask.NickName); 31 + Assert.IsNull(hostmask.UserName); 32 + Assert.AreEqual("host", hostmask.HostName); 33 + } 34 + 35 + [TestMethod] 36 + public void TestOnlyNickName() 37 + { 38 + var hostmask = new Hostmask("nick"); 39 + Assert.AreEqual("nick", hostmask.NickName); 40 + Assert.IsNull(hostmask.UserName); 41 + Assert.IsNull(hostmask.HostName); 42 + } 43 + 44 + [TestMethod] 45 + public void TestHostmaskFromLine() 46 + { 47 + var line = new Line(":nick!user@host PRIVMSG #channel hello"); 48 + var hostmask = new Hostmask("nick!user@host"); 49 + Assert.AreEqual(hostmask.ToString(), line.Hostmask.ToString()); 50 + Assert.AreEqual("nick", line.Hostmask.NickName); 51 + Assert.AreEqual("user", line.Hostmask.UserName); 52 + Assert.AreEqual("host", line.Hostmask.HostName); 53 + } 54 + 55 + [TestMethod] 56 + public void TestEmptyHostmaskFromLine() 57 + { 58 + var line = new Line("PRIVMSG #channel hello"); 59 + Assert.IsNull(line.Hostmask.HostName); 60 + Assert.IsNull(line.Hostmask.UserName); 61 + Assert.IsNull(line.Hostmask.NickName); 62 + } 63 + } 64 + }
+55
IrcTokens/Tests/ParserTests.cs
··· 1 + using System.Collections.Generic; 2 + using System.IO; 3 + using IrcTokens.Tests.Data; 4 + using Microsoft.VisualStudio.TestTools.UnitTesting; 5 + using YamlDotNet.Serialization; 6 + using YamlDotNet.Serialization.NamingConventions; 7 + 8 + namespace IrcTokens.Tests 9 + { 10 + [TestClass] 11 + public class ParserTests 12 + { 13 + private static T LoadYaml<T>(string path) 14 + { 15 + var deserializer = new DeserializerBuilder() 16 + .WithNamingConvention(CamelCaseNamingConvention.Instance) 17 + .Build(); 18 + 19 + return deserializer.Deserialize<T>(File.ReadAllText(path)); 20 + } 21 + 22 + [TestMethod] 23 + public void TestSplit() 24 + { 25 + foreach (var test in LoadYaml<SplitModel>("Tests/Data/msg-split.yaml").Tests) 26 + { 27 + var tokens = new Line(test.Input); 28 + var atoms = test.Atoms; 29 + 30 + Assert.AreEqual(atoms.Verb.ToUpper(), tokens.Command, $"command failed on: '{test.Input}'"); 31 + Assert.AreEqual(atoms.Source, tokens.Source, $"source failed on: '{test.Input}'"); 32 + CollectionAssert.AreEqual(atoms.Tags, tokens.Tags, $"tags failed on: '{test.Input}'"); 33 + CollectionAssert.AreEqual(atoms.Params ?? new List<string>(), tokens.Params, $"params failed on: '{test.Input}'"); 34 + } 35 + } 36 + 37 + [TestMethod] 38 + public void TestJoin() 39 + { 40 + foreach (var test in LoadYaml<JoinModel>("Tests/Data/msg-join.yaml").Tests) 41 + { 42 + var atoms = test.Atoms; 43 + var line = new Line 44 + { 45 + Command = atoms.Verb, 46 + Params = atoms.Params, 47 + Source = atoms.Source ?? null, 48 + Tags = atoms.Tags 49 + }.Format(); 50 + 51 + Assert.IsTrue(test.Matches.Contains(line), test.Description); 52 + } 53 + } 54 + } 55 + }
+118
IrcTokens/Tests/TokenizationTests.cs
··· 1 + using System.Collections.Generic; 2 + using Microsoft.VisualStudio.TestTools.UnitTesting; 3 + 4 + namespace IrcTokens.Tests 5 + { 6 + [TestClass] 7 + public class TokenizationTests 8 + { 9 + [TestMethod] 10 + public void TestTagsMissing() 11 + { 12 + var line = new Line("PRIVMSG #channel"); 13 + Assert.IsNull(line.Tags); 14 + } 15 + 16 + [TestMethod] 17 + public void TestTagsMissingValue() 18 + { 19 + var line = new Line("@id= PRIVMSG #channel"); 20 + Assert.AreEqual(string.Empty, line.Tags["id"]); 21 + } 22 + 23 + [TestMethod] 24 + public void TestTagsMissingEqual() 25 + { 26 + var line = new Line("@id PRIVMSG #channel"); 27 + Assert.AreEqual(string.Empty, line.Tags["id"]); 28 + } 29 + 30 + [TestMethod] 31 + public void TestTagsUnescape() 32 + { 33 + var line = new Line(@"@id=1\\\:\r\n\s2 PRIVMSG #channel"); 34 + Assert.AreEqual("1\\;\r\n 2", line.Tags["id"]); 35 + } 36 + 37 + [TestMethod] 38 + public void TestTagsOverlap() 39 + { 40 + var line = new Line(@"@id=1\\\s\\s PRIVMSG #channel"); 41 + Assert.AreEqual("1\\ \\s", line.Tags["id"]); 42 + } 43 + 44 + [TestMethod] 45 + public void TestTagsLoneEndSlash() 46 + { 47 + var line = new Line("@id=1\\ PRIVMSG #channel"); 48 + Assert.AreEqual("1", line.Tags["id"]); 49 + } 50 + 51 + [TestMethod] 52 + public void TestSourceWithoutTags() 53 + { 54 + var line = new Line(":nick!user@host PRIVMSG #channel"); 55 + Assert.AreEqual("nick!user@host", line.Source); 56 + } 57 + 58 + [TestMethod] 59 + public void TestSourceWithTags() 60 + { 61 + var line = new Line("@id=123 :nick!user@host PRIVMSG #channel"); 62 + Assert.AreEqual("nick!user@host", line.Source); 63 + } 64 + 65 + [TestMethod] 66 + public void TestSourceMissingWithoutTags() 67 + { 68 + var line = new Line("PRIVMSG #channel"); 69 + Assert.IsNull(line.Source); 70 + } 71 + 72 + [TestMethod] 73 + public void TestSourceMissingWithTags() 74 + { 75 + var line = new Line("@id=123 PRIVMSG #channel"); 76 + Assert.IsNull(line.Source); 77 + } 78 + 79 + [TestMethod] 80 + public void TestCommand() 81 + { 82 + var line = new Line("privmsg #channel"); 83 + Assert.AreEqual("PRIVMSG", line.Command); 84 + } 85 + 86 + [TestMethod] 87 + public void TestParamsTrailing() 88 + { 89 + var line = new Line("PRIVMSG #channel :hello world"); 90 + CollectionAssert.AreEqual(new List<string> {"#channel", "hello world"}, line.Params); 91 + } 92 + 93 + [TestMethod] 94 + public void TestParamsOnlyTrailing() 95 + { 96 + var line = new Line("PRIVMSG :hello world"); 97 + CollectionAssert.AreEqual(new List<string> {"hello world"}, line.Params); 98 + } 99 + 100 + [TestMethod] 101 + public void TestParamsMissing() 102 + { 103 + var line = new Line("PRIVMSG"); 104 + Assert.AreEqual("PRIVMSG", line.Command); 105 + CollectionAssert.AreEqual(new List<string>(), line.Params); 106 + } 107 + 108 + [TestMethod] 109 + public void TestAllTokens() 110 + { 111 + var line = new Line("@id=123 :nick!user@host PRIVMSG #channel :hello world"); 112 + CollectionAssert.AreEqual(new Dictionary<string, string> {{"id", "123"}}, line.Tags); 113 + Assert.AreEqual("nick!user@host", line.Source); 114 + Assert.AreEqual("PRIVMSG", line.Command); 115 + CollectionAssert.AreEqual(new List<string> {"#channel", "hello world"}, line.Params); 116 + } 117 + } 118 + }
+22
LICENSE
··· 1 + MIT License 2 + 3 + Copyright (c) 2020 Ben Harris 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE. 22 +
+5
README.md
··· 1 + # irctokens 2 + 3 + this is a c\# port of jesopo's [irctokens]( 4 + https://github.com/jesopo/irctokens) 5 +