/*
* 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 };
///
/// Allocates a random integer to use as a message id
///
public static int RandomMessageId => RandomNumberGenerator.GetInt32(1, int.MaxValue);
///
/// 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);
}
///
/// 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) => 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) => (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)
{
if(header == 0)
{
throw new ArgumentException("A header command of 0 is illegal", nameof(header));
}
//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);
}
}
}