diff options
author | vnugent <public@vaughnnugent.com> | 2023-01-08 16:01:54 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-01-08 16:01:54 -0500 |
commit | de94d788e9a47432a7630a8215896b8dd3628599 (patch) | |
tree | 666dec06eef861d101cb6948aff52a3d354c8d73 /lib/Utils/src/IO | |
parent | be6dc557a3b819248b014992eb96c1cb21f8112b (diff) |
Reorder + analyzer cleanup
Diffstat (limited to 'lib/Utils/src/IO')
-rw-r--r-- | lib/Utils/src/IO/ArrayPoolStreamBuffer.cs | 70 | ||||
-rw-r--r-- | lib/Utils/src/IO/BackingStream.cs | 181 | ||||
-rw-r--r-- | lib/Utils/src/IO/FileOperations.cs | 105 | ||||
-rw-r--r-- | lib/Utils/src/IO/IDataAccumulator.cs | 64 | ||||
-rw-r--r-- | lib/Utils/src/IO/ISlindingWindowBuffer.cs | 91 | ||||
-rw-r--r-- | lib/Utils/src/IO/IVnTextReader.cs | 72 | ||||
-rw-r--r-- | lib/Utils/src/IO/InMemoryTemplate.cs | 196 | ||||
-rw-r--r-- | lib/Utils/src/IO/IsolatedStorageDirectory.cs | 154 | ||||
-rw-r--r-- | lib/Utils/src/IO/SlidingWindowBufferExtensions.cs | 213 | ||||
-rw-r--r-- | lib/Utils/src/IO/TemporayIsolatedFile.cs | 57 | ||||
-rw-r--r-- | lib/Utils/src/IO/VnMemoryStream.cs | 469 | ||||
-rw-r--r-- | lib/Utils/src/IO/VnStreamReader.cs | 180 | ||||
-rw-r--r-- | lib/Utils/src/IO/VnStreamWriter.cs | 292 | ||||
-rw-r--r-- | lib/Utils/src/IO/VnTextReaderExtensions.cs | 223 | ||||
-rw-r--r-- | lib/Utils/src/IO/WriteOnlyBufferedStream.cs | 255 |
15 files changed, 2622 insertions, 0 deletions
diff --git a/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs b/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs new file mode 100644 index 0000000..df366e3 --- /dev/null +++ b/lib/Utils/src/IO/ArrayPoolStreamBuffer.cs @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ArrayPoolStreamBuffer.cs +* +* ArrayPoolStreamBuffer.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.Buffers; + +namespace VNLib.Utils.IO +{ + internal class ArrayPoolStreamBuffer<T> : ISlindingWindowBuffer<T> + { + private readonly ArrayPool<T> _pool; + private T[] _buffer; + + public ArrayPoolStreamBuffer(ArrayPool<T> pool, int bufferSize) + { + _pool = pool; + _buffer = _pool.Rent(bufferSize); + } + + public int WindowStartPos { get; set; } + public int WindowEndPos { get; set; } + + public Memory<T> Buffer => _buffer.AsMemory(); + + public void Advance(int count) + { + WindowEndPos += count; + } + + public void AdvanceStart(int count) + { + WindowStartPos += count; + } + + public void Close() + { + //Return buffer to pool + _pool.Return(_buffer); + _buffer = null; + } + + public void Reset() + { + //Reset window positions + WindowStartPos = 0; + WindowEndPos = 0; + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/BackingStream.cs b/lib/Utils/src/IO/BackingStream.cs new file mode 100644 index 0000000..cb56b09 --- /dev/null +++ b/lib/Utils/src/IO/BackingStream.cs @@ -0,0 +1,181 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: BackingStream.cs +* +* BackingStream.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Provides basic stream support sync/async stream operations to a + /// backing stream with virtual event methods. Provides a pass-through + /// as best as possbile. + /// </summary> + public abstract class BackingStream<T> : Stream where T: Stream + { + /// <summary> + /// The backing/underlying stream operations are being performed on + /// </summary> + protected T BaseStream { get; set; } + /// <summary> + /// A value that will cause all calls to write to throw <see cref="NotSupportedException"/> + /// </summary> + protected bool ForceReadOnly { get; set; } + ///<inheritdoc/> + public override bool CanRead => BaseStream.CanRead; + ///<inheritdoc/> + public override bool CanSeek => BaseStream.CanSeek; + ///<inheritdoc/> + public override bool CanWrite => BaseStream.CanWrite && !ForceReadOnly; + ///<inheritdoc/> + public override long Length => BaseStream.Length; + ///<inheritdoc/> + public override int WriteTimeout { get => BaseStream.WriteTimeout; set => BaseStream.WriteTimeout = value; } + ///<inheritdoc/> + public override int ReadTimeout { get => BaseStream.ReadTimeout; set => BaseStream.ReadTimeout = value; } + ///<inheritdoc/> + public override long Position { get => BaseStream.Position; set => BaseStream.Position = value; } + ///<inheritdoc/> + public override void Flush() + { + BaseStream.Flush(); + OnFlush(); + } + ///<inheritdoc/> + public override int Read(byte[] buffer, int offset, int count) => BaseStream.Read(buffer, offset, count); + ///<inheritdoc/> + public override int Read(Span<byte> buffer) => BaseStream.Read(buffer); + ///<inheritdoc/> + public override long Seek(long offset, SeekOrigin origin) => BaseStream.Seek(offset, origin); + ///<inheritdoc/> + public override void SetLength(long value) => BaseStream.SetLength(value); + ///<inheritdoc/> + public override void Write(byte[] buffer, int offset, int count) + { + if (ForceReadOnly) + { + throw new NotSupportedException("Stream is set to readonly mode"); + } + BaseStream.Write(buffer, offset, count); + //Call onwrite function + OnWrite(count); + } + ///<inheritdoc/> + public override void Write(ReadOnlySpan<byte> buffer) + { + if (ForceReadOnly) + { + throw new NotSupportedException("Stream is set to readonly mode"); + } + BaseStream.Write(buffer); + //Call onwrite function + OnWrite(buffer.Length); + } + ///<inheritdoc/> + public override void Close() + { + BaseStream.Close(); + //Call on close function + OnClose(); + } + + /// <summary> + /// Raised directly after the base stream is closed, when a call to close is made + /// </summary> + protected virtual void OnClose() { } + /// <summary> + /// Raised directly after the base stream is flushed, when a call to flush is made + /// </summary> + protected virtual void OnFlush() { } + /// <summary> + /// Raised directly after a successfull write operation. + /// </summary> + /// <param name="count">The number of bytes written to the stream</param> + protected virtual void OnWrite(int count) { } + + ///<inheritdoc/> + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return BaseStream.ReadAsync(buffer, offset, count, cancellationToken); + } + ///<inheritdoc/> + public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + { + return BaseStream.ReadAsync(buffer, cancellationToken); + } + ///<inheritdoc/> + public override void CopyTo(Stream destination, int bufferSize) => BaseStream.CopyTo(destination, bufferSize); + ///<inheritdoc/> + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return BaseStream.CopyToAsync(destination, bufferSize, cancellationToken); + } + ///<inheritdoc/> + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (ForceReadOnly) + { + throw new NotSupportedException("Stream is set to readonly mode"); + } + //We want to maintain pass through as much as possible, so supress warning +#pragma warning disable CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' + await BaseStream.WriteAsync(buffer, offset, count, cancellationToken); +#pragma warning restore CA1835 // Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' + + //Call on-write and pass the number of bytes written + OnWrite(count); + } + ///<inheritdoc/> + public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) + { + if (ForceReadOnly) + { + throw new NotSupportedException("Stream is set to readonly mode"); + } + await BaseStream.WriteAsync(buffer, cancellationToken); + //Call on-write and pass the length + OnWrite(buffer.Length); + } + ///<inheritdoc/> + public override async Task FlushAsync(CancellationToken cancellationToken) + { + await BaseStream.FlushAsync(cancellationToken); + //Call onflush + OnFlush(); + } + + ///<inheritdoc/> + public override async ValueTask DisposeAsync() + { + //Dispose the base stream and await it + await BaseStream.DisposeAsync(); + //Call onclose + OnClose(); + //Suppress finalize + GC.SuppressFinalize(this); + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/FileOperations.cs b/lib/Utils/src/IO/FileOperations.cs new file mode 100644 index 0000000..e040da4 --- /dev/null +++ b/lib/Utils/src/IO/FileOperations.cs @@ -0,0 +1,105 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: FileOperations.cs +* +* FileOperations.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Contains cross-platform optimized filesystem operations. + /// </summary> + public static class FileOperations + { + public const int INVALID_FILE_ATTRIBUTES = -1; + + [DllImport("Shlwapi", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return:MarshalAs(UnmanagedType.Bool)] + private static unsafe extern bool PathFileExists(char* path); + [DllImport("kernel32", SetLastError = true, CharSet = CharSet.Auto)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + [return:MarshalAs(UnmanagedType.I4)] + private static unsafe extern int GetFileAttributes(char* path); + + static readonly bool IsWindows = OperatingSystem.IsWindows(); + /// <summary> + /// Determines if a file exists. If application is current running in the Windows operating system, Shlwapi.PathFileExists is invoked, + /// otherwise <see cref="File.Exists(string?)"/> is invoked + /// </summary> + /// <param name="filePath">the path to the file</param> + /// <returns>True if the file can be opened, false otherwise</returns> + public static bool FileExists(string filePath) + { + //If windows is detected, use the unmanged function + if (!IsWindows) + { + return File.Exists(filePath); + } + unsafe + { + //Get a char pointer to the file path + fixed (char* path = filePath) + { + //Invoke the winap file function + return PathFileExists(path); + } + } + } + + /// <summary> + /// If Windows is detected at load time, gets the attributes for the specified file. + /// </summary> + /// <param name="filePath">The path to the existing file</param> + /// <returns>The attributes of the file </returns> + /// <exception cref="PathTooLongException"></exception> + /// <exception cref="FileNotFoundException"></exception> + /// <exception cref="UnauthorizedAccessException"></exception> + public static FileAttributes GetAttributes(string filePath) + { + //If windows is detected, use the unmanged function + if (!IsWindows) + { + return File.GetAttributes(filePath); + } + unsafe + { + //Get a char pointer to the file path + fixed (char* path = filePath) + { + //Invoke the winap file function and cast the returned int value to file attributes + int attr = GetFileAttributes(path); + //Check for error + if (attr == INVALID_FILE_ATTRIBUTES) + { + throw new FileNotFoundException("The requested file was not found", filePath); + } + //Cast to file attributes and return + return (FileAttributes)attr; + } + } + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/IDataAccumulator.cs b/lib/Utils/src/IO/IDataAccumulator.cs new file mode 100644 index 0000000..5129a55 --- /dev/null +++ b/lib/Utils/src/IO/IDataAccumulator.cs @@ -0,0 +1,64 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IDataAccumulator.cs +* +* IDataAccumulator.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// A data structure that represents a sliding window over a buffer + /// for resetable forward-only reading or writing + /// </summary> + /// <typeparam name="T">The accumuation data type</typeparam> + public interface IDataAccumulator<T> + { + /// <summary> + /// Gets the number of available items within the buffer + /// </summary> + int AccumulatedSize { get; } + /// <summary> + /// The number of elements remaining in the buffer + /// </summary> + int RemainingSize { get; } + /// <summary> + /// The remaining space in the internal buffer as a contiguous segment + /// </summary> + Span<T> Remaining { get; } + /// <summary> + /// The buffer window over the accumulated data + /// </summary> + Span<T> Accumulated { get; } + + /// <summary> + /// Advances the accumulator buffer window by the specified amount + /// </summary> + /// <param name="count">The number of elements accumulated</param> + void Advance(int count); + + /// <summary> + /// Resets the internal state of the accumulator + /// </summary> + void Reset(); + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/ISlindingWindowBuffer.cs b/lib/Utils/src/IO/ISlindingWindowBuffer.cs new file mode 100644 index 0000000..ff4e142 --- /dev/null +++ b/lib/Utils/src/IO/ISlindingWindowBuffer.cs @@ -0,0 +1,91 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: ISlindingWindowBuffer.cs +* +* ISlindingWindowBuffer.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Represents a sliding window buffer for reading/wiriting data + /// </summary> + /// <typeparam name="T"></typeparam> + public interface ISlindingWindowBuffer<T> : IDataAccumulator<T> + { + /// <summary> + /// The number of elements remaining in the buffer + /// </summary> + int IDataAccumulator<T>.RemainingSize => Buffer.Length - WindowEndPos; + /// <summary> + /// The remaining space in the internal buffer as a contiguous segment + /// </summary> + Span<T> IDataAccumulator<T>.Remaining => RemainingBuffer.Span; + /// <summary> + /// The buffer window over the accumulated data + /// </summary> + Span<T> IDataAccumulator<T>.Accumulated => AccumulatedBuffer.Span; + /// <summary> + /// Gets the number of available items within the buffer + /// </summary> + int IDataAccumulator<T>.AccumulatedSize => WindowEndPos - WindowStartPos; + + /// <summary> + /// The starting positon of the available data within the buffer + /// </summary> + int WindowStartPos { get; } + /// <summary> + /// The ending position of the available data within the buffer + /// </summary> + int WindowEndPos { get; } + /// <summary> + /// Buffer memory wrapper + /// </summary> + Memory<T> Buffer { get; } + + /// <summary> + /// Releases resources used by the current instance + /// </summary> + void Close(); + /// <summary> + /// <para> + /// Advances the begining of the accumulated data window. + /// </para> + /// <para> + /// This method is used during reading to singal that data + /// has been read from the internal buffer and the + /// accumulator window can be shifted. + /// </para> + /// </summary> + /// <param name="count">The number of elements to shift by</param> + void AdvanceStart(int count); + + /// <summary> + /// Gets a window within the buffer of available buffered data + /// </summary> + Memory<T> AccumulatedBuffer => Buffer[WindowStartPos..WindowEndPos]; + /// <summary> + /// Gets the available buffer window to write data to + /// </summary> + Memory<T> RemainingBuffer => Buffer[WindowEndPos..]; + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/IVnTextReader.cs b/lib/Utils/src/IO/IVnTextReader.cs new file mode 100644 index 0000000..625ba78 --- /dev/null +++ b/lib/Utils/src/IO/IVnTextReader.cs @@ -0,0 +1,72 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IVnTextReader.cs +* +* IVnTextReader.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Text; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Represents a streaming text reader with internal buffers + /// </summary> + public interface IVnTextReader + { + /// <summary> + /// The base stream to read data from + /// </summary> + Stream BaseStream { get; } + /// <summary> + /// The character encoding used by the TextReader + /// </summary> + Encoding Encoding { get; } + /// <summary> + /// Number of available bytes of buffered data within the current buffer window + /// </summary> + int Available { get; } + /// <summary> + /// Gets or sets the line termination used to deliminate a line of data + /// </summary> + ReadOnlyMemory<byte> LineTermination { get; } + /// <summary> + /// The unread/available data within the internal buffer + /// </summary> + Span<byte> BufferedDataWindow { get; } + /// <summary> + /// Shifts the sliding buffer window by the specified number of bytes. + /// </summary> + /// <param name="count">The number of bytes read from the buffer</param> + void Advance(int count); + /// <summary> + /// Reads data from the stream into the remaining buffer space for processing + /// </summary> + void FillBuffer(); + /// <summary> + /// Compacts the available buffer space back to the begining of the buffer region + /// and determines if there is room for more data to be buffered + /// </summary> + /// <returns>The remaining buffer space if any</returns> + ERRNO CompactBufferWindow(); + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/InMemoryTemplate.cs b/lib/Utils/src/IO/InMemoryTemplate.cs new file mode 100644 index 0000000..ae8bf79 --- /dev/null +++ b/lib/Utils/src/IO/InMemoryTemplate.cs @@ -0,0 +1,196 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: InMemoryTemplate.cs +* +* InMemoryTemplate.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Represents a lazily loaded file stored in memory, with a change mointor + /// that reloads the template if the file was modified in the filesystem + /// </summary> + public abstract class InMemoryTemplate : VnDisposeable + { + protected ManualResetEventSlim TemplateLock; + private readonly FileSystemWatcher? Watcher; + private bool Modified; + private VnMemoryStream templateBuffer; + protected readonly FileInfo TemplateFile; + + /// <summary> + /// Gets the name of the template + /// </summary> + public abstract string TemplateName { get; } + + /// <summary> + /// Creates a new in-memory copy of a file that will detect changes and refresh + /// </summary> + /// <param name="listenForChanges">Should changes to the template file be moniored for changes, and reloaded as necessary</param> + /// <param name="path">The path of the file template</param> + protected InMemoryTemplate(string path, bool listenForChanges = true) + { + TemplateFile = new FileInfo(path); + TemplateLock = new(true); + //Make sure the file exists + if (!TemplateFile.Exists) + { + throw new FileNotFoundException("Template file does not exist"); + } + if (listenForChanges) + { + //Setup a watcher to reload the template when modified + Watcher = new FileSystemWatcher(TemplateFile.DirectoryName!) + { + EnableRaisingEvents = true, + IncludeSubdirectories = false, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size + }; + Watcher.Changed += Watcher_Changed; + } + //Set modified flag to make sure the template is read on first use + this.Modified = true; + } + + private void Watcher_Changed(object sender, FileSystemEventArgs e) + { + //Make sure the event was raied for this template + if (!e.FullPath.Equals(TemplateFile.FullName, StringComparison.OrdinalIgnoreCase)) + { + return; + } + TemplateLock.Reset(); + try + { + //Set modified flag + Modified = true; + //Refresh the fileinfo object + TemplateFile.Refresh(); + //Invoke onmodifed function + OnModifed(); + } + finally + { + TemplateLock.Set(); + } + } + + /// <summary> + /// Gets a cached copy of the template data + /// </summary> + protected VnMemoryStream GetTemplateData() + { + //Make sure access is synchronized incase the file gets updated during access on another thread + TemplateLock.Wait(); + //Determine if the file has been modified and needs to be reloaded + if (Modified) + { + TemplateLock.Reset(); + try + { + //Read a new copy of the templte into mem + ReadFile(); + } + finally + { + TemplateLock.Set(); + } + } + //Return a copy of the memory stream + return templateBuffer.GetReadonlyShallowCopy(); + } + /// <summary> + /// Updates the internal copy of the file to its memory representation + /// </summary> + protected void ReadFile() + { + //Open the file stream + using FileStream fs = TemplateFile.OpenRead(); + //Dispose the old template buffer + templateBuffer?.Dispose(); + //Create a new stream for storing the cached copy + VnMemoryStream newBuf = new(); + try + { + fs.CopyTo(newBuf, null); + } + catch + { + newBuf.Dispose(); + throw; + } + //Create the readonly copy + templateBuffer = VnMemoryStream.CreateReadonly(newBuf); + //Clear the modified flag + Modified = false; + } + /// <summary> + /// Updates the internal copy of the file to its memory representation, asynchronously + /// </summary> + /// <param name="cancellationToken"></param> + /// <returns>A task that completes when the file has been copied into memory</returns> + protected async Task ReadFileAsync(CancellationToken cancellationToken = default) + { + //Open the file stream + await using FileStream fs = TemplateFile.OpenRead(); + //Dispose the old template buffer + templateBuffer?.Dispose(); + //Create a new stream for storing the cached copy + VnMemoryStream newBuf = new(); + try + { + //Copy async + await fs.CopyToAsync(newBuf, 8192, Memory.Memory.Shared, cancellationToken); + } + catch + { + newBuf.Dispose(); + throw; + } + //Create the readonly copy + templateBuffer = VnMemoryStream.CreateReadonly(newBuf); + //Clear the modified flag + Modified = false; + } + + /// <summary> + /// Invoked when the template file has been modifed. Note: This event is raised + /// while the <see cref="TemplateLock"/> is held. + /// </summary> + protected abstract void OnModifed(); + + ///<inheritdoc/> + protected override void Free() + { + //Dispose the watcher + Watcher?.Dispose(); + //free the stream + templateBuffer?.Dispose(); + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/IsolatedStorageDirectory.cs b/lib/Utils/src/IO/IsolatedStorageDirectory.cs new file mode 100644 index 0000000..65460ff --- /dev/null +++ b/lib/Utils/src/IO/IsolatedStorageDirectory.cs @@ -0,0 +1,154 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: IsolatedStorageDirectory.cs +* +* IsolatedStorageDirectory.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.IO.IsolatedStorage; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Represents an open directory within an <see cref="IsolatedStorageFile"/> store for which files can be created, opened, or deleted. + /// </summary> + public sealed class IsolatedStorageDirectory : IsolatedStorage + { + private readonly string DirectoryPath; + private readonly IsolatedStorageFile Storage; + /// <summary> + /// Creates a new <see cref="IsolatedStorageDirectory"/> within the specified file using the directory name. + /// </summary> + /// <param name="storage">A configured and open <see cref="IsolatedStorageFile"/></param> + /// <param name="dir">The directory name to open or create within the store</param> + public IsolatedStorageDirectory(IsolatedStorageFile storage, string dir) + { + this.Storage = storage; + this.DirectoryPath = dir; + //If the directory doesnt exist, create it + if (!this.Storage.DirectoryExists(dir)) + this.Storage.CreateDirectory(dir); + } + + private IsolatedStorageDirectory(IsolatedStorageDirectory parent, string dirName) + { + //Store ref to parent dir + Parent = parent; + //Referrence store + this.Storage = parent.Storage; + //Add the name of this dir to the end of the specified dir path + this.DirectoryPath = Path.Combine(parent.DirectoryPath, dirName); + } + + /// <summary> + /// Creates a file by its path name within the currnet directory + /// </summary> + /// <param name="fileName">The name of the file</param> + /// <returns>The open file</returns> + /// <exception cref="IsolatedStorageException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="DirectoryNotFoundException"></exception> + public IsolatedStorageFileStream CreateFile(string fileName) + { + return this.Storage.CreateFile(Path.Combine(DirectoryPath, fileName)); + } + /// <summary> + /// Removes a file from the current directory + /// </summary> + /// <param name="fileName">The path of the file to remove</param> + /// <exception cref="IsolatedStorageException"></exception> + public void DeleteFile(string fileName) + { + this.Storage.DeleteFile(Path.Combine(this.DirectoryPath, fileName)); + } + /// <summary> + /// Opens a file that exists within the current directory + /// </summary> + /// <param name="fileName">Name with extension of the file</param> + /// <param name="mode">File mode</param> + /// <param name="access">File access</param> + /// <returns>The open <see cref="IsolatedStorageFileStream"/> from the current directory</returns> + public IsolatedStorageFileStream OpenFile(string fileName, FileMode mode, FileAccess access) + { + return this.Storage.OpenFile(Path.Combine(DirectoryPath, fileName), mode, access); + } + /// <summary> + /// Opens a file that exists within the current directory + /// </summary> + /// <param name="fileName">Name with extension of the file</param> + /// <param name="mode">File mode</param> + /// <param name="access">File access</param> + /// <param name="share">The file shareing mode</param> + /// <returns>The open <see cref="IsolatedStorageFileStream"/> from the current directory</returns> + public IsolatedStorageFileStream OpenFile(string fileName, FileMode mode, FileAccess access, FileShare share) + { + return this.Storage.OpenFile(Path.Combine(DirectoryPath, fileName), mode, access, share); + } + + /// <summary> + /// Determiens if the specified file path refers to an existing file within the directory + /// </summary> + /// <param name="fileName">The name of the file to search for</param> + /// <returns>True if the file exists within the current directory</returns> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="IsolatedStorageException"></exception> + /// <exception cref="InvalidOperationException"></exception> + public bool FileExists(string fileName) + { + return this.Storage.FileExists(Path.Combine(this.DirectoryPath, fileName)); + } + + /// <summary> + /// Removes the directory and its contents from the store + /// </summary> + public override void Remove() + { + Storage.DeleteDirectory(this.DirectoryPath); + } + + public override long AvailableFreeSpace => Storage.AvailableFreeSpace; + public override long Quota => Storage.Quota; + public override long UsedSize => Storage.UsedSize; + public override bool IncreaseQuotaTo(long newQuotaSize) => Storage.IncreaseQuotaTo(newQuotaSize); + + /// <summary> + /// The parent <see cref="IsolatedStorageDirectory"/> this directory is a child within. null if there are no parent directories + /// above this dir + /// </summary> + + public IsolatedStorageDirectory? Parent { get; } +#nullable disable + + /// <summary> + /// Creates a child directory within the current directory + /// </summary> + /// <param name="directoryName">The name of the child directory</param> + /// <returns>A new <see cref="IsolatedStorageDirectory"/> for which <see cref="IsolatedStorageFileStream"/>s can be opened/created</returns> + /// <exception cref="ArgumentException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public IsolatedStorageDirectory CreateChildDirectory(string directoryName) + { + return new IsolatedStorageDirectory(this, directoryName); + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/SlidingWindowBufferExtensions.cs b/lib/Utils/src/IO/SlidingWindowBufferExtensions.cs new file mode 100644 index 0000000..0509061 --- /dev/null +++ b/lib/Utils/src/IO/SlidingWindowBufferExtensions.cs @@ -0,0 +1,213 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: SlidingWindowBufferExtensions.cs +* +* SlidingWindowBufferExtensions.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; +using System.Runtime.CompilerServices; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Extention methods for <see cref="ISlindingWindowBuffer{T}"/> + /// </summary> + public static class SlidingWindowBufferExtensions + { + /// <summary> + /// Shifts/resets the current buffered data window down to the + /// begining of the buffer if the buffer window is shifted away + /// from the begining. + /// </summary> + /// <returns>The number of bytes of available space in the buffer</returns> + public static ERRNO CompactBufferWindow<T>(this ISlindingWindowBuffer<T> sBuf) + { + //Nothing to compact if the starting data pointer is at the beining of the window + if (sBuf.WindowStartPos > 0) + { + //Get span over engire buffer + Span<T> buffer = sBuf.Buffer.Span; + //Get data within window + Span<T> usedData = sBuf.Accumulated; + //Copy remaining to the begining of the buffer + usedData.CopyTo(buffer); + + //Reset positions, then advance to the specified size + sBuf.Reset(); + sBuf.Advance(usedData.Length); + } + //Return the number of bytes of available space + return sBuf.RemainingSize; + } + + /// <summary> + /// Appends the specified data to the end of the buffer + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="sBuf"></param> + /// <param name="val">The value to append to the end of the buffer</param> + /// <exception cref="IndexOutOfRangeException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this IDataAccumulator<T> sBuf, T val) + { + //Set the value at first position + sBuf.Remaining[0] = val; + //Advance by 1 + sBuf.Advance(1); + } + /// <summary> + /// Appends the specified data to the end of the buffer + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="sBuf"></param> + /// <param name="val">The value to append to the end of the buffer</param> + /// <exception cref="ArgumentException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this IDataAccumulator<T> sBuf, ReadOnlySpan<T> val) + { + val.CopyTo(sBuf.Remaining); + sBuf.Advance(val.Length); + } + /// <summary> + /// Formats and appends a value type to the accumulator with proper endianess + /// </summary> + /// <typeparam name="T">The value type to appent</typeparam> + /// <param name="accumulator">The binary accumulator to append</param> + /// <param name="value">The value type to append</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this IDataAccumulator<byte> accumulator, T value) where T: unmanaged + { + //Use forward reader for the memory extension to append a value type to a binary accumulator + ForwardOnlyWriter<byte> w = new(accumulator.Remaining); + w.Append(value); + accumulator.Advance(w.Written); + } + + /// <summary> + /// Attempts to write as much data as possible to the remaining space + /// in the buffer and returns the number of bytes accumulated. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="accumulator"></param> + /// <param name="value">The value to accumulate</param> + /// <returns>The number of bytes accumulated</returns> + public static ERRNO TryAccumulate<T>(this IDataAccumulator<T> accumulator, ReadOnlySpan<T> value) + { + //Calc data size and reserve space for final crlf + int dataToCopy = Math.Min(value.Length, accumulator.RemainingSize); + + //Write as much data as possible + accumulator.Append(value[..dataToCopy]); + + //Return number of bytes not written + return dataToCopy; + } + + /// <summary> + /// Appends a <see cref="ISpanFormattable"/> instance to the end of the accumulator + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="accumulator"></param> + /// <param name="formattable">The formattable instance to write to the accumulator</param> + /// <param name="format">The format arguments</param> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Append<T>(this IDataAccumulator<char> accumulator, in T formattable, ReadOnlySpan<char> format = default) where T : struct, ISpanFormattable + { + ForwardOnlyWriter<char> writer = new(accumulator.Remaining); + writer.Append(formattable, format); + accumulator.Advance(writer.Written); + } + + /// <summary> + /// Uses the remaining data buffer to compile a <see cref="IStringSerializeable"/> + /// instance, then advances the accumulator by the number of characters used. + /// </summary> + /// <typeparam name="T"></typeparam> + /// <param name="accumulator"></param> + /// <param name="compileable">The <see cref="IStringSerializeable"/> instance to compile</param> + public static void Append<T>(this IDataAccumulator<char> accumulator, in T compileable) where T : IStringSerializeable + { + //Write directly to the remaining space + int written = compileable.Compile(accumulator.Remaining); + //Advance the writer + accumulator.Advance(written); + } + + /// <summary> + /// Reads available data from the current window and writes as much as possible it to the supplied buffer + /// and advances the buffer window + /// </summary> + /// <typeparam name="T">Element type</typeparam> + /// <param name="sBuf"></param> + /// <param name="buffer">The output buffer to write data to</param> + /// <returns>The number of elements written to the buffer</returns> + public static ERRNO Read<T>(this ISlindingWindowBuffer<T> sBuf, in Span<T> buffer) + { + //Calculate the amount of data to copy + int dataToCopy = Math.Min(buffer.Length, sBuf.AccumulatedSize); + //Copy the data to the buffer + sBuf.Accumulated[..dataToCopy].CopyTo(buffer); + //Advance the window + sBuf.AdvanceStart(dataToCopy); + //Return the number of bytes copied + return dataToCopy; + } + + /// <summary> + /// Fills the remaining window space of the current accumulator with + /// data from the specified stream asynchronously. + /// </summary> + /// <param name="accumulator"></param> + /// <param name="input">The stream to read data from</param> + /// <param name="cancellationToken">A token to cancel the operation</param> + /// <returns>A value task representing the operation</returns> + public static async ValueTask AccumulateDataAsync(this ISlindingWindowBuffer<byte> accumulator, Stream input, CancellationToken cancellationToken) + { + //Get a buffer from the end of the current window to the end of the buffer + Memory<byte> bufWindow = accumulator.RemainingBuffer; + //Read from stream async + int read = await input.ReadAsync(bufWindow, cancellationToken); + //Update the end of the buffer window to the end of the read data + accumulator.Advance(read); + } + /// <summary> + /// Fills the remaining window space of the current accumulator with + /// data from the specified stream. + /// </summary> + /// <param name="accumulator"></param> + /// <param name="input">The stream to read data from</param> + public static void AccumulateData(this IDataAccumulator<byte> accumulator, Stream input) + { + //Get a buffer from the end of the current window to the end of the buffer + Span<byte> bufWindow = accumulator.Remaining; + //Read from stream async + int read = input.Read(bufWindow); + //Update the end of the buffer window to the end of the read data + accumulator.Advance(read); + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/TemporayIsolatedFile.cs b/lib/Utils/src/IO/TemporayIsolatedFile.cs new file mode 100644 index 0000000..3bee92b --- /dev/null +++ b/lib/Utils/src/IO/TemporayIsolatedFile.cs @@ -0,0 +1,57 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: TemporayIsolatedFile.cs +* +* TemporayIsolatedFile.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.IO.IsolatedStorage; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Allows for temporary files to be generated, used, then removed from an <see cref="IsolatedStorageFile"/> + /// </summary> + public sealed class TemporayIsolatedFile : BackingStream<IsolatedStorageFileStream> + { + private readonly IsolatedStorageDirectory Storage; + private readonly string Filename; + /// <summary> + /// Creates a new temporary filestream within the specified <see cref="IsolatedStorageFile"/> + /// </summary> + /// <param name="storage">The file store to genreate temporary files within</param> + public TemporayIsolatedFile(IsolatedStorageDirectory storage) + { + //Store ref + this.Storage = storage; + //Creaet a new random filename + this.Filename = Path.GetRandomFileName(); + //try to created a new file within the isolaged storage + this.BaseStream = storage.CreateFile(this.Filename); + } + protected override void OnClose() + { + //Remove the file from the storage + Storage.DeleteFile(this.Filename); + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/VnMemoryStream.cs b/lib/Utils/src/IO/VnMemoryStream.cs new file mode 100644 index 0000000..4e8a2b3 --- /dev/null +++ b/lib/Utils/src/IO/VnMemoryStream.cs @@ -0,0 +1,469 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnMemoryStream.cs +* +* VnMemoryStream.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.IO +{ + + using Utils.Memory; + + /// <summary> + /// Provides an unmanaged memory stream. Desigend to help reduce garbage collector load for + /// high frequency memory operations. Similar to <see cref="UnmanagedMemoryStream"/> + /// </summary> + public sealed class VnMemoryStream : Stream, ICloneable + { + private long _position; + private long _length; + //Memory + private readonly MemoryHandle<byte> _buffer; + private bool IsReadonly; + //Default owns handle + private readonly bool OwnsHandle = true; + + /// <summary> + /// Creates a new <see cref="VnMemoryStream"/> pointing to the begining of memory, and consumes the handle. + /// </summary> + /// <param name="handle"><see cref="MemoryHandle{T}"/> to consume</param> + /// <param name="length">Length of the stream</param> + /// <param name="readOnly">Should the stream be readonly?</param> + /// <exception cref="ArgumentException"></exception> + /// <returns>A <see cref="VnMemoryStream"/> wrapper to access the handle data</returns> + public static VnMemoryStream ConsumeHandle(MemoryHandle<byte> handle, Int64 length, bool readOnly) + { + handle.ThrowIfClosed(); + return new VnMemoryStream(handle, length, readOnly, true); + } + + /// <summary> + /// Converts a writable <see cref="VnMemoryStream"/> to readonly to allow shallow copies + /// </summary> + /// <param name="stream">The stream to make readonly</param> + /// <returns>The readonly stream</returns> + public static VnMemoryStream CreateReadonly(VnMemoryStream stream) + { + //Set the readonly flag + stream.IsReadonly = true; + //Return the stream + return stream; + } + + /// <summary> + /// Creates a new memory stream + /// </summary> + public VnMemoryStream() : this(Memory.Shared) { } + /// <summary> + /// Create a new memory stream where buffers will be allocated from the specified heap + /// </summary> + /// <param name="heap"><see cref="PrivateHeap"/> to allocate memory from</param> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ArgumentNullException"></exception> + public VnMemoryStream(IUnmangedHeap heap) : this(heap, 0, false) { } + + /// <summary> + /// Creates a new memory stream and pre-allocates the internal + /// buffer of the specified size on the specified heap to avoid resizing. + /// </summary> + /// <param name="heap"><see cref="PrivateHeap"/> to allocate memory from</param> + /// <param name="bufferSize">Number of bytes (length) of the stream if known</param> + /// <param name="zero">Zero memory allocations during buffer expansions</param> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="ArgumentNullException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public VnMemoryStream(IUnmangedHeap heap, long bufferSize, bool zero) + { + _ = heap ?? throw new ArgumentNullException(nameof(heap)); + _buffer = heap.Alloc<byte>(bufferSize, zero); + } + + /// <summary> + /// Creates a new memory stream from the data provided + /// </summary> + /// <param name="heap"><see cref="PrivateHeap"/> to allocate memory from</param> + /// <param name="data">Initial data</param> + public VnMemoryStream(IUnmangedHeap heap, ReadOnlySpan<byte> data) + { + _ = heap ?? throw new ArgumentNullException(nameof(heap)); + //Alloc the internal buffer to match the data stream + _buffer = heap.AllocAndCopy(data); + //Set length + _length = data.Length; + //Position will default to 0 cuz its dotnet :P + return; + } + + /// <summary> + /// WARNING: Dangerous constructor, make sure read-only and owns hanlde are set accordingly + /// </summary> + /// <param name="buffer">The buffer to referrence directly</param> + /// <param name="length">The length property of the stream</param> + /// <param name="readOnly">Is the stream readonly (should mostly be true!)</param> + /// <param name="ownsHandle">Does the new stream own the memory -> <paramref name="buffer"/></param> + private VnMemoryStream(MemoryHandle<byte> buffer, long length, bool readOnly, bool ownsHandle) + { + OwnsHandle = ownsHandle; + _buffer = buffer; //Consume the handle + _length = length; //Store length of the buffer + IsReadonly = readOnly; + } + + /// <summary> + /// UNSAFE Number of bytes between position and length. Never negative + /// </summary> + private long LenToPosDiff => Math.Max(_length - _position, 0); + + /// <summary> + /// If the current stream is a readonly stream, creates an unsafe shallow copy for reading only. + /// </summary> + /// <returns>New stream shallow copy of the internal stream</returns> + /// <exception cref="NotSupportedException"></exception> + public VnMemoryStream GetReadonlyShallowCopy() + { + //Create a new readonly copy (stream does not own the handle) + return !IsReadonly + ? throw new NotSupportedException("This stream is not readonly. Cannot create shallow copy on a mutable stream") + : new VnMemoryStream(_buffer, _length, true, false); + } + + /// <summary> + /// Writes data directly to the destination stream from the internal buffer + /// without allocating or copying any data. + /// </summary> + /// <param name="destination">The stream to write data to</param> + /// <param name="bufferSize">The size of the chunks to write to the destination stream</param> + /// <exception cref="IOException"></exception> + public override void CopyTo(Stream destination, int bufferSize) + { + _ = destination ?? throw new ArgumentNullException(nameof(destination)); + + if (!destination.CanWrite) + { + throw new IOException("The destinaion stream is not writeable"); + } + + do + { + //Calc the remaining bytes to read no larger than the buffer size + int bytesToRead = (int)Math.Min(LenToPosDiff, bufferSize); + + //Create a span wrapper by using the offet function to support memory handles larger than 2gb + ReadOnlySpan<byte> span = _buffer.GetOffsetSpan(_position, bytesToRead); + + destination.Write(span); + + //Update position + _position += bytesToRead; + + } while (LenToPosDiff > 0); + } + + /// <summary> + /// Allocates a temporary buffer of the desired size, copies data from the internal + /// buffer and writes it to the destination buffer asynchronously. + /// </summary> + /// <param name="destination">The stream to write output data to</param> + /// <param name="bufferSize">The size of the buffer to use when copying data</param> + /// <param name="cancellationToken">A token to cancel the opreation</param> + /// <returns>A task that resolves when the remaining data in the stream has been written to the destination</returns> + /// <exception cref="IOException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + _ = destination ?? throw new ArgumentNullException(nameof(destination)); + + if (!destination.CanWrite) + { + throw new IOException("The destinaion stream is not writeable"); + } + + cancellationToken.ThrowIfCancellationRequested(); + + /* + * Alloc temp copy buffer. This is a requirement because + * the stream may be larger than an int32 so it must be + * copied by segment + */ + + using VnTempBuffer<byte> copyBuffer = new(bufferSize); + + do + { + //read from internal stream + int read = Read(copyBuffer); + + if(read <= 0) + { + break; + } + + //write async + await destination.WriteAsync(copyBuffer.AsMemory(0, read), cancellationToken); + + } while (true); + + } + + /// <summary> + /// <inheritdoc/> + /// <para> + /// This property is always true + /// </para> + /// </summary> + public override bool CanRead => true; + /// <summary> + /// <inheritdoc/> + /// <para> + /// This propery is always true + /// </para> + /// </summary> + public override bool CanSeek => true; + /// <summary> + /// True unless the stream is (or has been converted to) a readonly + /// stream. + /// </summary> + public override bool CanWrite => !IsReadonly; + ///<inheritdoc/> + public override long Length => _length; + ///<inheritdoc/> + public override bool CanTimeout => false; + + ///<inheritdoc/> + public override long Position + { + get => _position; + set => Seek(value, SeekOrigin.Begin); + } + /// <summary> + /// Closes the stream and frees the internal allocated memory blocks + /// </summary> + public override void Close() + { + //Only dispose buffer if we own it + if (OwnsHandle) + { + _buffer.Dispose(); + } + } + ///<inheritdoc/> + public override void Flush() { } + // Override to reduce base class overhead + ///<inheritdoc/> + public override Task FlushAsync(CancellationToken cancellationToken) => Task.CompletedTask; + ///<inheritdoc/> + public override int Read(byte[] buffer, int offset, int count) => Read(new Span<byte>(buffer, offset, count)); + ///<inheritdoc/> + public override int Read(Span<byte> buffer) + { + if (buffer.Length == 0) + { + return 0; + } + //Number of bytes to read from memory buffer + int bytesToRead = checked((int)Math.Min(LenToPosDiff, buffer.Length)); + //Copy bytes to buffer + Memory.Copy(_buffer, _position, buffer, 0, bytesToRead); + //Increment buffer position + _position += bytesToRead; + //Bytestoread should never be larger than int.max because span length is an integer + return bytesToRead; + } + + /* + * Async reading will always run synchronously in a memory stream, + * so overrides are just so avoid base class overhead + */ + ///<inheritdoc/> + public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + { + //Read synchronously and return a completed task + int read = Read(buffer.Span); + return ValueTask.FromResult(read); + } + ///<inheritdoc/> + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + //Read synchronously and return a completed task + int read = Read(buffer.AsSpan(offset, count)); + return Task.FromResult(read); + } + ///<inheritdoc/> + public override long Seek(long offset, SeekOrigin origin) + { + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Offset cannot be less than 0"); + } + switch (origin) + { + case SeekOrigin.Begin: + //Length will never be greater than int.Max so output will never exceed int.max + _position = Math.Min(_length, offset); + return _position; + case SeekOrigin.Current: + long newPos = _position + offset; + //Length will never be greater than int.Max so output will never exceed length + _position = Math.Min(_length, newPos); + return newPos; + case SeekOrigin.End: + long real_index = _length - offset; + //If offset moves the position negative, just set the position to 0 and continue + _position = Math.Min(real_index, 0); + return real_index; + default: + throw new ArgumentException("Stream operation is not supported on current stream"); + } + } + + + /// <summary> + /// Resizes the internal buffer to the exact size (in bytes) of the + /// value argument. A value of 0 will free the entire buffer. A value + /// greater than zero will resize the buffer (and/or alloc) + /// </summary> + /// <param name="value">The size of the stream (and internal buffer)</param> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="NotSupportedException"></exception> + /// <exception cref="ObjectDisposedException"></exception> + /// <exception cref="ArgumentOutOfRangeException"></exception> + public override void SetLength(long value) + { + if (IsReadonly) + { + throw new NotSupportedException("This stream is readonly"); + } + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), "Value cannot be less than 0"); + } + //Resize the buffer to the specified length + _buffer.Resize(value); + //Set length + _length = value; + //Make sure the position is not pointing outside of the buffer + _position = Math.Min(_position, _length); + return; + } + ///<inheritdoc/> + public override void Write(byte[] buffer, int offset, int count) => Write(new ReadOnlySpan<byte>(buffer, offset, count)); + ///<inheritdoc/> + public override void Write(ReadOnlySpan<byte> buffer) + { + if (IsReadonly) + { + throw new NotSupportedException("Write operation is not allowed on readonly stream!"); + } + //Calculate the new final position + long newPos = (_position + buffer.Length); + //Determine if the buffer needs to be expanded + if (buffer.Length > LenToPosDiff) + { + //Expand buffer if required + _buffer.ResizeIfSmaller(newPos); + //Update length + _length = newPos; + } + //Copy the input buffer to the internal buffer + Memory.Copy(buffer, _buffer, _position); + //Update the position + _position = newPos; + return; + } + ///<inheritdoc/> + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + //Write synchronously and return a completed task + Write(buffer, offset, count); + return Task.CompletedTask; + } + ///<inheritdoc/> + public override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) + { + //Write synchronously and return a completed task + Write(buffer.Span); + return ValueTask.CompletedTask; + } + ///<inheritdoc/> + public override void WriteByte(byte value) + { + Span<byte> buf = MemoryMarshal.CreateSpan(ref value, 1); + Write(buf); + } + + /// <summary> + /// Allocates and copies internal buffer to new managed byte[] + /// </summary> + /// <returns>Copy of internal buffer</returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <exception cref="OutOfMemoryException"></exception> + public byte[] ToArray() + { + //Alloc a new array of the size of the internal buffer + byte[] data = new byte[_length]; + //Copy data from the internal buffer to the output buffer + _buffer.Span.CopyTo(data); + return data; + + } + /// <summary> + /// Returns a <see cref="ReadOnlySpan{T}"/> window over the data within the entire stream + /// </summary> + /// <returns>A <see cref="ReadOnlySpan{T}"/> of the data within the entire stream</returns> + /// <exception cref="OverflowException"></exception> + public ReadOnlySpan<byte> AsSpan() + { + ReadOnlySpan<byte> output = _buffer.Span; + return output[..(int)_length]; + } + + /// <summary> + /// If the current stream is a readonly stream, creates a shallow copy for reading only. + /// </summary> + /// <returns>New stream shallow copy of the internal stream</returns> + /// <exception cref="InvalidOperationException"></exception> + public object Clone() => GetReadonlyShallowCopy(); + + /* + * Override the Dispose async method to avoid the base class overhead + * and task allocation since this will always be a syncrhonous + * operation (freeing memory) + */ + + ///<inheritdoc/> + public override ValueTask DisposeAsync() + { + //Dispose and return completed task + base.Dispose(true); + GC.SuppressFinalize(this); + return ValueTask.CompletedTask; + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/VnStreamReader.cs b/lib/Utils/src/IO/VnStreamReader.cs new file mode 100644 index 0000000..70b9734 --- /dev/null +++ b/lib/Utils/src/IO/VnStreamReader.cs @@ -0,0 +1,180 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnStreamReader.cs +* +* VnStreamReader.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Text; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Binary based buffered text reader, optimized for reading network streams + /// </summary> + public class VnStreamReader : TextReader, IVnTextReader + { + private bool disposedValue; + + private readonly ISlindingWindowBuffer<byte> _buffer; + ///<inheritdoc/> + public virtual Stream BaseStream { get; } + ///<inheritdoc/> + public Encoding Encoding { get; } + + /// <summary> + /// Number of available bytes of buffered data within the current buffer window + /// </summary> + public int Available => _buffer.AccumulatedSize; + /// <summary> + /// Gets or sets the line termination used to deliminate a line of data + /// </summary> + public ReadOnlyMemory<byte> LineTermination { get; set; } + Span<byte> IVnTextReader.BufferedDataWindow => _buffer.Accumulated; + + /// <summary> + /// Creates a new <see cref="TextReader"/> that reads encoded data from the base. + /// Internal buffers will be alloced from <see cref="ArrayPool{T}.Shared"/> + /// </summary> + /// <param name="baseStream">The underlying stream to read data from</param> + /// <param name="enc">The <see cref="Encoding"/> to use when reading from the stream</param> + /// <param name="bufferSize">The size of the internal binary buffer</param> + public VnStreamReader(Stream baseStream, Encoding enc, int bufferSize) + { + BaseStream = baseStream; + Encoding = enc; + //Init a new buffer + _buffer = InitializeBuffer(bufferSize); + } + + /// <summary> + /// Invoked by the constuctor method to allocte the internal buffer with the specified buffer size. + /// </summary> + /// <param name="bufferSize">The requested size of the buffer to alloc</param> + /// <remarks>By default requests the buffer from the <see cref="ArrayPool{T}.Shared"/> instance</remarks> + protected virtual ISlindingWindowBuffer<byte> InitializeBuffer(int bufferSize) => new ArrayPoolStreamBuffer<byte>(ArrayPool<byte>.Shared, bufferSize); + + ///<inheritdoc/> + public override async Task<string?> ReadLineAsync() + { + //If buffered data is available, check for line termination + if (Available > 0) + { + //Get current buffer window + Memory<byte> buffered = _buffer.AccumulatedBuffer; + //search for line termination in current buffer + int term = buffered.IndexOf(LineTermination); + //Termination found in buffer window + if (term > -1) + { + //Capture the line from the begining of the window to the termination + Memory<byte> line = buffered[..term]; + //Shift the window to the end of the line (excluding the termination) + _buffer.AdvanceStart(term + LineTermination.Length); + //Decode the line to a string + return Encoding.GetString(line.Span); + } + //Termination not found + } + //Compact the buffer window and see if space is avialble to buffer more data + if (_buffer.CompactBufferWindow()) + { + //There is room, so buffer more data + await _buffer.AccumulateDataAsync(BaseStream, CancellationToken.None); + //Check again to see if more data is buffered + if (Available <= 0) + { + //No string found + return null; + } + //Get current buffer window + Memory<byte> buffered = _buffer.AccumulatedBuffer; + //search for line termination in current buffer + int term = buffered.IndexOf(LineTermination); + //Termination found in buffer window + if (term > -1) + { + //Capture the line from the begining of the window to the termination + Memory<byte> line = buffered[..term]; + //Shift the window to the end of the line (excluding the termination) + _buffer.AdvanceStart(term + LineTermination.Length); + //Decode the line to a string + return Encoding.GetString(line.Span); + } + } + //Termination not found within the entire buffer, so buffer space has been exhausted + + //OOM is raised in the TextReader base class, the standard is preserved +#pragma warning disable CA2201 // Do not raise reserved exception types + throw new OutOfMemoryException("A line termination was not found within the buffer"); +#pragma warning restore CA2201 // Do not raise reserved exception types + } + + ///<inheritdoc/> + public override int Read(char[] buffer, int index, int count) => Read(buffer.AsSpan(index, count)); + ///<inheritdoc/> + public override int Read(Span<char> buffer) + { + if (Available <= 0) + { + return 0; + } + //Get current buffer window + Span<byte> buffered = _buffer.Accumulated; + //Convert all avialable data + int encoded = Encoding.GetChars(buffered, buffer); + //Shift buffer window to the end of the converted data + _buffer.AdvanceStart(encoded); + //return the number of chars written + return Encoding.GetCharCount(buffered); + } + ///<inheritdoc/> + public override void Close() => _buffer.Close(); + ///<inheritdoc/> + protected override void Dispose(bool disposing) + { + if (!disposedValue) + { + Close(); + disposedValue = true; + } + base.Dispose(disposing); + } + + /// <summary> + /// Resets the internal buffer window + /// </summary> + protected void ClearBuffer() + { + _buffer.Reset(); + } + + void IVnTextReader.Advance(int count) => _buffer.AdvanceStart(count); + void IVnTextReader.FillBuffer() => _buffer.AccumulateData(BaseStream); + ERRNO IVnTextReader.CompactBufferWindow() => _buffer.CompactBufferWindow(); + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/VnStreamWriter.cs b/lib/Utils/src/IO/VnStreamWriter.cs new file mode 100644 index 0000000..f875932 --- /dev/null +++ b/lib/Utils/src/IO/VnStreamWriter.cs @@ -0,0 +1,292 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnStreamWriter.cs +* +* VnStreamWriter.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Text; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; + +using VNLib.Utils.Memory; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Provides a memory optimized <see cref="TextWriter"/> implementation. Optimized for writing + /// to network streams + /// </summary> + public class VnStreamWriter : TextWriter + { + private readonly Encoder Enc; + + private readonly ISlindingWindowBuffer<byte> _buffer; + + private bool closed; + + /// <summary> + /// Gets the underlying stream that interfaces with the backing store + /// </summary> + public virtual Stream BaseStream { get; } + ///<inheritdoc/> + public override Encoding Encoding { get; } + + /// <summary> + /// Line termination to use when writing lines to the output + /// </summary> + public ReadOnlyMemory<byte> LineTermination { get; set; } + ///<inheritdoc/> + public override string NewLine + { + get => Encoding.GetString(LineTermination.Span); + set => LineTermination = Encoding.GetBytes(value); + } + + /// <summary> + /// Creates a new <see cref="VnStreamWriter"/> that writes formatted data + /// to the specified base stream + /// </summary> + /// <param name="baseStream">The stream to write data to</param> + /// <param name="encoding">The <see cref="Encoding"/> to use when writing data</param> + /// <param name="bufferSize">The size of the internal buffer used to buffer binary data before writing to the base stream</param> + public VnStreamWriter(Stream baseStream, Encoding encoding, int bufferSize = 1024) + { + //Store base stream + BaseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream)); + Encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); + //Get an encoder + Enc = encoding.GetEncoder(); + _buffer = InitializeBuffer(bufferSize); + } + + /// <summary> + /// Invoked by the constuctor method to allocte the internal buffer with the specified buffer size. + /// </summary> + /// <param name="bufferSize">The requested size of the buffer to alloc</param> + /// <remarks>By default requests the buffer from the <see cref="MemoryPool{T}.Shared"/> instance</remarks> + protected virtual ISlindingWindowBuffer<byte> InitializeBuffer(int bufferSize) => new ArrayPoolStreamBuffer<byte>(ArrayPool<byte>.Shared, bufferSize); + ///<inheritdoc/> + public void Write(byte value) + { + //See if there is room in the binary buffer + if (_buffer.AccumulatedSize == 0) + { + //There is not enough room to store the single byte + Flush(); + } + //Store at the end of the window + _buffer.Append(value); + } + ///<inheritdoc/> + public override void Write(char value) + { + ReadOnlySpan<char> tbuf = MemoryMarshal.CreateSpan(ref value, 0x01); + Write(tbuf); + } + ///<inheritdoc/> + public override void Write(object? value) => Write(value?.ToString()); + ///<inheritdoc/> + public override void Write(string? value) => Write(value.AsSpan()); + ///<inheritdoc/> + public override void Write(ReadOnlySpan<char> buffer) + { + Check(); + + ForwardOnlyReader<char> reader = new(buffer); + + //Create a variable for a character buffer window + bool completed; + do + { + //Get an available buffer window to store characters in and convert the characters to binary + Enc.Convert(reader.Window, _buffer.Remaining, true, out int charsUsed, out int bytesUsed, out completed); + //Update byte position + _buffer.Advance(bytesUsed); + //Update char position + reader.Advance(charsUsed); + + //Converting did not complete because the buffer was too small + if (!completed || reader.WindowSize == 0) + { + //Flush the buffer and continue + Flush(); + } + + } while (!completed); + //Reset the encoder + Enc.Reset(); + } + ///<inheritdoc/> + public override async Task WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default) + { + Check(); + //Create a variable for a character buffer window + bool completed; + ForwardOnlyMemoryReader<char> reader = new(buffer); + do + { + //Get an available buffer window to store characters in and convert the characters to binary + Enc.Convert(reader.Window.Span, _buffer.Remaining, true, out int charsUsed, out int bytesUsed, out completed); + //Update byte position + _buffer.Advance(bytesUsed); + //Update char position + reader.Advance(charsUsed); + //Converting did not complete because the buffer was too small + if (!completed || reader.WindowSize == 0) + { + //Flush the buffer and continue + await FlushWriterAsync(cancellationToken); + } + } while (!completed); + //Reset the encoder + Enc.Reset(); + } + + ///<inheritdoc/> + public override void WriteLine() + { + Check(); + //See if there is room in the binary buffer + if (_buffer.RemainingSize < LineTermination.Length) + { + //There is not enough room to store the termination, so we need to flush the buffer + Flush(); + } + _buffer.Append(LineTermination.Span); + } + ///<inheritdoc/> + public override void WriteLine(object? value) => WriteLine(value?.ToString()); + ///<inheritdoc/> + public override void WriteLine(string? value) => WriteLine(value.AsSpan()); + ///<inheritdoc/> + public override void WriteLine(ReadOnlySpan<char> buffer) + { + //Write the value itself + Write(buffer); + //Write the line termination + WriteLine(); + } + + ///<inheritdoc/> + ///<exception cref="ObjectDisposedException"></exception> + public override void Flush() + { + Check(); + //If data is available to be written, write it to the base stream + if (_buffer.AccumulatedSize > 0) + { + //Write all buffered data to stream + BaseStream.Write(_buffer.Accumulated); + //Reset the buffer + _buffer.Reset(); + } + } + /// <summary> + /// Asynchronously flushes the internal buffers to the <see cref="BaseStream"/>, and resets the internal buffer state + /// </summary> + /// <returns>A <see cref="ValueTask"/> that represents the asynchronous flush operation</returns> + /// <exception cref="ObjectDisposedException"></exception> + public async ValueTask FlushWriterAsync(CancellationToken cancellationToken = default) + { + Check(); + if (_buffer.AccumulatedSize > 0) + { + //Flush current window to the stream + await BaseStream.WriteAsync(_buffer.AccumulatedBuffer, cancellationToken); + //Reset the buffer + _buffer.Reset(); + } + } + + ///<inheritdoc/> + public override Task FlushAsync() => FlushWriterAsync().AsTask(); + + /// <summary> + /// Resets internal properies for resuse + /// </summary> + protected void Reset() + { + _buffer.Reset(); + Enc.Reset(); + } + ///<inheritdoc/> + public override void Close() + { + //Only invoke close once + if (closed) + { + return; + } + try + { + Flush(); + } + finally + { + //Release the memory handle if its set + _buffer.Close(); + //Set closed flag + closed = true; + } + } + ///<inheritdoc/> + protected override void Dispose(bool disposing) + { + Close(); + base.Dispose(disposing); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Check() + { + if (closed) + { + throw new ObjectDisposedException("The stream is closed"); + } + } + ///<inheritdoc/> + public override async ValueTask DisposeAsync() + { + //Only invoke close once + if (closed) + { + return; + } + try + { + await FlushWriterAsync(); + } + finally + { + //Set closed flag + closed = true; + //Release the memory handle if its set + _buffer.Close(); + } + GC.SuppressFinalize(this); + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/VnTextReaderExtensions.cs b/lib/Utils/src/IO/VnTextReaderExtensions.cs new file mode 100644 index 0000000..119461b --- /dev/null +++ b/lib/Utils/src/IO/VnTextReaderExtensions.cs @@ -0,0 +1,223 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: VnTextReaderExtensions.cs +* +* VnTextReaderExtensions.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; + +using VNLib.Utils.Extensions; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// Extension methods to help reuse code for used TextReader implementations + /// </summary> + public static class VnTextReaderExtensions + { + public const int E_BUFFER_TOO_SMALL = -1; + + + /* + * Generic extensions provide constained compiler method invocation + * for structs the implement the IVNtextReader + */ + + /// <summary> + /// Attempts to read a line from the stream and store it in the specified buffer + /// </summary> + /// <param name="reader"></param> + /// <param name="charBuffer">The character buffer to write data to</param> + /// <returns>Returns the number of bytes read, <see cref="E_BUFFER_TOO_SMALL"/> + /// if the buffer was not large enough, 0 if no data was available</returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <remarks>Allows reading lines of data from the stream without allocations</remarks> + public static ERRNO ReadLine<T>(this ref T reader, Span<char> charBuffer) where T:struct, IVnTextReader + { + return readLine(ref reader, charBuffer); + } + /// <summary> + /// Attempts to read a line from the stream and store it in the specified buffer + /// </summary> + /// <param name="reader"></param> + /// <param name="charBuffer">The character buffer to write data to</param> + /// <returns>Returns the number of bytes read, <see cref="E_BUFFER_TOO_SMALL"/> + /// if the buffer was not large enough, 0 if no data was available</returns> + /// <exception cref="OutOfMemoryException"></exception> + /// <remarks>Allows reading lines of data from the stream without allocations</remarks> + public static ERRNO ReadLine<T>(this T reader, Span<char> charBuffer) where T : class, IVnTextReader + { + return readLine(ref reader, charBuffer); + } + + /// <summary> + /// Fill a buffer with reamining buffered data + /// </summary> + /// <param name="reader"></param> + /// <param name="buffer">Buffer to copy data to</param> + /// <param name="offset">Offset in buffer to begin writing</param> + /// <param name="count">Number of bytes to read</param> + /// <returns>The number of bytes copied to the input buffer</returns> + public static int ReadRemaining<T>(this ref T reader, byte[] buffer, int offset, int count) where T : struct, IVnTextReader + { + return reader.ReadRemaining(buffer.AsSpan(offset, count)); + } + /// <summary> + /// Fill a buffer with reamining buffered data + /// </summary> + /// <param name="reader"></param> + /// <param name="buffer">Buffer to copy data to</param> + /// <param name="offset">Offset in buffer to begin writing</param> + /// <param name="count">Number of bytes to read</param> + /// <returns>The number of bytes copied to the input buffer</returns> + public static int ReadRemaining<T>(this T reader, byte[] buffer, int offset, int count) where T : class, IVnTextReader + { + return reader.ReadRemaining(buffer.AsSpan(offset, count)); + } + + /// <summary> + /// Fill a buffer with reamining buffered data, up to + /// the size of the supplied buffer + /// </summary> + /// <param name="reader"></param> + /// <param name="buffer">Buffer to copy data to</param> + /// <returns>The number of bytes copied to the input buffer</returns> + /// <remarks>You should use the <see cref="IVnTextReader.Available"/> property to know how much remaining data is buffered</remarks> + public static int ReadRemaining<T>(this ref T reader, Span<byte> buffer) where T : struct, IVnTextReader + { + return readRemaining(ref reader, buffer); + } + /// <summary> + /// Fill a buffer with reamining buffered data, up to + /// the size of the supplied buffer + /// </summary> + /// <param name="reader"></param> + /// <param name="buffer">Buffer to copy data to</param> + /// <returns>The number of bytes copied to the input buffer</returns> + /// <remarks>You should use the <see cref="IVnTextReader.Available"/> property to know how much remaining data is buffered</remarks> + public static int ReadRemaining<T>(this T reader, Span<byte> buffer) where T : class, IVnTextReader + { + return readRemaining(ref reader, buffer); + } + + private static ERRNO readLine<T>(ref T reader, Span<char> chars) where T: IVnTextReader + { + /* + * I am aware of a potential bug, the line decoding process + * shifts the interal buffer by the exact number of bytes to + * the end of the line, without considering if the decoder failed + * to properly decode the entire line. + * + * I dont expect this to be an issue unless there is a bug within the specified + * encoder implementation + */ + ReadOnlySpan<byte> LineTermination = reader.LineTermination.Span; + //If buffered data is available, check for line termination + if (reader.Available > 0) + { + //Get current buffer window + ReadOnlySpan<byte> bytes = reader.BufferedDataWindow; + //search for line termination in current buffer + int term = bytes.IndexOf(LineTermination); + //Termination found in buffer window + if (term > -1) + { + //Capture the line from the begining of the window to the termination + ReadOnlySpan<byte> line = bytes[..term]; + //Get the number ot chars + int charCount = reader.Encoding.GetCharCount(line); + //See if the buffer is large enough + if (bytes.Length < charCount) + { + return E_BUFFER_TOO_SMALL; + } + //Use the decoder to convert the data + _ = reader.Encoding.GetChars(line, chars); + //Shift the window to the end of the line (excluding the termination, regardless of the conversion result) + reader.Advance(term + LineTermination.Length); + //Return the number of characters + return charCount; + } + //Termination not found but there may be more data waiting + } + //Compact the buffer window and make sure it was compacted so there is room to fill the buffer + if (reader.CompactBufferWindow()) + { + //There is room, so buffer more data + reader.FillBuffer(); + //Check again to see if more data is buffered + if (reader.Available <= 0) + { + //No data avialable + return 0; + } + //Get current buffer window + ReadOnlySpan<byte> bytes = reader.BufferedDataWindow; + //search for line termination in current buffer + int term = bytes.IndexOf(LineTermination); + //Termination found in buffer window + if (term > -1) + { + //Capture the line from the begining of the window to the termination + ReadOnlySpan<byte> line = bytes[..term]; + //Get the number ot chars + int charCount = reader.Encoding.GetCharCount(line); + //See if the buffer is large enough + if (bytes.Length < charCount) + { + return E_BUFFER_TOO_SMALL; + } + //Use the decoder to convert the data + _ = reader.Encoding.GetChars(line, chars); + //Shift the window to the end of the line (excluding the termination, regardless of the conversion result) + reader.Advance(term + LineTermination.Length); + //Return the number of characters + return charCount; + } + } + + //Termination not found within the entire buffer, so buffer space has been exhausted + + //Supress as this response is expected when the buffer is exhausted, +#pragma warning disable CA2201 // Do not raise reserved exception types + throw new OutOfMemoryException("The line was not found within the current buffer, cannot continue"); +#pragma warning restore CA2201 // Do not raise reserved exception types + } + + private static int readRemaining<T>(ref T reader, Span<byte> buffer) where T: IVnTextReader + { + //guard for empty buffer + if (buffer.Length == 0 || reader.Available == 0) + { + return 0; + } + //get the remaining bytes in the reader + Span<byte> remaining = reader.BufferedDataWindow; + //Calculate the number of bytes to copy + int canCopy = Math.Min(remaining.Length, buffer.Length); + //Copy remaining bytes to buffer + remaining[..canCopy].CopyTo(buffer); + //Shift the window by the number of bytes copied + reader.Advance(canCopy); + return canCopy; + } + } +}
\ No newline at end of file diff --git a/lib/Utils/src/IO/WriteOnlyBufferedStream.cs b/lib/Utils/src/IO/WriteOnlyBufferedStream.cs new file mode 100644 index 0000000..5e7faa1 --- /dev/null +++ b/lib/Utils/src/IO/WriteOnlyBufferedStream.cs @@ -0,0 +1,255 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Utils +* File: WriteOnlyBufferedStream.cs +* +* WriteOnlyBufferedStream.cs is part of VNLib.Utils which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Utils is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published +* by the Free Software Foundation, either version 2 of the License, +* or (at your option) any later version. +* +* VNLib.Utils 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 +* General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with VNLib.Utils. If not, see http://www.gnu.org/licenses/. +*/ + +using System; +using System.IO; +using System.Buffers; +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Utils.Memory; + +namespace VNLib.Utils.IO +{ + /// <summary> + /// A basic accumulator style write buffered stream + /// </summary> + public class WriteOnlyBufferedStream : Stream + { + private readonly ISlindingWindowBuffer<byte> _buffer; + private readonly bool LeaveOpen; + + /// <summary> + /// Gets the underlying stream that interfaces with the backing store + /// </summary> + public Stream BaseStream { get; init; } + + /// <summary> + /// Initalizes a new <see cref="WriteOnlyBufferedStream"/> using the + /// specified backing stream, using the specified buffer size, and + /// optionally leaves the stream open + /// </summary> + /// <param name="baseStream">The backing stream to write buffered data to</param> + /// <param name="bufferSize">The size of the internal buffer</param> + /// <param name="leaveOpen">A value indicating of the stream should be left open when the buffered stream is closed</param> + public WriteOnlyBufferedStream(Stream baseStream, int bufferSize, bool leaveOpen = false) + { + BaseStream = baseStream; + //Create buffer + _buffer = InitializeBuffer(bufferSize); + LeaveOpen = leaveOpen; + } + /// <summary> + /// Invoked by the constuctor method to allocte the internal buffer with the specified buffer size. + /// </summary> + /// <param name="bufferSize">The requested size of the buffer to alloc</param> + /// <remarks>By default requests the buffer from the <see cref="ArrayPool{T}.Shared"/> instance</remarks> + protected virtual ISlindingWindowBuffer<byte> InitializeBuffer(int bufferSize) + { + return new ArrayPoolStreamBuffer<byte>(ArrayPool<byte>.Shared, bufferSize); + } + + ///<inheritdoc/> + public override void Close() + { + try + { + //Make sure the buffer is empty + WriteBuffer(); + + if (!LeaveOpen) + { + //Dispose stream + BaseStream.Dispose(); + } + } + finally + { + _buffer.Close(); + } + } + ///<inheritdoc/> + public override async ValueTask DisposeAsync() + { + try + { + if (_buffer.AccumulatedSize > 0) + { + await WriteBufferAsync(CancellationToken.None); + } + + if (!LeaveOpen) + { + //Dispose stream + await BaseStream.DisposeAsync(); + } + + GC.SuppressFinalize(this); + } + finally + { + _buffer.Close(); + } + } + + ///<inheritdoc/> + public override void Flush() => WriteBuffer(); + ///<inheritdoc/> + public override Task FlushAsync(CancellationToken cancellationToken) => WriteBufferAsync(cancellationToken).AsTask(); + + private void WriteBuffer() + { + //Only if data is available to write + if (_buffer.AccumulatedSize > 0) + { + //Write data to stream + BaseStream.Write(_buffer.Accumulated); + //Reset position + _buffer.Reset(); + } + } + + private async ValueTask WriteBufferAsync(CancellationToken token = default) + { + if(_buffer.AccumulatedSize > 0) + { + await BaseStream.WriteAsync(_buffer.AccumulatedBuffer, token); + _buffer.Reset(); + } + } + ///<inheritdoc/> + public override void Write(byte[] buffer, int offset, int count) => Write(buffer.AsSpan(offset, count)); + + public override void Write(ReadOnlySpan<byte> buffer) + { + ForwardOnlyReader<byte> reader = new(buffer); + //Attempt to buffer/flush data until all data is sent + do + { + //Try to buffer as much as possible + ERRNO buffered = _buffer.TryAccumulate(reader.Window); + + if(buffered < reader.WindowSize) + { + //Buffer is full and needs to be flushed + WriteBuffer(); + //Advance reader and continue to buffer + reader.Advance(buffered); + continue; + } + + break; + } + while (true); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } + + public async override ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default) + { + ForwardOnlyMemoryReader<byte> reader = new(buffer); + //Attempt to buffer/flush data until all data is sent + do + { + //Try to buffer as much as possible + ERRNO buffered = _buffer.TryAccumulate(reader.Window.Span); + + if (buffered < reader.WindowSize) + { + //Buffer is full and needs to be flushed + await WriteBufferAsync(cancellationToken); + //Advance reader and continue to buffer + reader.Advance(buffered); + continue; + } + + break; + } + while (true); + } + + + /// <summary> + /// Always false + /// </summary> + public override bool CanRead => false; + /// <summary> + /// Always returns false + /// </summary> + public override bool CanSeek => false; + /// <summary> + /// Always true + /// </summary> + public override bool CanWrite => true; + /// <summary> + /// Returns the size of the underlying buffer + /// </summary> + public override long Length => _buffer.AccumulatedSize; + /// <summary> + /// Always throws <see cref="NotSupportedException"/> + /// </summary> + /// <exception cref="NotSupportedException"></exception> + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + /// <summary> + /// Always throws <see cref="NotSupportedException"/> + /// </summary> + /// <exception cref="NotSupportedException"></exception> + /// <returns></returns> + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("This stream is not readable"); + } + + /// <summary> + /// Always throws <see cref="NotSupportedException"/> + /// </summary> + /// <exception cref="NotSupportedException"></exception> + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// <summary> + /// Always throws <see cref="NotSupportedException"/> + /// </summary> + /// <exception cref="NotSupportedException"></exception> + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} |