/* * Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Net.Messaging.FBM * File: Helpers.cs * * Helpers.cs is part of VNLib.Net.Messaging.FBM which is part of the larger * VNLib collection of libraries and utilities. * * VNLib.Net.Messaging.FBM is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * VNLib.Net.Messaging.FBM is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://www.gnu.org/licenses/. */ using System; using System.IO; using System.Text; using System.Buffers.Binary; using System.Collections.Generic; using System.Security.Cryptography; using System.Runtime.CompilerServices; using VNLib.Utils; using VNLib.Utils.IO; using VNLib.Utils.Memory; using VNLib.Utils.Extensions; namespace VNLib.Net.Messaging.FBM { /// /// Contains FBM library helper methods /// public static class Helpers { /// /// The message-id of a connection control frame / out of band message /// public const int CONTROL_FRAME_MID = -500; /// /// Gets the default header character encoding instance /// public static Encoding DefaultEncoding { get; } = Encoding.UTF8; /// /// The FBM protocol header line termination symbols /// public static ReadOnlyMemory Termination { get; } = new byte[] { 0xFF, 0xF1 }; /// /// Parses the header line for a message-id /// /// A sequence of bytes that make up a header line /// The message-id if parsed, -1 if message-id is not valid public static int GetMessageId(ReadOnlySpan line) { //Make sure the message line is large enough to contain a message-id if (line.Length < 1 + sizeof(int)) { return -1; } //The first byte should be the header id HeaderCommand headerId = (HeaderCommand)line[0]; //Make sure the headerid is set if (headerId != HeaderCommand.MessageId) { return -2; } //Get the messageid after the header byte ReadOnlySpan messageIdSegment = line.Slice(1, sizeof(int)); //get the messageid from the messageid segment return BinaryPrimitives.ReadInt32BigEndian(messageIdSegment); } /// /// Appends the message id header to the accumulator /// /// The accumulatore to write the message id to /// The message id to write to the accumulator public static void WriteMessageid(IDataAccumulator accumulator, int messageid) { //Alloc buffer for message id + the message id header Span buffer = stackalloc byte[sizeof(int) + 1]; //Set 1st byte as message id buffer[0] = (byte)HeaderCommand.MessageId; //Write the message id as a big-endian message BinaryPrimitives.WriteInt32BigEndian(buffer[1..], messageid); //Write the header and message id + the trailing termination accumulator.Append(buffer); WriteTermination(accumulator); } /// /// Alloctes a random integer to use as a message id /// public static int RandomMessageId => RandomNumberGenerator.GetInt32(1, int.MaxValue); /// /// Gets the remaining data after the current position of the stream. /// /// The stream to segment /// The remaining data segment [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ReadOnlySpan GetRemainingData(VnMemoryStream response) { return response.AsSpan()[(int)response.Position..]; } /// /// Reads the next available line from the response message /// /// /// The read line public static ReadOnlySpan ReadLine(VnMemoryStream response) { //Get the span from the current stream position to end of the stream ReadOnlySpan line = GetRemainingData(response); //Search for next line termination int index = line.IndexOf(Termination.Span); if (index == -1) { return ReadOnlySpan.Empty; } //Update stream position to end of termination response.Seek(index + Termination.Length, SeekOrigin.Current); //slice up line and exclude the termination return line[..index]; } /// /// Parses headers from the request stream, stores headers from the buffer into the /// header collection /// /// The FBM packet buffer /// The header character buffer to write headers to /// The collection to store headers in /// The encoding type used to deocde header values /// The results of the parse operation internal static HeaderParseError ParseHeaders(VnMemoryStream vms, IFBMHeaderBuffer buffer, ICollection headers, Encoding encoding) { HeaderParseError status = HeaderParseError.None; //Get a sliding window writer over the enitre buffer ForwardOnlyWriter writer = new(buffer.GetSpan()); //Accumulate headers while (true) { //Read the next line from the current stream ReadOnlySpan line = ReadLine(vms); if (line.IsEmpty) { //Done reading headers break; } //Read the header command from the next line HeaderCommand cmd = GetHeaderCommand(line); //Write the next header value from the line to the remaining space in the buffer ERRNO charsRead = GetHeaderValue(line, writer.Remaining, encoding); if (charsRead < 0) { //Out of buffer space status |= HeaderParseError.HeaderOutOfMem; break; } else if (!charsRead) { //Invalid header status |= HeaderParseError.InvalidHeaderRead; } else { //Use the writer to capture the offset, and the character size FBMMessageHeader header = new(buffer, cmd, writer.Written, (int)charsRead); //Store header as a read-only sequence headers.Add(header); //Advance the writer writer.Advance(charsRead); } } return status; } /// /// Gets a enum from the first byte of the message /// /// /// The enum value from hte first byte of the message [MethodImpl(MethodImplOptions.AggressiveInlining)] public static HeaderCommand GetHeaderCommand(ReadOnlySpan line) { return (HeaderCommand)line[0]; } /// /// Gets the value of the header following the colon bytes in the specifed /// data message data line /// /// The message header line to get the value of /// The output character buffer to write characters to /// The encoding to decode the specified data with /// The number of characters encoded or -1 if the output buffer is too small public static ERRNO GetHeaderValue(ReadOnlySpan line, Span output, Encoding encoding) { //Get the data following the header byte ReadOnlySpan value = line[1..]; //Calculate the character account int charCount = encoding.GetCharCount(value); //Determine if the output buffer is large enough if (charCount > output.Length) { return -1; } //Decode the characters and return the char count _ = encoding.GetChars(value, output); return charCount; } /// /// Ends the header section of the request and appends the message body to /// the end of the request /// /// /// The message body to send with request /// public static void WriteBody(IDataAccumulator buffer, ReadOnlySpan body) { //start with termination WriteTermination(buffer); //Write the body buffer.Append(body); } /// /// Rounds the requested byte size up to the 1kb /// number of bytes /// /// The number of bytes to get the rounded 1kb size of /// The number of bytes equivalent to the requested byte size rounded to the next 1kb size [MethodImpl(MethodImplOptions.AggressiveInlining)] public static nint ToNearestKb(nint byteSize) { //Get page count by dividing count by number of pages nint kbs = (nint)Math.Ceiling(byteSize / (double)1024); //Multiply back to page sizes return kbs * 1024; } /// /// Writes a line termination to the message buffer /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void WriteTermination(IDataAccumulator buffer) => buffer.Append(Termination.Span); /// /// Appends an arbitrary header to the current request buffer /// /// /// The of the header /// The value of the header /// Encoding to use when writing character message /// public static void WriteHeader(IDataAccumulator buffer, byte header, ReadOnlySpan value, Encoding encoding) { //Write header command enum value buffer.Append(header); //Convert the characters to binary and write to the buffer int written = encoding.GetBytes(value, buffer.Remaining); //Advance the buffer buffer.Advance(written); //Write termination (0) WriteTermination(buffer); } } }