From 33a54101ec3fd37dabf2ae499753a8a3c164aed3 Mon Sep 17 00:00:00 2001 From: Nathan Ferreira <14365254+nathan130200@users.noreply.github.com> Date: Tue, 7 May 2024 09:12:33 -0300 Subject: [PATCH] Update project. XMPP# - Core - Minor improvements. - Fixed wrong indent chars & side for default formatting options. - Fixed `Element.Value` returning entire inner text from all descendant nodes. XMPP# - Expat - Minor improvements. - Better namespace handler (using namespace stack to track different XML scopes). - All tests passing. --- XmppSharp.Expat/CHANGELOG.md | 20 +- XmppSharp.Expat/ExpatXmppParser.cs | 127 ++++++++-- XmppSharp.Expat/XmppSharp.Expat.csproj | 4 +- XmppSharp.Test/ExpatParserTests.cs | 334 ++++++++++++++++++++++++- XmppSharp.Test/XmppSharp.Test.csproj | 3 - XmppSharp/AsyncDelegates.cs | 4 + XmppSharp/CHANGELOG.md | 45 +++- XmppSharp/Dom/Element.cs | 10 +- XmppSharp/Parsers/BaseXmppParser.cs | 4 +- XmppSharp/Parsers/DefaultXmppParser.cs | 2 +- XmppSharp/Xml.cs | 4 +- XmppSharp/XmlFormatting.cs | 7 +- XmppSharp/XmppSharp.csproj | 2 +- 13 files changed, 520 insertions(+), 46 deletions(-) diff --git a/XmppSharp.Expat/CHANGELOG.md b/XmppSharp.Expat/CHANGELOG.md index bc6feca..b62fc13 100644 --- a/XmppSharp.Expat/CHANGELOG.md +++ b/XmppSharp.Expat/CHANGELOG.md @@ -1,5 +1,21 @@ # XMPP# Expat This package implements the Expat parser in Xmpp Sharp library. -# Common Types -- `ExpatXmppParser` \ No newline at end of file +[![github](https://img.shields.io/badge/XMPP%23_%20Expat-ffe000?style=flat-square&logo=github&label=Github)](https://github.com/nathan130200/XmppSharp) + +### Common Types +- `ExpatXmppParser` + +### Version History + +*1.0.0* + +- Initial Release + +___ + +*1.0.1* + +- Minor improvements. +- Better namespace handler (using namespace stack to track different XML scopes). +- All tests passing. \ No newline at end of file diff --git a/XmppSharp.Expat/ExpatXmppParser.cs b/XmppSharp.Expat/ExpatXmppParser.cs index 863c94a..764fad1 100644 --- a/XmppSharp.Expat/ExpatXmppParser.cs +++ b/XmppSharp.Expat/ExpatXmppParser.cs @@ -1,4 +1,7 @@ -using Expat; +using System.Text; +using System.Text.RegularExpressions; +using System.Xml; +using Expat; using XmppSharp.Dom; using XmppSharp.Exceptions; using XmppSharp.Factory; @@ -9,31 +12,57 @@ namespace XmppSharp.Parsers; /// /// An enhanced XMPP parser built using Expat library. /// -public class ExpatXmppParser : BaseXmppParser +public partial class ExpatXmppParser : BaseXmppParser { private Parser _parser; private Element _currentElem; + private XmlNamespaceManager _nsStack; + private NameTable _xmlNames; + + void AddNamespacesToScope(IReadOnlyDictionary attrs) + { + foreach (var (key, value) in attrs) + { + if (key == "xmlns") + this._nsStack.AddNamespace(string.Empty, value); + else if (key.StartsWith("xmlns:")) + { + var prefix = key[(key.IndexOf(':') + 1)..]; + this._nsStack.AddNamespace(prefix, value); + } + } + } public ExpatXmppParser(ExpatEncodingType encoding = ExpatEncodingType.Utf8) { + this._nsStack = new(this._xmlNames = new NameTable()); + this._parser = new Parser(encoding); this._parser.OnElementStart += e => { - var entity = Xml.ExtractQualifiedName(e.Name); + this._nsStack.PushScope(); - string ns; + AddNamespacesToScope(e.Attributes); - if (entity.HasPrefix) - e.Attributes.TryGetValue($"xmlns:{entity.Prefix}", out ns); - else - e.Attributes.TryGetValue("xmlns", out ns); + var qname = Xml.ExtractQualifiedName(e.Name); + + var ns = this._nsStack.LookupNamespace(qname.HasPrefix ? qname.Prefix : string.Empty); if (e.Name is "iq" or "message" or "presence") // work-around ns ??= Namespace.Client; var element = ElementFactory.Create(e.Name, ns); + //foreach (var (key, value) in _nsStack.GetNamespacesInScope(XmlNamespaceScope.Local)) + //{ + // var att = string.IsNullOrWhiteSpace(key) ? "xmlns" : $"xmlns:{key}"; + // element.SetAttribute(att, value); + //} + + foreach (var (key, value) in e.Attributes) + element.SetAttribute(key, value); + if (e.Name == "stream:stream") AsyncHelper.RunSync(() => FireStreamStart(element as StreamStream)); else @@ -45,6 +74,8 @@ public ExpatXmppParser(ExpatEncodingType encoding = ExpatEncodingType.Utf8) this._parser.OnElementEnd += e => { + this._nsStack.PopScope(); + if (e.Value == "stream:stream") AsyncHelper.RunSync(() => FireStreamEnd()); else @@ -72,10 +103,21 @@ public ExpatXmppParser(ExpatEncodingType encoding = ExpatEncodingType.Utf8) { if (_currentElem != null) { + var trimWS = _currentElem.GetAttribute("xml:space") != "preserve"; + + // skip whitespace if not explicit declared. + if (string.IsNullOrWhiteSpace(e.Value) && trimWS) + return; + + var val = e.Value; + + if (trimWS) // same for trailing whitespace + val = TrimWhitespace(val); + if (_currentElem.LastNode is Text text) - text.Value += e.Value; + text.Value += val; else - _currentElem.AddChild(new Text(e.Value)); + _currentElem.AddChild(new Text(val)); } }; @@ -90,23 +132,78 @@ public ExpatXmppParser(ExpatEncodingType encoding = ExpatEncodingType.Utf8) }; } + [GeneratedRegex("\n")] + protected static partial Regex NewLineRegex(); + + [GeneratedRegex(@"\s+")] + protected static partial Regex ContiguousSpaceRegex(); + + static string TrimWhitespace(string str) + { + if (string.IsNullOrEmpty(str)) + return string.Empty; + + str = NewLineRegex().Replace(str, string.Empty); + str = str.Replace("\t", " "); + str = str.Trim(); + str = ContiguousSpaceRegex().Replace(str, " "); + + return str; + } + public void Reset() { this.EnsureNotDisposed(); + + while (this._nsStack.PopScope()) + ; + + // reset namespace stack. + this._nsStack = new(this._xmlNames); + + // reset the parser this._parser.Reset(); } - public void Write(byte[] buffer, int offset = 0, int length = -1, bool isFinalBlock = false) + public void Write(byte[] buffer, int offset, int length, bool isFinalBlock = false) + { + this.EnsureNotDisposed(); + + byte[] temp; + + try + { + temp = GC.AllocateUninitializedArray(length, true); + Buffer.BlockCopy(buffer, offset, temp, 0, length); + this._parser.Feed(temp, length, isFinalBlock); + } + finally + { + temp = null; + } + } + + public void Write(byte[] buffer, int length, bool isFinalBlock = false) { - length = length < 0 ? buffer.Length : length; - Write(buffer[offset..length], isFinalBlock); + this.EnsureNotDisposed(); + this._parser.Feed(buffer, length, isFinalBlock); } public void Write(byte[] buffer, bool isFinalBlock = false) - => this._parser.Feed(buffer, buffer.Length, isFinalBlock); + { + this.EnsureNotDisposed(); + this._parser.Feed(buffer, buffer.Length, isFinalBlock); + } - protected override void OnDispose() + protected override void Disposing() { + this._xmlNames = null; + + while (this._nsStack.PopScope()) + ; + + this._nsStack = null; + this._parser?.Dispose(); this._parser = null; } diff --git a/XmppSharp.Expat/XmppSharp.Expat.csproj b/XmppSharp.Expat/XmppSharp.Expat.csproj index 5171629..89562d4 100644 --- a/XmppSharp.Expat/XmppSharp.Expat.csproj +++ b/XmppSharp.Expat/XmppSharp.Expat.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0;net7.0 enable annotations @@ -26,7 +26,7 @@ - 1.0.0 + 1.0.1 true nathan130200 git diff --git a/XmppSharp.Test/ExpatParserTests.cs b/XmppSharp.Test/ExpatParserTests.cs index 20f085a..39cee07 100644 --- a/XmppSharp.Test/ExpatParserTests.cs +++ b/XmppSharp.Test/ExpatParserTests.cs @@ -1,6 +1,8 @@ -using Expat; +using System.IO.Compression; +using Expat; using XmppSharp.Dom; using XmppSharp.Parsers; +using XmppSharp.Protocol.Base; namespace XmppSharp.Test; @@ -26,6 +28,334 @@ public async Task ParseFromBuffer() var result = await tcs.Task; - Console.WriteLine("XML:\n" + result); + Console.WriteLine("XML:\n" + result.ToString(XmlFormatting.None)); + } + + [TestMethod] + public async Task ParseStreamError() + { + // Not declared: xmlns:stream="http://etherx.jabber.org/streams" + // but expat can parse even with missing namespace declaration 😛 + // test output will be: + + /* + + Element: stream:error + Element: bad-namespace-prefix + Attribute: xmlns=urn:ietf:params:xml:ns:xmpp-streams + + */ + + var xml = @" + +"; + + using var parser = new ExpatXmppParser(); + + var tcs = new TaskCompletionSource(); + + parser.OnStreamElement += e => + { + tcs.TrySetResult(e); + return Task.CompletedTask; + }; + + using var stream = new MemoryStream(xml.GetBytes()); + + stream.Position = 0; + + _ = Task.Run(async () => + { + // simulate IO + + try + { + var buf = new byte[16]; + int cnt; + + while (true) + { + cnt = await stream.ReadAsync(buf); + parser.Write(buf, cnt, cnt == 0); + + if (cnt == 0) + break; + } + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }); + + _ = Task.Delay(3000).ContinueWith(_ => tcs.TrySetCanceled()); + + var element = await tcs.Task; + + Assert.IsNotNull(element); + Assert.AreEqual("stream:error", element.TagName); + + Dump(element); + } + + [TestMethod] + public async Task ParseStreamStart() + { + var xml = new StreamStream + { + From = "localhost", + To = "user1", + Id = Guid.NewGuid().ToString(), + Version = "1.0", + Language = "en" + }.StartTag(); + + using var parser = new ExpatXmppParser(); + + var tcs = new TaskCompletionSource(); + + parser.OnStreamStart += e => + { + tcs.TrySetResult(e); + return Task.CompletedTask; + }; + + using var stream = new MemoryStream(xml.GetBytes()); + stream.Position = 0; + + _ = Task.Run(async () => + { + // simulate IO + + try + { + var buf = new byte[16]; + int cnt; + + while (true) + { + cnt = await stream.ReadAsync(buf); + parser.Write(buf, cnt, cnt == 0); + + if (cnt == 0) + break; + } + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }); + + _ = Task.Delay(3000).ContinueWith(_ => tcs.TrySetCanceled()); + + var element = await tcs.Task; + + Console.WriteLine("XML:\n" + element.ToString(XmlFormatting.Indented)); + } + + [TestMethod] + public async Task ParseFromString() + { + // .NET XmlReader consider those whitespaces after XML Decl invalid and cannot parse, while expat just skip since it's still a well-formed XML. + + var xml = @" + + + + + + + + + + + + + + + + + + + + + + + + +"; + + using var parser = new ExpatXmppParser(); + + var tcs = new TaskCompletionSource(); + + parser.OnStreamElement += e => + { + tcs.TrySetResult(e); + return Task.CompletedTask; + }; + + using var stream = new MemoryStream(xml.GetBytes()); + stream.Position = 0; + + _ = Task.Run(async () => + { + // simulate IO + + try + { + var buf = new byte[16]; + int cnt; + + while (true) + { + cnt = await stream.ReadAsync(buf); + parser.Write(buf, cnt, cnt == 0); + + if (cnt == 0) + break; + } + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }); + + _ = Task.Delay(3000).ContinueWith(_ => tcs.TrySetCanceled()); + + var element = await tcs.Task; + + Console.WriteLine("XML:\n" + element.ToString(XmlFormatting.Indented)); + } + + [TestMethod] + public async Task ParseFromZipFile() + { + using var fs = File.OpenRead(Path.Combine(Directory.GetCurrentDirectory(), "zipfile.zip")); + using var archive = new ZipArchive(fs, ZipArchiveMode.Read); + + var entry = archive.GetEntry("snippet.xml"); + Assert.IsNotNull(entry); + + using var parser = new ExpatXmppParser(); + + var tcs = new TaskCompletionSource(); + + parser.OnStreamElement += e => + { + tcs.TrySetResult(e); + return Task.CompletedTask; + }; + + using var stream = entry.Open(); + + _ = Task.Run(async () => + { + // simulate IO + + var buf = new byte[entry.Length / 8]; + int cnt; + + while (true) + { + cnt = await stream.ReadAsync(buf); + parser.Write(buf, cnt, cnt == 0); + + if (cnt == 0) + break; + } + }); + + _ = Task.Delay(3000).ContinueWith(_ => tcs.TrySetCanceled()); + + var element = await tcs.Task; + + Assert.AreEqual("CodeSnippets", element.TagName); + Assert.AreEqual("http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet", element.DefaultNamespace); + Assert.AreEqual("CodeSnippet", element.FirstChild.TagName); + + Console.WriteLine("XML:\n" + element.ToString(XmlFormatting.None) + "\n"); + + Dump(element); + } + + [TestMethod] + public async Task ParseRealSample() + { + var xml = @" + + + +"; + + using var parser = new ExpatXmppParser(); + + var tcs = new TaskCompletionSource(); + + parser.OnStreamElement += e => + { + tcs.TrySetResult(e); + return Task.CompletedTask; + }; + + using var stream = new MemoryStream(xml.GetBytes()); + stream.Position = 0; + + _ = Task.Run(async () => + { + // simulate IO + + try + { + var buf = new byte[16]; + int cnt; + + while (true) + { + cnt = await stream.ReadAsync(buf); + parser.Write(buf, cnt, cnt == 0); + + if (cnt == 0) + break; + } + } + catch (Exception e) + { + tcs.TrySetException(e); + } + }); + + _ = Task.Delay(3000).ContinueWith(_ => tcs.TrySetCanceled()); + + var element = await tcs.Task; + + Console.WriteLine("XML:\n" + element.ToString(XmlFormatting.Indented)); + } + + static void Dump(Element e, int depth = 0) + { + var tab = new string(' ', depth); + + Console.Write(tab + "Element: " + e.TagName); + + tab = new string(' ', depth + 1); + + if (string.IsNullOrWhiteSpace(e.Value)) + Console.WriteLine(); + else + Console.WriteLine(" (value: {0})", e.Value); + + foreach (var (key, value) in e.Attributes()) + { + if (key == "xmlns " && value == e.Parent?.GetAttribute(key)) + continue; + + Console.WriteLine(tab + "Attribute: {0}={1}", key, value); + } + + foreach (var child in e.Children()) + Dump(child, depth + 3); } } diff --git a/XmppSharp.Test/XmppSharp.Test.csproj b/XmppSharp.Test/XmppSharp.Test.csproj index e8517a2..bf8bda9 100644 --- a/XmppSharp.Test/XmppSharp.Test.csproj +++ b/XmppSharp.Test/XmppSharp.Test.csproj @@ -29,9 +29,6 @@ PreserveNewest - - PreserveNewest - PreserveNewest diff --git a/XmppSharp/AsyncDelegates.cs b/XmppSharp/AsyncDelegates.cs index e098c7e..804f8a4 100644 --- a/XmppSharp/AsyncDelegates.cs +++ b/XmppSharp/AsyncDelegates.cs @@ -39,6 +39,7 @@ public static class AsyncUtilities /// /// Delegate of the event that will be invoked. /// Returns a task that invokes the event asynchronously. + [StackTraceHidden] public static async Task InvokeAsync(this AsyncAction func) { try @@ -59,6 +60,7 @@ public static async Task InvokeAsync(this AsyncAction func) /// Delegate of the event that will be invoked. /// Parameter value. /// Returns a task that invokes the event asynchronously. + [StackTraceHidden] public static async Task InvokeAsync(this AsyncAction func, TParam param) { try @@ -77,6 +79,7 @@ public static async Task InvokeAsync(this AsyncAction func, TPar /// /// Type of result that will be returned. /// When awaited, returns the event value or the default value of if an error occurs. + [StackTraceHidden] public static async Task InvokeAsync(this AsyncFunc func) { try @@ -102,6 +105,7 @@ public static async Task InvokeAsync(this AsyncFunc f /// When awaited, returns the event value or the default value of if an error occurs. /// The error will be displayed in the debug window using . /// + [StackTraceHidden] public static async Task InvokeAsync(this AsyncFunc func, TParam param) { try diff --git a/XmppSharp/CHANGELOG.md b/XmppSharp/CHANGELOG.md index 729c58e..b8e9610 100644 --- a/XmppSharp/CHANGELOG.md +++ b/XmppSharp/CHANGELOG.md @@ -1,49 +1,70 @@ # XMPP# -[![github](https://img.shields.io/badge/XmppSharp-1?style=plastic&logo=github&label=Github)](https://github.com/nathan130200/XmppSharp) +[![github](https://img.shields.io/badge/XMPP%23_%20Core-ffe000?style=flat-square&logo=github&label=Github)](https://github.com/nathan130200/XmppSharp) A manipulation library and utility with the main objective to reduce the level of unnecessary verbosity for constructing XML tags for XMPP protocol, supporting `net6.0`, `net7.0` and `net8.0`. -# Lastest Updates +### Version History + +*3.0.0* -**3.0.0** -____ - Most big change! Bring back own XML implementation supporting nodes: - `Element` - `Text` - `Comment` - `Cdata` -**3.1.0** +____ + +*3.1.0* - Add enhanced utilities & helper methods to interact with element and nodes. -**3.1.1** ____ + +*3.1.1* + - Minor fixes. - Fixed wrong sub classing around some elements. - Added full control about XML formatting. - In .Net6 use `TryParseHelper` helper methods to parse attribute values. While in .Net7 or higher use `IParsable` abstract static interfaces feature, for parsing attribute values. -**3.1.2** ____ + +*3.1.2* + - Added missing `StartTag` and `EndTag` in elements. Both strings will contains well-formed XML string. - Ability to make a shallow copy of element instead a full copy with `Element.Clone(deep)` overload. - More formatting options in `XmlFormatting` structure. -**3.1.3** ____ + +*3.1.3* + - Enhance XMPP parser with different ctors provide an Stream or an factory function to create an stream. `XmppParser::Reset()` no longer needs an stream as argument. - Renamed `XmppParser::Advance` to `XmppParser::AdvanceAsync` for async version and leave `XmppParser::Advance` for sync method version. - Added `GetAwaiter` in XMPP parser for simple calling `await myParser;` have same behaviour and return same result as calling `await myParser.AdvanceAsync()` -**3.1.4** ____ + + +*3.1.4* + - Minor fixes around `XmppParser` and added helper method to advance and get next element. -**3.1.5** -___ +____ + +*3.1.5* + - Rename `XmppParser` to `DefaultXmppParser` to indicade this uses regular .NET `XmlReader` to parse xmpp packets. - Add basic abstraction layer to implement your own xmpp parser. Also i'm releasing a separated package `XmppSharp.Expat` to provide expat XMPP parser implementation. (Note: You must install native libraries to use expat. - Added `AsyncHelper` (from `AspNetCore` repo) to calling async functions in sync methods. -> In **XMPP#** repository i did an github actions to automatically build expat using vcpkg with most common systems: ubuntu, macos, windows but only x64 is working at this moment). Consider using [XmppShap.Expat](https://www.nuget.org/packages/XmppSharp.Expat/) package too if you need a fast and stable parser. \ No newline at end of file +> In **XMPP#** repository i did an github actions to automatically build expat using vcpkg with most common systems: ubuntu, macos, windows but only x64 is working at this moment). Consider using [XmppShap.Expat](https://www.nuget.org/packages/XmppSharp.Expat/) package too if you need a fast and stable parser. + +____ + +*3.1.6* + +- Minor improvements. +- Fixed wrong indent chars & side for default formatting options. +- Fixed `Element.Value` returning entire inner text from all descendant nodes. \ No newline at end of file diff --git a/XmppSharp/Dom/Element.cs b/XmppSharp/Dom/Element.cs index 1438e83..245089e 100644 --- a/XmppSharp/Dom/Element.cs +++ b/XmppSharp/Dom/Element.cs @@ -96,7 +96,7 @@ public IEnumerable DescendantsAndSelf() public override string Value { - get => string.Concat(this.DescendantNodes().OfType().Select(x => x.Value)); + get => string.Concat(this.Nodes().OfType().Select(x => x.Value)); set { this.Nodes().Remove(); @@ -113,6 +113,14 @@ public override string Value public override string ToString() => this.ToString(XmlFormatting.None); + /// + /// Gets the XML string representation of the current element and its child nodes. + /// + /// Determines if result xml will be indented. + /// Well-formed XML serialized with the entire XML tree. + public string ToString(bool indented) + => this.ToString(indented ? XmlFormatting.Indented : XmlFormatting.None); + /// /// Gets the XML string representation of the current element and its child nodes. /// diff --git a/XmppSharp/Parsers/BaseXmppParser.cs b/XmppSharp/Parsers/BaseXmppParser.cs index fcd50b6..403ba5a 100644 --- a/XmppSharp/Parsers/BaseXmppParser.cs +++ b/XmppSharp/Parsers/BaseXmppParser.cs @@ -36,7 +36,7 @@ protected void EnsureNotDisposed() throw new ObjectDisposedException(GetType().FullName); } - protected virtual void OnDispose() + protected virtual void Disposing() { } @@ -47,7 +47,7 @@ public void Dispose() return; _disposed = true; - OnDispose(); + Disposing(); } protected async Task FireStreamStart(StreamStream e) diff --git a/XmppSharp/Parsers/DefaultXmppParser.cs b/XmppSharp/Parsers/DefaultXmppParser.cs index 290e788..a6f77a0 100644 --- a/XmppSharp/Parsers/DefaultXmppParser.cs +++ b/XmppSharp/Parsers/DefaultXmppParser.cs @@ -69,7 +69,7 @@ public DefaultXmppParser(Func streamFactory, Encoding? encoding = defaul Reset(); } - protected override void OnDispose() + protected override void Disposing() { if (this._disposed) return; diff --git a/XmppSharp/Xml.cs b/XmppSharp/Xml.cs index 6528e21..79e7d13 100644 --- a/XmppSharp/Xml.cs +++ b/XmppSharp/Xml.cs @@ -36,13 +36,13 @@ public static XmlQualifiedName ExtractQualifiedName(string source) }; } - internal static XmlWriter CreateWriter(StringBuilder output, in XmlFormatting formatting) + internal static XmlWriter CreateWriter(StringBuilder output, XmlFormatting formatting) { Require.NotNull(output); var settings = new XmlWriterSettings { - Indent = formatting.IndentSize > 0, + Indent = formatting.IndentSize != 0, IndentChars = formatting.IndentChars, DoNotEscapeUriAttributes = formatting.DoNotEscapeUriAttributes, WriteEndDocumentOnClose = formatting.WriteEndDocumentOnClose, diff --git a/XmppSharp/XmlFormatting.cs b/XmppSharp/XmlFormatting.cs index 27ca47f..b2814c0 100644 --- a/XmppSharp/XmlFormatting.cs +++ b/XmppSharp/XmlFormatting.cs @@ -1,4 +1,5 @@ -using System.Xml; +using System.Diagnostics; +using System.Xml; namespace XmppSharp; @@ -79,7 +80,7 @@ public XmlFormatting() DoNotEscapeUriAttributes = false; IndentSize = 0; - IndentChars = "\t"; + IndentChars = " "; NewLineHandling = NewLineHandling.Replace; NewLineOnAttributes = false; @@ -96,6 +97,6 @@ public XmlFormatting() /// public static XmlFormatting Indented { get; } = None with { - IndentSize = 2, + IndentSize = 4, }; } diff --git a/XmppSharp/XmppSharp.csproj b/XmppSharp/XmppSharp.csproj index d6081c1..16fe6df 100644 --- a/XmppSharp/XmppSharp.csproj +++ b/XmppSharp/XmppSharp.csproj @@ -26,7 +26,7 @@ - 3.1.5 + 3.1.6 true nathan130200 git