diff --git a/Pgnoli.Testing/Messages/Backend/BackendParserTest.cs b/Pgnoli.Testing/Messages/Backend/BackendParserTest.cs index f62a067..b7a1acf 100644 --- a/Pgnoli.Testing/Messages/Backend/BackendParserTest.cs +++ b/Pgnoli.Testing/Messages/Backend/BackendParserTest.cs @@ -26,6 +26,7 @@ private class StubContext : IMessageContext [TestCase("Backend.Query.CloseComplete.Default", typeof(CloseComplete))] [TestCase("Backend.Query.CommandComplete.SelectRowCount", typeof(CommandComplete))] [TestCase("Backend.Query.DataRow.SingleInt", typeof(DataRow))] + [TestCase("Backend.Query.ErrorResponse.Default", typeof(ErrorResponse))] [TestCase("Backend.Query.ParseComplete.Default", typeof(ParseComplete))] [TestCase("Backend.Query.RowDescription.SingleInt", typeof(RowDescription))] public void Parse(string msgPath, Type expected) @@ -37,7 +38,5 @@ public void Parse(string msgPath, Type expected) var msg = parser.Parse(bytes); Assert.That(msg, Is.TypeOf(expected)); } - - } } diff --git a/Pgnoli.Testing/Messages/Backend/Query/CommandCompleteTest.cs b/Pgnoli.Testing/Messages/Backend/Query/CommandCompleteTest.cs index 39be8bd..d59af9b 100644 --- a/Pgnoli.Testing/Messages/Backend/Query/CommandCompleteTest.cs +++ b/Pgnoli.Testing/Messages/Backend/Query/CommandCompleteTest.cs @@ -9,16 +9,29 @@ namespace Pgnoli.Testing.Messages.Backend.Query { public class CommandCompleteTest { + private static CommandComplete.CommandCompleteBuilder[] BuilderCases + => new[] + { + CommandComplete.Select(1978), + CommandComplete.Delete(1978), + CommandComplete.Merge(1978), + CommandComplete.Move(1978), + CommandComplete.Update(1978), + CommandComplete.Fetch(1978), + CommandComplete.Copy(1978), + }; + [Test] - public void Roundtrip_SelectRowCount_Success() + [TestCaseSource(nameof(BuilderCases))] + public void Roundtrip_SelectRowCount_Success(CommandComplete.CommandCompleteBuilder builder) { - var msg = CommandComplete.Select(1978).Build(); + var msg = builder.Build(); Assert.That(msg.Payload.RowCount, Is.EqualTo(1978)); var bytes = msg.GetBytes(); Assert.That(bytes, Is.Not.Null); Assert.That(bytes, Has.Length.GreaterThan(0)); - Assert.That(bytes[0], Is.GreaterThanOrEqualTo('A').And.LessThanOrEqualTo('Z')); + Assert.That(bytes[0], Is.EqualTo('C')); var roundtrip = new CommandComplete(bytes); Assert.DoesNotThrow(() => roundtrip.Read()); @@ -49,7 +62,6 @@ public void Roundtrip_InsertNoRowCount_Success() }); } - [Test] public void Write_SelectRowCount_Success() { diff --git a/Pgnoli.Testing/Messages/Backend/Query/ErrorResponseTest.cs b/Pgnoli.Testing/Messages/Backend/Query/ErrorResponseTest.cs new file mode 100644 index 0000000..d239ebf --- /dev/null +++ b/Pgnoli.Testing/Messages/Backend/Query/ErrorResponseTest.cs @@ -0,0 +1,68 @@ +using Pgnoli.Messages; +using Pgnoli.Messages.Backend.Query; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pgnoli.Testing.Messages.Backend.Query +{ + public class ErrorResponseTest + { + [Test] + public void Roundtrip_Ok_Success() + { + var msg = ErrorResponse.Warning("SQL00001", "!Oups!").With('q', "SELECT @@version").Build(); + var bytes = msg.GetBytes(); + Assert.That(bytes, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(bytes, Has.Length.EqualTo(51)); + Assert.That(bytes[0], Is.EqualTo('E')); + Assert.That(bytes[50], Is.EqualTo(0)); + }); + + var roundtrip = new ErrorResponse(bytes); + Assert.DoesNotThrow(() => roundtrip.Read()); + Assert.Multiple(() => + { + Assert.That(roundtrip.Payload.Severity, Is.EqualTo(msg.Payload.Severity)); + Assert.That(roundtrip.Payload.SqlState, Is.EqualTo(msg.Payload.SqlState)); + Assert.That(roundtrip.Payload.Message, Is.EqualTo(msg.Payload.Message)); + Assert.That(roundtrip.Payload.OptionalMessages, Has.Count.EqualTo(msg.Payload.OptionalMessages.Count)); + foreach (var option in roundtrip.Payload.OptionalMessages) + { + Assert.That(msg.Payload.OptionalMessages.ContainsKey(option.Key), Is.True); + Assert.That(roundtrip.Payload.OptionalMessages[option.Key], Is.EqualTo(msg.Payload.OptionalMessages[option.Key])); + } + }); + } + + [Test] + public void Write_Default_Success() + { + var msg = ErrorResponse.Warning("SQL00001", "!Oups!").With('q', "SELECT @@version").Build(); + + var bytes = msg.GetBytes(); + + var reader = new ResourceBytesReader(); + Assert.That(bytes, Is.EqualTo(reader.Read("Backend.Query.ErrorResponse.Default"))); + } + + [Test] + public void Read_Default_Success() + { + var reader = new ResourceBytesReader(); + var bytes = reader.Read("Backend.Query.ErrorResponse.Default"); + var msg = new ErrorResponse(bytes); + + Assert.DoesNotThrow(() => msg.Read()); + Assert.That(msg.Payload.Severity, Is.EqualTo(ErrorSeverity.Warning)); + Assert.That(msg.Payload.SqlState, Is.EqualTo("SQL00001")); + Assert.That(msg.Payload.Message, Is.EqualTo("!Oups!")); + Assert.That(msg.Payload.OptionalMessages.ContainsKey('q'), Is.True); + Assert.That(msg.Payload.OptionalMessages['q'], Is.EqualTo("SELECT @@version")); + } + } +} diff --git a/Pgnoli.Testing/Messages/Backend/Query/ParseCompleteTest.cs b/Pgnoli.Testing/Messages/Backend/Query/ParseCompleteTest.cs index 88a0e89..4311caa 100644 --- a/Pgnoli.Testing/Messages/Backend/Query/ParseCompleteTest.cs +++ b/Pgnoli.Testing/Messages/Backend/Query/ParseCompleteTest.cs @@ -12,8 +12,7 @@ public class ParseCompleteTest [Test] public void Write_Default_Success() { - var msg = new ParseComplete(); - msg.Write(); + var msg = ParseComplete.Message.Build(); var bytes = msg.GetBytes(); var reader = new ResourceBytesReader(); diff --git a/Pgnoli.Testing/Messages/CodeMessageTest.cs b/Pgnoli.Testing/Messages/CodeMessageTest.cs new file mode 100644 index 0000000..72ea963 --- /dev/null +++ b/Pgnoli.Testing/Messages/CodeMessageTest.cs @@ -0,0 +1,84 @@ +using Pgnoli.Messages; +using Pgnoli.Messages.Backend.Handshake; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading.Tasks; + +namespace Pgnoli.Testing.Messages +{ + public class CodeMessageTest + { + private class StubMessage : CodeMessage + { + public const char Code = '='; + + public StubMessage(byte[] bytes) + : base(Code, bytes) { } + + protected override int GetPayloadLength() => 0; + protected internal override void ReadPayload(Buffer buffer) { } + protected internal override void WritePayload(Buffer buffer) { } + } + + private class StubBrokenMessage : StubMessage + { + public StubBrokenMessage(byte[] bytes) + : base(bytes) { } + + protected override int GetPayloadLength() => 1; + } + + [Test] + public void Read_ValidBytes_DoesNotThrow() + { + var bytes = new Byte[] { Convert.ToByte(StubMessage.Code), 0, 0, 0, 4 }; + var msg = new StubMessage(bytes); + Assert.DoesNotThrow(() => msg.Read()); + } + + [Test] + public void Read_UnexpectedCode_Throws() + { + var bytes = new Byte[] { (byte)(Convert.ToByte(StubMessage.Code)-1), 0, 0, 0, 4 }; + var msg = new StubMessage(bytes); + Assert.Throws(() => msg.Read()); + } + + [Test] + public void Read_UnexpectedLength_Throws() + { + var bytes = new Byte[] { Convert.ToByte(StubMessage.Code), 0, 0, 0, 20 }; + var msg = new StubMessage(bytes); + Assert.Throws(() => msg.Read()); + } + + [Test] + public void Read_MessageNotFullyConsumedException_Throws() + { + var bytes = new Byte[] { Convert.ToByte(StubMessage.Code), 0, 0, 0, 5, 1 }; + var msg = new StubBrokenMessage(bytes); + Assert.Throws(() => msg.Read()); + } + + + [Test] + public void Read_TooLongBuffer_TrimmedDoesNotThrow() + { + var bytes = new Byte[] { Convert.ToByte(StubMessage.Code), 0, 0, 0, 4, 1, 0, 10 }; + var msg = new StubMessage(bytes); + Assert.DoesNotThrow(() => msg.Read()); + Assert.That(msg.GetBytes(), Is.EqualTo(bytes[..(bytes[4]+1)])); + } + + [Test] + public void Read_EmptyBuffer_Throws() + { + var bytes = Array.Empty(); + var msg = new StubMessage(bytes); + Assert.Throws(() => msg.Read()); + } + } +} diff --git a/Pgnoli.Testing/Messages/Frontend/FrontendParserTest.cs b/Pgnoli.Testing/Messages/Frontend/FrontendParserTest.cs new file mode 100644 index 0000000..0f1c5f9 --- /dev/null +++ b/Pgnoli.Testing/Messages/Frontend/FrontendParserTest.cs @@ -0,0 +1,36 @@ +using Pgnoli.Options.DateStyles; +using Pgnoli.Messages; +using Pgnoli.Messages.Frontend; +using Pgnoli.Messages.Frontend.Query; +using Pgnoli.Types.TypeHandlers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection.PortableExecutable; +using System.Text; +using System.Threading.Tasks; + +namespace Pgnoli.Testing.Messages.Frontend +{ + public class FrontendParserTest + { + [Test] + [TestCase("Frontend.Query.Bind.UnnamedPortal", typeof(Bind))] + [TestCase("Frontend.Query.Close.UnnamedPortal", typeof(Close))] + [TestCase("Frontend.Query.Describe.UnnamedPortal", typeof(Describe))] + [TestCase("Frontend.Query.Execute.UnnamedPortal", typeof(Execute))] + [TestCase("Frontend.Query.Query.To_timestamp", typeof(Pgnoli.Messages.Frontend.Query.Query))] + [TestCase("Frontend.Query.Parse.To_timestamp", typeof(Parse))] + [TestCase("Frontend.Query.Sync.Default", typeof(Sync))] + public void Parse(string msgPath, Type expected) + { + var reader = new ResourceBytesReader(); + var bytes = reader.Read(msgPath); + + var parser = new FrontendParser(); + var msg = parser.Parse(bytes, 0, out var length); + Assert.That(msg, Is.TypeOf(expected)); + Assert.That(length, Is.EqualTo(bytes.Length)); + } + } +} diff --git a/Pgnoli.Testing/Messages/Frontend/Query/CloseTest.cs b/Pgnoli.Testing/Messages/Frontend/Query/CloseTest.cs new file mode 100644 index 0000000..615f0c3 --- /dev/null +++ b/Pgnoli.Testing/Messages/Frontend/Query/CloseTest.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Pgnoli.Messages.Frontend.Query; + +namespace Pgnoli.Testing.Messages.Frontend.Query +{ + public class CloseTest + { + [Test] + public void Write_Default_Success() + { + var msg = Close.UnnamedPortal.Build(); + var bytes = msg.GetBytes(); + + var reader = new ResourceBytesReader(); + Assert.That(bytes, Is.EqualTo(reader.Read("Frontend.Query.Close.UnnamedPortal"))); + } + + [Test] + public void Read_UnnamedPortal_Success() + { + var reader = new ResourceBytesReader(); + var bytes = reader.Read("Frontend.Query.Close.UnnamedPortal"); + var msg = new Close(bytes); + + Assert.DoesNotThrow(() => msg.Read()); + Assert.That(msg.Payload.PortalType, Is.EqualTo(PortalType.Portal)); + Assert.That(msg.Payload.Name, Is.EqualTo(string.Empty)); + } + + private static Close.CloseBuilder[] BuilderCases + => new[] + { + Close.Portal("the_name"), + Close.PreparedStatement("the_name"), + Close.UnnamedPortal, + Close.UnnamedPreparedStatement + }; + + [Test] + [TestCaseSource(nameof(BuilderCases))] + public void Roundtrip_Close_Success(Close.CloseBuilder builder) + { + var msg = builder.Build(); + + var bytes = msg.GetBytes(); + Assert.That(bytes, Is.Not.Null); + Assert.That(bytes, Has.Length.GreaterThan(0)); + Assert.That(bytes[0], Is.EqualTo('C')); + + var roundtrip = new Close(bytes); + Assert.DoesNotThrow(() => roundtrip.Read()); + Assert.Multiple(() => + { + Assert.That(msg.Payload.Name, Is.EqualTo(roundtrip.Payload.Name)); + Assert.That(msg.Payload.PortalType, Is.EqualTo(roundtrip.Payload.PortalType)); + }); + } + } +} diff --git a/Pgnoli.Testing/Messages/Frontend/Query/DescribeTest.cs b/Pgnoli.Testing/Messages/Frontend/Query/DescribeTest.cs index a03f6b0..83dddc5 100644 --- a/Pgnoli.Testing/Messages/Frontend/Query/DescribeTest.cs +++ b/Pgnoli.Testing/Messages/Frontend/Query/DescribeTest.cs @@ -12,7 +12,7 @@ public class DescribeTest [Test] public void Write_UnnamedPortal_Success() { - var msg = Describe.Portal.Build(); + var msg = Describe.UnnamedPortal.Build(); var bytes = msg.GetBytes(); var reader = new ResourceBytesReader(); @@ -27,25 +27,36 @@ public void Read_UnnamedPortal_Success() var msg = new Describe(bytes); Assert.DoesNotThrow(() => msg.Read()); + Assert.That(msg.Payload.Name, Is.EqualTo(string.Empty)); + Assert.That(msg.Payload.PortalType, Is.EqualTo(PortalType.Portal)); } + private static Describe.DescribeBuilder[] BuilderCases + => new[] + { + Describe.Portal("the_name"), + Describe.PreparedStatement("the_name"), + Describe.UnnamedPortal, + Describe.UnnamedPreparedStatement + }; + [Test] - public void Roundtrip_NamedPortal_Success() + [TestCaseSource(nameof(BuilderCases))] + public void Roundtrip_Close_Success(Describe.DescribeBuilder builder) { - var msg = Describe.Portal.Named("myName").Build(); - Assert.Multiple(() => - { - Assert.That(msg.Payload.PortalType, Is.EqualTo(PortalType.Portal)); - Assert.That(msg.Payload.Name, Is.EqualTo("myName")); - }); + var msg = builder.Build(); + var bytes = msg.GetBytes(); + Assert.That(bytes, Is.Not.Null); + Assert.That(bytes, Has.Length.GreaterThan(0)); + Assert.That(bytes[0], Is.EqualTo('D')); var roundtrip = new Describe(bytes); Assert.DoesNotThrow(() => roundtrip.Read()); Assert.Multiple(() => { - Assert.That(msg.Payload.PortalType, Is.EqualTo(roundtrip.Payload.PortalType)); Assert.That(msg.Payload.Name, Is.EqualTo(roundtrip.Payload.Name)); + Assert.That(msg.Payload.PortalType, Is.EqualTo(roundtrip.Payload.PortalType)); }); } } diff --git a/Pgnoli.Testing/Messages/Frontend/Query/QueryTest.cs b/Pgnoli.Testing/Messages/Frontend/Query/QueryTest.cs new file mode 100644 index 0000000..f6f6ea9 --- /dev/null +++ b/Pgnoli.Testing/Messages/Frontend/Query/QueryTest.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using FQ = Pgnoli.Messages.Frontend.Query; + +namespace Pgnoli.Testing.Messages.Frontend.Query +{ + public class QueryTest + { + [Test] + public void Write_To_timestamp_Success() + { + var msg = FQ.Query.Message("select to_timestamp('2022-12-10 15:18:16+7', 'YYYY-MM-DD HH24:MI:SSTZH')::timestamptz").Build(); + var bytes = msg.GetBytes(); + + var reader = new ResourceBytesReader(); + Assert.That(bytes, Is.EqualTo(reader.Read("Frontend.Query.Query.To_timestamp"))); + } + + [Test] + public void Read_To_timestamp_Success() + { + var reader = new ResourceBytesReader(); + var bytes = reader.Read("Frontend.Query.Query.To_timestamp"); + var msg = new FQ.Query(bytes); + + Assert.DoesNotThrow(() => msg.Read()); + Assert.That(msg.Payload.Sql, Is.Not.Null.And.Not.Empty); + } + } +} diff --git a/Pgnoli.Testing/Pgnoli.Testing.csproj b/Pgnoli.Testing/Pgnoli.Testing.csproj index d08a67a..9b6a4b7 100644 --- a/Pgnoli.Testing/Pgnoli.Testing.csproj +++ b/Pgnoli.Testing/Pgnoli.Testing.csproj @@ -6,14 +6,17 @@ + + + @@ -23,13 +26,16 @@ + + + diff --git a/Pgnoli.Testing/Resources/Backend/Query/ErrorResponse/Default.txt b/Pgnoli.Testing/Resources/Backend/Query/ErrorResponse/Default.txt new file mode 100644 index 0000000..1465a3d --- /dev/null +++ b/Pgnoli.Testing/Resources/Backend/Query/ErrorResponse/Default.txt @@ -0,0 +1,51 @@ +69 E ErrorResponse +0 +0 +0 +50 Length = 50 +83 S => Severity +87 W +97 a +114 r +110 n +105 i +110 n +103 g +0 +67 C => Code +83 S +81 Q +76 L +48 0 +48 0 +48 0 +48 0 +49 1 +0 +77 M => Message +33 ! +79 O +117 u +112 p +115 s +33 ! +0 +113 q +83 S +69 E +76 L +69 E +67 C +84 T +32 +64 @ +64 @ +118 v +101 e +114 r +115 s +105 i +111 o +110 n +0 +0 Terminal \ No newline at end of file diff --git a/Pgnoli.Testing/Resources/Frontend/Query/Close/UnnamedPortal.txt b/Pgnoli.Testing/Resources/Frontend/Query/Close/UnnamedPortal.txt new file mode 100644 index 0000000..abdd6e8 --- /dev/null +++ b/Pgnoli.Testing/Resources/Frontend/Query/Close/UnnamedPortal.txt @@ -0,0 +1,7 @@ +67 C (Close) +0 +0 +0 +6 6 = length +80 P => Portal +0 unnamed portal \ No newline at end of file diff --git a/Pgnoli.Testing/Resources/Frontend/Query/Parse/To_timestamp.txt b/Pgnoli.Testing/Resources/Frontend/Query/Parse/To_timestamp.txt index 9d9d4dd..45b2684 100644 --- a/Pgnoli.Testing/Resources/Frontend/Query/Parse/To_timestamp.txt +++ b/Pgnoli.Testing/Resources/Frontend/Query/Parse/To_timestamp.txt @@ -91,4 +91,4 @@ 122 z 0 0 -0 \ No newline at end of file +0 \ No newline at end of file diff --git a/Pgnoli.Testing/Resources/Frontend/Query/Query/To_timestamp.txt b/Pgnoli.Testing/Resources/Frontend/Query/Query/To_timestamp.txt new file mode 100644 index 0000000..0a8a352 --- /dev/null +++ b/Pgnoli.Testing/Resources/Frontend/Query/Query/To_timestamp.txt @@ -0,0 +1,91 @@ +81 Q +0 +0 +0 +90 +115 s +101 e +108 l +101 e +99 c +116 t +32 +116 t +111 o +95 _ +116 t +105 i +109 m +101 e +115 s +116 t +97 a +109 m +112 p +40 ( +39 ' +50 2 +48 0 +50 2 +50 2 +45 - +49 1 +50 2 +45 - +49 1 +48 0 +32 +49 1 +53 5 +58 : +49 1 +56 8 +58 : +49 1 +54 6 +43 + +55 7 +39 ' +44 , +32 +39 ' +89 Y +89 Y +89 Y +89 Y +45 - +77 M +77 M +45 - +68 D +68 D +32 +72 H +72 H +50 2 +52 4 +58 : +77 M +73 I +58 : +83 S +83 S +84 T +90 Z +72 H +39 ' +41 ) +58 : +58 : +116 t +105 i +109 m +101 e +115 s +116 t +97 a +109 m +112 p +116 t +122 z +0 \ No newline at end of file diff --git a/Pgnoli.Testing/Types/TypeHandlers/Text/DateTypeHandlerTest.cs b/Pgnoli.Testing/Types/TypeHandlers/Text/DateTypeHandlerTest.cs index a556fe8..1bf3542 100644 --- a/Pgnoli.Testing/Types/TypeHandlers/Text/DateTypeHandlerTest.cs +++ b/Pgnoli.Testing/Types/TypeHandlers/Text/DateTypeHandlerTest.cs @@ -73,7 +73,6 @@ public void Read_IsoYMD_Success(string value, DateTime expected) Assert.That(result, Is.EqualTo(DateOnly.FromDateTime(expected))); } - [Test] [TestCase("48-49-45-48-49-45-50-48-48-48", "2000-01-01")] [TestCase("49-50-45-50-56-45-49-57-55-56", "1978-12-28")] diff --git a/Pgnoli/Buffer.cs b/Pgnoli/Buffer.cs index e6544fa..c551a5b 100644 --- a/Pgnoli/Buffer.cs +++ b/Pgnoli/Buffer.cs @@ -91,7 +91,7 @@ protected T ApplyEndianness(T value) int x => (T)Convert.ChangeType(BinaryPrimitives.ReverseEndianness(x), typeof(T)), long x => (T)Convert.ChangeType(BinaryPrimitives.ReverseEndianness(x), typeof(T)), _ => throw new ArgumentOutOfRangeException(nameof(value)) - }; ; + }; } public byte ReadByte() diff --git a/Pgnoli/BufferException.cs b/Pgnoli/BufferException.cs index 94a703c..9c39364 100644 --- a/Pgnoli/BufferException.cs +++ b/Pgnoli/BufferException.cs @@ -31,6 +31,12 @@ public BufferOverflowException(int size, int position, int bytesCount) : base($"The operation tries to read or write after the end of the buffer. The buffer has a total length of '{size}' and is positionned at the index '{position}' but the code was trying to read or write '{bytesCount}' bytes.") { } } + public class BufferEmptyException : BufferPgnoliException + { + public BufferEmptyException() + : base($"The operation tries to read a buffer but this buffer is empty (length equal to 0).") { } + } + public class BufferUnexpectedCharException : BufferPgnoliException { public BufferUnexpectedCharException(char unexpectedChar) diff --git a/Pgnoli/Messages/Backend/BackendParser.cs b/Pgnoli/Messages/Backend/BackendParser.cs index 6914483..83d6aa6 100644 --- a/Pgnoli/Messages/Backend/BackendParser.cs +++ b/Pgnoli/Messages/Backend/BackendParser.cs @@ -36,6 +36,7 @@ protected Message Decode(byte[] bytes) Query.CloseComplete.Code => new Query.CloseComplete(bytes), Query.CommandComplete.Code => new Query.CommandComplete(bytes), Query.DataRow.Code => new Query.DataRow(bytes, Context.FieldDescriptions, Context.TypeHandlerFactory), + Query.ErrorResponse.Code => new Query.ErrorResponse(bytes), Query.ParseComplete.Code => new Query.ParseComplete(bytes), Query.RowDescription.Code => new Query.RowDescription(bytes), _ => throw new ArgumentException(nameof(bytes)), diff --git a/Pgnoli/Messages/Backend/Query/ErrorResponse.cs b/Pgnoli/Messages/Backend/Query/ErrorResponse.cs new file mode 100644 index 0000000..202aa14 --- /dev/null +++ b/Pgnoli/Messages/Backend/Query/ErrorResponse.cs @@ -0,0 +1,107 @@ +using System.Text; +using System.Xml.Linq; + +namespace Pgnoli.Messages.Backend.Query +{ + public sealed class ErrorResponse : CodeMessage + { + public const char Code = 'E'; + + public ErrorResponsePayload Payload { get; private set; } + + internal ErrorResponse(ErrorResponsePayload payload) + : base(Code) { Payload = payload; } + + internal ErrorResponse(byte[] bytes) + : base(Code, bytes) { } + + protected override int GetPayloadLength() + => 1 + Enum.GetName(typeof(ErrorSeverity), Payload.Severity)!.Length + 1 + + 1 + Payload.SqlState.Length + 1 + + 1 + Payload.Message.Length + 1 + + Payload.OptionalMessages.Sum(x => 1 + x.Value.Length + 1) + + 1; + + protected internal override void WritePayload(Buffer buffer) + { + foreach (var item in Payload.OptionalMessages + .Prepend(new KeyValuePair('M', Payload.Message)) + .Prepend(new KeyValuePair('C', Payload.SqlState)) + .Prepend(new KeyValuePair('S', Enum.GetName(typeof(ErrorSeverity), Payload.Severity)!))) + { + buffer.WriteAsciiChar(item.Key); + buffer.WriteString(item.Value); + buffer.WriteByte(0); + } + buffer.WriteByte(0); + } + + protected internal override void ReadPayload(Buffer buffer) + { + Payload = Payload with { OptionalMessages = new() }; + var key = buffer.ReadAsciiChar(); + while (key != '\0') + { + var value = buffer.ReadStringUntilNullTerminator(); + switch (key) + { + case 'S': Payload = Payload with { Severity = (ErrorSeverity)Enum.Parse(typeof(ErrorSeverity), value) }; break; + case 'C': Payload = Payload with { SqlState = value }; break; + case 'M': Payload = Payload with { Message = value }; break; + default: + Payload.OptionalMessages.Add(key, value); + break; + } + key = buffer.ReadAsciiChar(); + } + } + + public static ErrorResponseBuilder Error(string sqlState, string message) + => new(ErrorSeverity.Error, sqlState, message); + + public static ErrorResponseBuilder Fatal(string sqlState, string message) + => new(ErrorSeverity.Fatal, sqlState, message); + + public static ErrorResponseBuilder Panic(string sqlState, string message) + => new(ErrorSeverity.Panic, sqlState, message); + + public static ErrorResponseBuilder Warning(string sqlState, string message) + => new(ErrorSeverity.Warning, sqlState, message); + + public static ErrorResponseBuilder Notice(string sqlState, string message) + => new(ErrorSeverity.Notice, sqlState, message); + + public static ErrorResponseBuilder Debug(string sqlState, string message) + => new(ErrorSeverity.Debug, sqlState, message); + + public static ErrorResponseBuilder Info(string sqlState, string message) + => new(ErrorSeverity.Info, sqlState, message); + + public static ErrorResponseBuilder Log(string sqlState, string message) + => new(ErrorSeverity.Log, sqlState, message); + + public record struct ErrorResponsePayload(ErrorSeverity Severity, string SqlState, string Message, Dictionary OptionalMessages) + { } + + public class ErrorResponseBuilder : IMessageBuilder + { + private ErrorResponsePayload Payload { get; set; } + + public ErrorResponseBuilder(ErrorSeverity severity, string sqlState, string message) + => Payload = new ErrorResponsePayload(severity, sqlState, message, new Dictionary()); + + public ErrorResponseBuilder With(char key, string value) + { + Payload.OptionalMessages.Add(key, value); + return this; + } + + public ErrorResponse Build() + { + var msg = new ErrorResponse(Payload); + msg.Write(); + return msg; + } + } + } +} diff --git a/Pgnoli/Messages/Backend/Query/ParseComplete.cs b/Pgnoli/Messages/Backend/Query/ParseComplete.cs index f860038..183197d 100644 --- a/Pgnoli/Messages/Backend/Query/ParseComplete.cs +++ b/Pgnoli/Messages/Backend/Query/ParseComplete.cs @@ -6,14 +6,14 @@ namespace Pgnoli.Messages.Backend.Query { - public class ParseComplete : CodeMessage + public sealed class ParseComplete : CodeMessage { public const char Code = '1'; - protected internal ParseComplete(byte[] bytes) + internal ParseComplete(byte[] bytes) : base(Code, bytes) { } - protected internal ParseComplete() + internal ParseComplete() : base(Code) { } protected override int GetPayloadLength() diff --git a/Pgnoli/Messages/Backend/Query/RowDescription.cs b/Pgnoli/Messages/Backend/Query/RowDescription.cs index 9019ac7..983ad15 100644 --- a/Pgnoli/Messages/Backend/Query/RowDescription.cs +++ b/Pgnoli/Messages/Backend/Query/RowDescription.cs @@ -69,6 +69,9 @@ public class RowDescriptionBuilder : IMessageBuilder public RowDescriptionPayload Payload { get; private set; } = new(); private FieldDescriptionFactory Factory { get; set; } = new(); + internal RowDescriptionBuilder() + => (Payload, Factory) = (new(), new()); + public RowDescriptionBuilder With(FieldDescription field) { Payload.Fields.Add(field); diff --git a/Pgnoli/Messages/CodeMessage.cs b/Pgnoli/Messages/CodeMessage.cs index 096aac3..33e0f4d 100644 --- a/Pgnoli/Messages/CodeMessage.cs +++ b/Pgnoli/Messages/CodeMessage.cs @@ -26,26 +26,13 @@ internal override byte[] Write() return Buffer.GetBytes(); } - public override int Read() + protected internal override void ReadPrefix(Buffer buffer) { - if (Buffer is null) - throw new ArgumentNullException(nameof(Buffer)); - - Buffer.Reset(); - if (Buffer.ReadAsciiChar() != MessageType) - throw new ArgumentOutOfRangeException(nameof(MessageType)); - - Length = Buffer.ReadInt(); - if (Buffer.Length < Length + 1) - throw new ArgumentOutOfRangeException(nameof(MessageType)); - else if (Buffer.Length > Length + 1) - Buffer.TrimEnd(Length + 1); - - ReadPayload(Buffer); - - if (!Buffer.IsEnd()) - throw new ArgumentException(nameof(Buffer)); - return Length + 1; + var code = buffer.ReadAsciiChar(); + if (code != MessageType) + throw new MessageMismatchCodeException(this.GetType(), MessageType, code); } + + protected internal override int PrefixLength => 1; } } diff --git a/Pgnoli/Messages/ErrorResponse.cs b/Pgnoli/Messages/ErrorResponse.cs deleted file mode 100644 index 3baf47b..0000000 --- a/Pgnoli/Messages/ErrorResponse.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Text; -using static Pgnoli.Messages.Backend.Query.RowDescription; - -namespace Pgnoli.Messages -{ - internal class ErrorResponse : CodeMessage - { - protected ErrorResponseBuilder? Payload { get; } - - private Dictionary? items; - public Dictionary Items - { - get - { - items ??= Initialize(); - return items; - } - } - - public ErrorResponse(ErrorResponseBuilder payload) - : base('T') { Payload = payload; } - - public ErrorResponse(byte[] bytes) - : base('T', bytes) { } - - protected override int GetPayloadLength() - => Items.Values.Sum(x => x.Length); - - protected virtual Dictionary Initialize() - { - if (Payload is null) - throw new ArgumentNullException(nameof(Payload)); - - var label = Enum.GetName(typeof(ErrorSeverity), Payload.Severity) - ?? throw new ArgumentOutOfRangeException(nameof(Payload.Severity)); - - return new Dictionary() - { - { 'S', label }, - { 'V', label }, - { 'M', Payload.Message ?? throw new ArgumentNullException(nameof(Payload.Message)) }, - }; - } - - protected internal override void WritePayload(Buffer buffer) - { - foreach (var item in Items) - { - buffer.WriteAsciiChar(item.Key); - buffer.WriteString(item.Value); - buffer.WriteByte(0); - } - buffer.WriteByte(0); - } - - protected internal override void ReadPayload(Buffer buffer) - { - Items.Clear(); - - var key = buffer.ReadAsciiChar(); - while(key!='\0') - { - var value = buffer.ReadStringUntilNullTerminator(); - Items.Add(key, value); - key = buffer.ReadAsciiChar(); - } - } - - internal class ErrorResponseBuilder : IMessageBuilder - { - public ErrorSeverity Severity { get; private set; } = ErrorSeverity.Error; - public string? Message { get; private set; } - - public ErrorResponseBuilder With(ErrorSeverity severity) - { - Severity = severity; - return this; - } - - public ErrorResponseBuilder WithMessage(string message) - { - Message = message; - return this; - } - - public ErrorResponse Build() - { - var msg = new ErrorResponse(this); - msg.Write(); - return msg; - } - } - } -} diff --git a/Pgnoli/Messages/ErrorSeverity.cs b/Pgnoli/Messages/ErrorSeverity.cs index fb78263..a8f9a5f 100644 --- a/Pgnoli/Messages/ErrorSeverity.cs +++ b/Pgnoli/Messages/ErrorSeverity.cs @@ -6,10 +6,15 @@ namespace Pgnoli.Messages { - internal enum ErrorSeverity + public enum ErrorSeverity { Error, Fatal, Panic, + Warning, + Notice, + Debug, + Info, + Log } } diff --git a/Pgnoli/Messages/Frontend/FrontendParser.cs b/Pgnoli/Messages/Frontend/FrontendParser.cs index c40a4a2..633e0f5 100644 --- a/Pgnoli/Messages/Frontend/FrontendParser.cs +++ b/Pgnoli/Messages/Frontend/FrontendParser.cs @@ -31,7 +31,7 @@ protected Message Decode(byte[] bytes) Query.Sync.Code => new Query.Sync(bytes), Query.Query.Code => new Query.Query(bytes), Handshake.Terminate.Code => new Handshake.Terminate(bytes), - _ => throw new UnexpectedMessageCodeException(code, bytes), + _ => throw new MessageUnexpectedCodeException(code, bytes), }; } } diff --git a/Pgnoli/Messages/Frontend/Query/Bind.cs b/Pgnoli/Messages/Frontend/Query/Bind.cs index 7c350a7..296a417 100644 --- a/Pgnoli/Messages/Frontend/Query/Bind.cs +++ b/Pgnoli/Messages/Frontend/Query/Bind.cs @@ -28,13 +28,6 @@ protected override int GetPayloadLength() + 2 + 2 * Payload.ResultsFormat.Length ; - private int GetFormatsLength(EncodingFormat[] formats) - => Payload.Parameters.All(x => x.EncodingFormat == EncodingFormat.Text) - ? 0 - : Payload.Parameters.All(x => x.EncodingFormat == EncodingFormat.Binary) - ? 1 - : Payload.Parameters.Count; - private int GetFormatValue(EncodingFormat[] formats) => Payload.Parameters.All(x => x.EncodingFormat == EncodingFormat.Text) ? 0 diff --git a/Pgnoli/Messages/Frontend/Query/Close.cs b/Pgnoli/Messages/Frontend/Query/Close.cs index 089fe2e..e68ac4f 100644 --- a/Pgnoli/Messages/Frontend/Query/Close.cs +++ b/Pgnoli/Messages/Frontend/Query/Close.cs @@ -24,7 +24,7 @@ internal Close(ClosePayload payload) : base(Code) { Payload = payload; } protected override int GetPayloadLength() - => Payload.Name.Length + 4; + => 1 + Payload.Name.Length + 1; protected internal override void WritePayload(Buffer buffer) { @@ -35,12 +35,14 @@ protected internal override void WritePayload(Buffer buffer) protected internal override void ReadPayload(Buffer buffer) { var portalType = buffer.ReadAsciiChar() == 'S' ? PortalType.PreparedStatement : PortalType.Portal; - var name = buffer.ReadFixedSizeString(buffer.Length - 6); + var name = buffer.ReadStringUntilNullTerminator(); Payload = new ClosePayload(portalType, name); } public static CloseBuilder PreparedStatement(string name) => new(PortalType.PreparedStatement, name); + public static CloseBuilder UnnamedPreparedStatement => new(PortalType.PreparedStatement, string.Empty); public static CloseBuilder Portal(string name) => new(PortalType.Portal, name); + public static CloseBuilder UnnamedPortal => new(PortalType.Portal, string.Empty); public record struct ClosePayload(PortalType PortalType, string Name) { } diff --git a/Pgnoli/Messages/Frontend/Query/Describe.cs b/Pgnoli/Messages/Frontend/Query/Describe.cs index 97243d9..0cfc3d8 100644 --- a/Pgnoli/Messages/Frontend/Query/Describe.cs +++ b/Pgnoli/Messages/Frontend/Query/Describe.cs @@ -19,24 +19,26 @@ internal Describe(DescribePayload payload) : base(Code) { Payload = payload; } protected override int GetPayloadLength() - => Math.Max(Payload.Name?.Length ?? 0, 1) + 1; + => 1 + Payload.Name.Length + 1; protected internal override void WritePayload(Buffer buffer) { buffer.WriteAsciiChar(Payload.PortalType == PortalType.PreparedStatement ? 'S' : 'P'); - buffer.WriteFixedSizeString(Payload.Name); + buffer.WriteAsciiStringNullTerminator(Payload.Name); } protected internal override void ReadPayload(Buffer buffer) { var portalType = buffer.ReadAsciiChar() == 'S' ? PortalType.PreparedStatement : PortalType.Portal; - var name = buffer.ReadFixedSizeString(buffer.Length - 6); + var name = buffer.ReadStringUntilNullTerminator(); Payload = new DescribePayload(portalType, name); } - public static DescribeBuilder PreparedStatement => new(PortalType.PreparedStatement); - public static DescribeBuilder Portal => new(PortalType.Portal); + public static DescribeBuilder UnnamedPreparedStatement => new(PortalType.PreparedStatement, string.Empty); + public static DescribeBuilder UnnamedPortal => new(PortalType.Portal, string.Empty); + public static DescribeBuilder PreparedStatement(string name) => new(PortalType.PreparedStatement, name); + public static DescribeBuilder Portal(string name) => new(PortalType.Portal, name); public record struct DescribePayload(PortalType PortalType, string Name) { } @@ -45,15 +47,9 @@ public class DescribeBuilder : IMessageBuilder { private DescribePayload Payload { get; set; } - public DescribeBuilder(PortalType value) + internal DescribeBuilder(PortalType value, string name) { - Payload = new DescribePayload(value, string.Empty); - } - - public DescribeBuilder Named(string name) - { - Payload = Payload with { Name = name }; - return this; + Payload = new DescribePayload(value, name); } public Describe Build() diff --git a/Pgnoli/Messages/Frontend/Query/Query.cs b/Pgnoli/Messages/Frontend/Query/Query.cs index 8a7e78a..f09ef36 100644 --- a/Pgnoli/Messages/Frontend/Query/Query.cs +++ b/Pgnoli/Messages/Frontend/Query/Query.cs @@ -18,7 +18,7 @@ internal Query(QueryPayload payload) : base(Code) { Payload = payload; } protected override int GetPayloadLength() - => Payload.Sql.Length; + => Payload.Sql.Length + 1; protected internal override void WritePayload(Buffer buffer) { diff --git a/Pgnoli/Messages/Message.cs b/Pgnoli/Messages/Message.cs index e49142a..7289be6 100644 --- a/Pgnoli/Messages/Message.cs +++ b/Pgnoli/Messages/Message.cs @@ -22,7 +22,7 @@ public Message() => (Buffer) = new Buffer(); public byte[] GetBytes() - => Buffer?.GetBytes() ?? throw new ArgumentNullException(nameof(Buffer)); + => Buffer.GetBytes(); protected abstract int GetPayloadLength(); internal virtual byte[] Write() @@ -36,21 +36,27 @@ internal virtual byte[] Write() public virtual int Read() { - if (Buffer is null) - throw new ArgumentNullException(nameof(Buffer)); + if (Buffer.Length == 0) + throw new BufferEmptyException(); Buffer.Reset(); + ReadPrefix(Buffer); + Length = Buffer.ReadInt(); - if (Length < Buffer.Length) - Buffer.TrimEnd(Length); + if (Buffer.Length < Length + PrefixLength) + throw new MessageUnexpectedLengthException(this.GetType(), Buffer.Length, Length); + else if (Buffer.Length > Length + PrefixLength) + Buffer.TrimEnd(Length + PrefixLength); ReadPayload(Buffer); if (!Buffer.IsEnd()) - throw new ArgumentException(nameof(Buffer)); - return Length; + throw new MessageNotFullyConsumedException(this.GetType(), Buffer.Length - Buffer.Position); + return Length + PrefixLength; } protected internal abstract void ReadPayload(Buffer buffer); + protected internal virtual void ReadPrefix(Buffer buffer) { } + protected internal virtual int PrefixLength => 0; } } diff --git a/Pgnoli/Messages/MessageExceptions.cs b/Pgnoli/Messages/MessageExceptions.cs new file mode 100644 index 0000000..efda299 --- /dev/null +++ b/Pgnoli/Messages/MessageExceptions.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Pgnoli.Messages +{ + public abstract class MessageException : PgnoliException + { + public MessageException(string message) + : base(message) { } + } + + public class MessageMismatchCodeException : MessageException + { + public MessageMismatchCodeException(Type MessageType, char expected, char actual) + : base($"When reading a message of type '{MessageType.GetType().Name}', the expected code is '{expected}' but the code is '{actual}'.") { } + } + + public class MessageUnexpectedCodeException : MessageException + { + public MessageUnexpectedCodeException(char code, byte[] bytes) + : base($"A message with code '{code}' was decoded but this code can't be associated to a type of message. The content of the message was '{bytes}'") { } + } + + public class MessageUnexpectedLengthException : MessageException + { + public MessageUnexpectedLengthException(Type MessageType, int expected, int actual) + : base($"When reading a message of type '{MessageType.GetType().Name}', the expected length was '{expected}' but the actual length of the buffer is '{actual}'. Buffer length cannot be less than expected legth of the message.") { } + } + + public class MessageNotFullyConsumedException : MessageException + { + public MessageNotFullyConsumedException(Type MessageType, int unreadBytes) + : base($"When reading a message of type '{MessageType.GetType().Name}', the buffer wasn't read until the end. '{unreadBytes}' were not read.") { } + } +} diff --git a/Pgnoli/Messages/UnexpectedMessageCodeException.cs b/Pgnoli/Messages/UnexpectedMessageCodeException.cs deleted file mode 100644 index 2536148..0000000 --- a/Pgnoli/Messages/UnexpectedMessageCodeException.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Pgnoli.Messages -{ - public class UnexpectedMessageCodeException : PgnoliException - { - public UnexpectedMessageCodeException(char code, byte[] bytes) - : base($"A message with code '{code}' was decoded but this code can't be associated to a type of message. The content of the message was '{bytes}'") { } - } -} diff --git a/Pgnoli/Types/TypeHandlers/Binary/BaseBinaryTypeHandler.cs b/Pgnoli/Types/TypeHandlers/Binary/BaseBinaryTypeHandler.cs index 79aa827..8b0948b 100644 --- a/Pgnoli/Types/TypeHandlers/Binary/BaseBinaryTypeHandler.cs +++ b/Pgnoli/Types/TypeHandlers/Binary/BaseBinaryTypeHandler.cs @@ -45,7 +45,7 @@ protected static Z ApplyEndianness(Z value) int x => (Z)Convert.ChangeType(BinaryPrimitives.ReverseEndianness(x), typeof(Z)), long x => (Z)Convert.ChangeType(BinaryPrimitives.ReverseEndianness(x), typeof(Z)), _ => throw new ArgumentOutOfRangeException(nameof(value)) - }; ; + }; } } } diff --git a/Pgnoli/Types/TypeHandlers/Binary/BaseNumericTypeHandler.cs b/Pgnoli/Types/TypeHandlers/Binary/BaseNumericTypeHandler.cs index 8b3b9e6..61bc5b0 100644 --- a/Pgnoli/Types/TypeHandlers/Binary/BaseNumericTypeHandler.cs +++ b/Pgnoli/Types/TypeHandlers/Binary/BaseNumericTypeHandler.cs @@ -27,6 +27,5 @@ public override T Read(ref Buffer buffer) return ReadUnderlyingValue(ref buffer); } - } } diff --git a/Pgnoli/Types/TypeHandlers/Binary/DecimalTypeHandler.cs b/Pgnoli/Types/TypeHandlers/Binary/DecimalTypeHandler.cs index 90c4829..2c63ca3 100644 --- a/Pgnoli/Types/TypeHandlers/Binary/DecimalTypeHandler.cs +++ b/Pgnoli/Types/TypeHandlers/Binary/DecimalTypeHandler.cs @@ -74,7 +74,6 @@ private IEnumerable Chunk(string str, int chunkSize) var subStr = str.Substring(i, Math.Min(chunkSize, str.Length - i)); yield return subStr; } - } } } \ No newline at end of file diff --git a/Pgnoli/Types/TypeHandlers/Binary/TimestampTypeHandler.cs b/Pgnoli/Types/TypeHandlers/Binary/TimestampTypeHandler.cs index 7a7d1fa..f8ce5c0 100644 --- a/Pgnoli/Types/TypeHandlers/Binary/TimestampTypeHandler.cs +++ b/Pgnoli/Types/TypeHandlers/Binary/TimestampTypeHandler.cs @@ -25,6 +25,5 @@ public override DateTime Read(ref Buffer buffer) var value = ReadUnderlyingValue(ref buffer); return Postgres_Epoch_Date.AddTicks(value * 10); } - } } diff --git a/README.md b/README.md index ecd31f1..0fdeef6 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Postgresql wire protocol is actually a general purpose Layer-7 application proto [![Tests](https://img.shields.io/appveyor/tests/seddryck/Pgnoli.svg)](https://ci.appveyor.com/project/Seddryck/Pgnoli/build/tests) [![CodeFactor](https://www.codefactor.io/repository/github/seddryck/Pgnoli/badge)](https://www.codefactor.io/repository/github/seddryck/Pgnoli) [![codecov](https://codecov.io/github/Seddryck/Pgnoli/branch/main/graph/badge.svg?token=9ZSJ6N0X9E)](https://codecov.io/github/Seddryck/Pgnoli) -[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSeddryck%2FPgnoli.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FSeddryck%2FPgnoli?ref=badge_shield) + **Status:** [![stars badge](https://img.shields.io/github/stars/Seddryck/Pgnoli.svg)](https://github.com/Seddryck/Pgnoli/stargazers) [![Bugs badge](https://img.shields.io/github/issues/Seddryck/Pgnoli/bug.svg?color=red&label=Bugs)](https://github.com/Seddryck/Pgnoli/issues?utf8=%E2%9C%93&q=is:issue+is:open+label:bug+)