using System.IO; using System.IO.Compression; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; #nullable enable namespace Sandbox; /// /// Write and read bytes to a stream. This aims to be as allocation free as possible while also being as fast as possible. /// public unsafe ref struct ByteStream { // Maximum reasonable allocation size (1GB) - prevents OOM attacks private const int MAX_BUFFER_SIZE = 1024 * 1024 * 1024; NativeMemoryBlock? writeData; ReadOnlySpan readSpan; int position; int usedSize; /// /// Is this stream writable? /// public readonly bool Writable => writeData is not null; /// /// The current read or write position. Values are clamped to valid range. /// public int Position { readonly get => position; set { // Clamp to valid range, prevent negative or out-of-bounds if ( value < 0 ) position = 0; else if ( value > usedSize ) position = usedSize; else position = value; } } internal readonly int BufferSize => writeData?.Size ?? usedSize; /// /// The total size of the data /// public readonly int Length => usedSize; internal ByteStream( int size ) { if ( size <= 0 || size > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( size ), $"Size must be between 1 and {MAX_BUFFER_SIZE}" ); writeData = NativeMemoryBlock.GetOrCreatePooled( size ); position = 0; } internal ByteStream( ReadOnlySpan data ) { if ( data.Length > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( data ), "Data too large" ); readSpan = data; usedSize = data.Length; position = 0; } internal ByteStream( void* data, int datasize ) : this( datasize <= 0 || datasize > MAX_BUFFER_SIZE ? throw new ArgumentOutOfRangeException( nameof( datasize ) ) : new ReadOnlySpan( data, datasize ) ) { } /// /// Create a writable byte stream /// public static ByteStream Create( int size ) { if ( size <= 0 || size > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( size ) ); return new ByteStream( size ); } /// /// Create a reader byte stream /// public static ByteStream CreateReader( ReadOnlySpan data ) { return new ByteStream( data ); } /// /// Create a reader byte stream /// internal static ByteStream CreateReader( void* data, int size ) { return new ByteStream( new ReadOnlySpan( data, size ) ); } public void Dispose() { writeData?.Dispose(); writeData = null; readSpan = default; position = default; usedSize = default; } /// /// Ensures buffer can accommodate write with overflow protection /// public void EnsureCanWrite( int size ) { if ( writeData is null ) throw new InvalidOperationException( "Cannot write to read-only stream" ); if ( size < 0 || size > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( size ), "Invalid write size" ); // Uses checked arithmetic to detect overflow checked { int required = position + size; if ( required > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( size ), "Write would exceed maximum buffer size" ); if ( required > writeData.Size ) writeData.Grow( required ); } } public int ReadRemaining { get { var remaining = usedSize - position; return remaining < 0 ? 0 : remaining; } } /// /// Validates read bounds with overflow protection /// public readonly void EnsureCanRead( int size ) { if ( size < 0 ) throw new ArgumentOutOfRangeException( nameof( size ), "Cannot read negative size" ); // Uses checked arithmetic to prevent overflow checked { int required = position + size; if ( required > usedSize || required < 0 ) throw new IndexOutOfRangeException( $"Read would exceed buffer bounds (pos:{position}, size:{size}, buffer:{BufferSize})" ); } } /// /// Writes an array of unmanaged types /// public void WriteArray( ReadOnlySpan arr ) where T : unmanaged { if ( !SandboxedUnsafe.IsAcceptablePod() ) throw new InvalidOperationException( "Type must be unmanaged" ); // Write length first Write( arr.Length ); if ( arr.Length == 0 ) return; Write( arr ); } /// /// Writes an array of unmanaged types /// public void WriteArray( T[]? arr, bool includeCount = true ) where T : unmanaged { if ( arr == null ) { if ( includeCount ) Write( -1 ); return; } if ( !SandboxedUnsafe.IsAcceptablePod() ) throw new InvalidOperationException( "Type must be unmanaged" ); if ( includeCount ) Write( arr.Length ); if ( arr.Length == 0 ) return; Write( arr.AsSpan() ); } public void Write( ByteStream stream ) { // dont try to copy to self! if ( writeData is not null && stream.writeData == writeData ) return; var len = stream.usedSize; if ( len == 0 ) return; if ( len > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( stream ), "Stream too large" ); Write( stream.ToSpan() ); } internal void Write( ReadOnlySpan rawData ) where T : unmanaged { if ( !SandboxedUnsafe.IsAcceptablePod() ) throw new InvalidOperationException( "Type must be unmanaged" ); checked { var bytesSize = rawData.Length * sizeof( T ); if ( bytesSize == 0 ) return; if ( bytesSize > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( rawData ), "Data too large" ); EnsureCanWrite( bytesSize ); var dst = new Span( (byte*)writeData!.Pointer + position, rawData.Length ); rawData.CopyTo( dst ); position += bytesSize; if ( position > usedSize ) usedSize = position; } } public void Write( byte[] rawData ) { if ( rawData == null ) throw new ArgumentNullException( nameof( rawData ) ); Write( rawData.AsSpan() ); } public void Write( byte[] rawData, int offset, int bytes ) { if ( rawData == null ) throw new ArgumentNullException( nameof( rawData ) ); if ( bytes < 0 ) throw new ArgumentOutOfRangeException( nameof( bytes ), "Cannot write negative bytes" ); if ( offset < 0 ) throw new ArgumentOutOfRangeException( nameof( offset ), "Offset cannot be negative" ); // Checks for overflow in offset + bytes checked { int endPos = offset + bytes; if ( endPos > rawData.Length ) throw new ArgumentOutOfRangeException( nameof( bytes ), "Offset + bytes exceeds array length" ); } if ( bytes == 0 ) return; if ( bytes > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( bytes ), "Too many bytes" ); Write( rawData.AsSpan().Slice( offset, bytes ) ); } public void Write( ByteStream stream, int offset, int maxSize ) { // dont try to copy to self! if ( writeData is not null && stream.writeData == writeData ) return; if ( offset < 0 ) throw new ArgumentOutOfRangeException( nameof( offset ), "Offset cannot be negative" ); if ( maxSize < 0 ) throw new ArgumentOutOfRangeException( nameof( maxSize ), "Size cannot be negative" ); if ( maxSize == 0 ) return; // Checks offset bounds and calculates safe copy length if ( offset > stream.usedSize ) throw new ArgumentOutOfRangeException( nameof( offset ), "Offset exceeds stream length" ); int maxReadLeft = stream.usedSize - offset; int len = maxSize > maxReadLeft ? maxReadLeft : maxSize; if ( len <= 0 ) return; if ( len > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( maxSize ), "Size too large" ); Write( stream.ToSpan().Slice( offset, len ) ); } /// /// Writes a string /// public void Write( string? str ) { if ( str == null ) { Write( -1 ); return; } if ( str.Length == 0 ) { Write( 0 ); return; } // Validates string isn't absurdly large if ( str.Length > MAX_BUFFER_SIZE / 4 ) // UTF8 can be up to 4 bytes per char throw new ArgumentOutOfRangeException( nameof( str ), "String too large" ); var dataLen = System.Text.Encoding.UTF8.GetByteCount( str ); if ( dataLen > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( str ), "Encoded string too large" ); Write( dataLen ); EnsureCanWrite( dataLen ); var dst = new Span( (byte*)writeData!.Pointer + position, dataLen ); System.Text.Encoding.UTF8.GetBytes( str, dst ); position += dataLen; if ( position > usedSize ) usedSize = position; } /// /// Get the data as an array of bytes /// public readonly byte[] ToArray() { return ToSpan().ToArray(); } /// /// Get the data as a span. Note this can't be kept around after disposing the ByteStream /// internal readonly ReadOnlySpan ToSpan() { return writeData is not null ? new ReadOnlySpan( writeData.Pointer, usedSize ) : readSpan; } /// /// Returns a pointer to the base of the data /// internal readonly IntPtr Base() { return writeData is not null ? (IntPtr)writeData.Pointer : throw new NotImplementedException( $"Only writeable {nameof( ByteStream )}s have a valid pointer." ); } /// /// Returns a pointer to the base of the data with offset validation /// internal readonly IntPtr Base( int offset ) { if ( writeData is null ) throw new NotImplementedException( $"Only writeable {nameof( ByteStream )}s have a valid pointer." ); if ( offset < 0 || offset > usedSize ) throw new ArgumentOutOfRangeException( nameof( offset ), "Offset out of range" ); return (IntPtr)((byte*)writeData.Pointer + offset); } /// /// Writes an unmanaged type /// public void Write( T value ) where T : unmanaged { if ( !SandboxedUnsafe.IsAcceptablePod() ) throw new InvalidOperationException( "Type must be unmanaged" ); var size = sizeof( T ); // Validates reasonable size if ( size > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( T ), "Type too large" ); EnsureCanWrite( size ); var dst = (byte*)writeData!.Pointer + position; Unsafe.WriteUnaligned( dst, value ); position += size; if ( position > usedSize ) usedSize = position; } /// /// Reads an unmanaged type /// public T Read() where T : unmanaged { if ( !SandboxedUnsafe.IsAcceptablePod() ) throw new InvalidOperationException( "Type must be unmanaged" ); var size = sizeof( T ); // Uses checked arithmetic checked { int newPos = position + size; if ( newPos > usedSize || newPos < 0 ) throw new IndexOutOfRangeException( $"Failed to read {typeof( T )} (pos:{position}, size:{size}, buffer:{BufferSize})" ); T value; if ( writeData is not null ) { var src = (byte*)writeData.Pointer + position; value = Unsafe.ReadUnaligned( src ); } else { fixed ( byte* src = readSpan ) { value = Unsafe.ReadUnaligned( src + position ); } } position = newPos; return value; } } /// /// Try to read variable, return false if not enough data /// public bool TryRead( out T v ) where T : unmanaged { if ( !SandboxedUnsafe.IsAcceptablePod() ) throw new InvalidOperationException( "Type must be unmanaged" ); v = default; var size = sizeof( T ); var remaining = usedSize - position; if ( remaining < size || remaining < 0 ) return false; // Additional overflow check checked { int newPos = position + size; if ( newPos > usedSize || newPos < 0 ) return false; if ( writeData is not null ) { var src = (byte*)writeData.Pointer + position; v = Unsafe.ReadUnaligned( src ); } else { fixed ( byte* src = readSpan ) { v = Unsafe.ReadUnaligned( src + position ); } } position = newPos; return true; } } /// /// Returns an array of unmanaged types /// public T[] ReadArray( int maxElements = 1024 ) where T : unmanaged { return ReadArraySpan( maxElements ).ToArray(); } /// /// Non allocating read of an array of unmanaged types /// internal ReadOnlySpan ReadArraySpan( int maxElements = 1024 ) where T : unmanaged { if ( !SandboxedUnsafe.IsAcceptablePod() ) throw new ArgumentOutOfRangeException( nameof( T ), "Type not acceptable" ); if ( maxElements <= 0 || maxElements > MAX_BUFFER_SIZE / sizeof( T ) ) throw new ArgumentOutOfRangeException( nameof( maxElements ), "Invalid max elements" ); var len = Read(); // Validates array length if ( len < 0 ) throw new InvalidOperationException( "Array length cannot be negative" ); if ( len == 0 ) return default; if ( len > maxElements ) throw new IndexOutOfRangeException( $"Array length {len} exceeds maximum {maxElements}" ); // Checks for multiplication overflow checked { int dataSize = sizeof( T ) * len; if ( dataSize > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( len ), "Array data too large" ); int newPos = position + dataSize; if ( newPos > usedSize || newPos < 0 ) throw new IndexOutOfRangeException( $"Array read exceeds buffer (pos:{position}, size:{dataSize}, buffer:{BufferSize})" ); ReadOnlySpan byteSpan; if ( writeData is not null ) { byteSpan = new ReadOnlySpan( (byte*)writeData.Pointer + position, dataSize ); } else { byteSpan = readSpan.Slice( position, dataSize ); } position = newPos; return MemoryMarshal.Cast( byteSpan ); } } public string? Read( string defaultValue = "" ) where T : IEquatable { int datasize = Read(); // Validates size if ( datasize == -1 ) return null; if ( datasize == 0 ) return string.Empty; if ( datasize < 0 ) throw new InvalidOperationException( "String size cannot be negative" ); if ( datasize > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( datasize ), "String too large" ); checked { int newPos = position + datasize; if ( newPos > usedSize || newPos < 0 ) throw new IndexOutOfRangeException( $"String read exceeds buffer (pos:{position}, size:{datasize}, buffer:{BufferSize})" ); ReadOnlySpan bytes; if ( writeData is not null ) { bytes = new ReadOnlySpan( (byte*)writeData.Pointer + position, datasize ); } else { bytes = readSpan.Slice( position, datasize ); } var str = System.Text.Encoding.UTF8.GetString( bytes ); position = newPos; return str; } } public void Write( T data, bool unused = false ) where T : IByteParsable { T.WriteObject( ref this, data ); } public T Read( T? defaultValue = default, bool unused = false ) where T : IByteParsable { return (T)T.ReadObject( ref this ); } public object? ReadObject( Type objectType ) { if ( objectType == typeof( byte ) ) return Read(); if ( objectType == typeof( ushort ) ) return Read(); if ( objectType == typeof( short ) ) return Read(); if ( objectType == typeof( uint ) ) return Read(); if ( objectType == typeof( int ) ) return Read(); if ( objectType == typeof( ulong ) ) return Read(); if ( objectType == typeof( long ) ) return Read(); if ( objectType == typeof( float ) ) return Read(); if ( objectType == typeof( double ) ) return Read(); if ( objectType == typeof( string ) ) return Read(); if ( objectType == typeof( Vector3 ) ) return Read(); if ( objectType == typeof( Rotation ) ) return Read(); if ( objectType == typeof( Angles ) ) return Read(); if ( objectType == typeof( Transform ) ) return Read(); throw new NotImplementedException( $"ReadObject doesn't support {objectType} - add support if it makes sense!" ); } /// /// Read a block of data. Note - this can't be made public as it could lead to unsafe usage. /// internal ByteStream ReadByteStream( int size ) { if ( size < 0 ) throw new ArgumentOutOfRangeException( nameof( size ), "Size cannot be negative" ); if ( size > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( size ), "Size too large" ); checked { int newPos = position + size; if ( newPos > usedSize || newPos < 0 ) throw new IndexOutOfRangeException( $"Read exceeds buffer (pos:{position}, size:{size}, buffer:{BufferSize})" ); ReadOnlySpan span; if ( writeData is not null ) { span = new ReadOnlySpan( (byte*)writeData.Pointer + position, size ); } else { span = readSpan.Slice( position, size ); } position = newPos; return CreateReader( span ); } } /// /// Note never make public - as the span could point to disposed memory! /// internal ReadOnlySpan GetRemainingBytes() { var remaining = usedSize - position; if ( remaining <= 0 ) return default; if ( writeData is not null ) { var span = new ReadOnlySpan( (byte*)writeData.Pointer + position, remaining ); position = usedSize; return span; } else { var span = readSpan.Slice( position, remaining ); position = usedSize; return span; } } /// /// Write an Array, that we know is a Value array. We definitely know it's a value array. /// We're not exposing this to the public api because they don't know whether it's a value array. /// internal void WriteValueArray( Array array ) { if ( array == null ) throw new ArgumentNullException( nameof( array ) ); if ( array.Length == 0 ) return; if ( array.Length > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( array ), "Array too large" ); var elementType = array.GetType().GetElementType()!; if ( !elementType.IsValueType ) throw new InvalidOperationException( $"{elementType} isn't a value type!" ); var elementSize = elementType.GetManagedSize(); if ( elementSize <= 0 ) throw new InvalidOperationException( $"Couldn't get size of {elementType}" ); // Checks for multiplication overflow checked { int bytes = elementSize * array.Length; if ( bytes > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( array ), "Array data too large" ); EnsureCanWrite( bytes ); var dst = (byte*)writeData!.Pointer + position; var handle = GCHandle.Alloc( array, GCHandleType.Pinned ); try { var src = (void*)handle.AddrOfPinnedObject(); Buffer.MemoryCopy( src, dst, bytes, bytes ); } finally { handle.Free(); } position += bytes; if ( position > usedSize ) usedSize = position; } } /// /// Read an Array, that we know is a Value array. We definitely know it's a value array. /// We're not exposing this to the public api because they don't know whether it's a value array. /// internal void ReadValueArray( Array array ) { if ( array == null ) throw new ArgumentNullException( nameof( array ) ); if ( array.Length == 0 ) return; if ( array.Length > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( array ), "Array too large" ); var elementType = array.GetType().GetElementType()!; if ( !elementType.IsValueType ) throw new InvalidOperationException( $"{elementType} isn't a value type!" ); var elementSize = elementType.GetManagedSize(); if ( elementSize <= 0 ) throw new InvalidOperationException( $"Couldn't get size of {elementType}" ); // Checks for multiplication overflow checked { int bytes = elementSize * array.Length; if ( bytes > MAX_BUFFER_SIZE ) throw new ArgumentOutOfRangeException( nameof( array ), "Array data too large" ); int newPos = position + bytes; if ( newPos > usedSize || newPos < 0 ) throw new IndexOutOfRangeException( $"Read exceeds buffer (pos:{position}, size:{bytes}, buffer:{BufferSize})" ); var handle = GCHandle.Alloc( array, GCHandleType.Pinned ); try { var dst = (void*)handle.AddrOfPinnedObject(); if ( writeData is not null ) { var src = (byte*)writeData.Pointer + position; Buffer.MemoryCopy( src, dst, bytes, bytes ); } else { fixed ( byte* src = readSpan ) { Buffer.MemoryCopy( src + position, dst, bytes, bytes ); } } } finally { handle.Free(); } position = newPos; } } /// public int Read( byte[] buffer, int offset, int count ) { if ( buffer == null ) throw new ArgumentNullException( nameof( buffer ) ); return Read( buffer.AsSpan( offset, count ) ); } /// public int Read( Span buffer ) { var remaining = usedSize - position; if ( remaining < 0 ) remaining = 0; var length = buffer.Length < remaining ? buffer.Length : remaining; if ( length <= 0 ) return 0; if ( writeData is not null ) { var src = new ReadOnlySpan( (byte*)writeData.Pointer + position, length ); src.CopyTo( buffer ); } else { readSpan.Slice( position, length ).CopyTo( buffer ); } position += length; return length; } public ByteStream Compress() { var data = ToArray(); using ( MemoryStream compressedStream = new MemoryStream() ) { using ( GZipStream gzipStream = new GZipStream( compressedStream, CompressionLevel.SmallestSize ) ) { gzipStream.Write( data, 0, data.Length ); } return ByteStream.CreateReader( compressedStream.ToArray() ); } } public ByteStream Decompress() { using ( MemoryStream compressedStream = new MemoryStream( ToArray() ) ) { using ( MemoryStream decompressedStream = new MemoryStream() ) { using ( GZipStream gzipStream = new GZipStream( compressedStream, CompressionMode.Decompress ) ) { gzipStream.CopyTo( decompressedStream ); } return ByteStream.CreateReader( decompressedStream.ToArray() ); } } } }