using System; using System.Collections.Generic; using System.IO; using SabreTools.Numerics.Extensions; using SabreTools.Text.Extensions; namespace SabreTools.IO.Extensions { public static class StreamExtensions { /// /// Align the stream position to a byte-size boundary /// /// Input stream to try aligning /// Number of bytes to align on /// True if the stream could be aligned, false otherwise public static bool AlignToBoundary(this Stream? input, int alignment) { // If the stream is invalid if (input is null || input.Length == 0 || !input.CanRead) return false; // If already at the end of the stream if (input.Position >= input.Length) return false; // Align the stream position while (input.Position % alignment != 0 && input.Position < input.Length) { _ = input.ReadByteValue(); } // Return if the alignment completed return input.Position % alignment == 0; } /// /// Block-copy an input stream to an output stream, absorbing any errors /// /// Input stream to copy from /// Ouput stream to copy to /// Number of bytes to read at a time, default 8192 /// True if the copy succeeded without an exception, false otherwise /// This may result in incomplete outputs if an exception occurs public static bool BlockCopy(this Stream? input, Stream? output, int blockSize = 8192) { // If either stream is invalid if (input is null || output is null) return false; // If the input is unreadable if (!input.CanRead) return false; // If the output is not writable if (!output.CanWrite) return false; // If the block size is invalid in some way if (blockSize <= 0) return false; try { // Copy the array in blocks byte[] buffer = new byte[blockSize]; while (true) { int read = input.Read(buffer, 0, blockSize); if (read <= 0) break; output.Write(buffer, 0, read); } return true; } catch { // Absorb the error return false; } } #region InterleaveWith /// /// Interleave two files into a single output /// /// First file to interleave /// Second file to interleave /// Path to the output file /// Number of bytes read before switching input /// True if the files were interleaved successfully, false otherwise public static bool InterleaveWith(this string even, string odd, string output, int blockSize) { // If either file does not exist if (!File.Exists(even) || !File.Exists(odd)) return false; try { // Get the input and output streams using var evenStream = File.Open(even, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var oddStream = File.Open(odd, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var outputStream = File.Open(output, FileMode.Create, FileAccess.Write, FileShare.None); // Interleave the streams return evenStream.InterleaveWith(oddStream, outputStream, blockSize); } catch { // Absorb all errors for now return false; } } /// /// Interleave two streams into a single output /// /// First stream to interleave /// Second stream to interleave /// Output stream /// Number of bytes read before switching input /// A filled stream on success, null otherwise /// /// Thrown if is non-positive. /// public static bool InterleaveWith(this Stream even, Stream odd, Stream output, int blockSize) { // If either stream is unreadable if (!even.CanRead || !odd.CanRead) return false; // If the output is unwritable if (!output.CanWrite) return false; // If the block size is invalid if (blockSize <= 0) throw new ArgumentOutOfRangeException(nameof(blockSize)); try { // Alternate between inputs during reading bool useEven = true; while (even.Position < even.Length || odd.Position < odd.Length) { byte[] read = new byte[blockSize]; int actual = (useEven ? even : odd).Read(read, 0, blockSize); output.Write(read, 0, actual); output.Flush(); useEven = !useEven; } return true; } catch { // Absorb all errors for now return false; } } #endregion /// /// Read a number of bytes from an offset in a stream, if possible /// /// Input stream to read from /// Offset within the stream to start reading /// Number of bytes to read from the offset /// Indicates if the original position of the stream should be retained after reading /// Filled byte array on success, null on error /// /// This method will return a null array if the length is greater than what is left /// in the stream. This is different behavior than a normal stream read that would /// attempt to read as much as possible, returning the amount of bytes read. /// public static byte[]? ReadFrom(this Stream? input, long offset, int length, bool retainPosition) { if (input is null || !input.CanRead || !input.CanSeek) return null; if (offset < 0 || offset >= input.Length) return null; if (length < 0 || offset + length > input.Length) return null; // Cache the current location long currentLocation = input.Position; // Seek to the requested offset long newPosition = input.SeekIfPossible(offset); if (newPosition != offset) return null; // Read from the position byte[] data = input.ReadBytes(length); // Seek back if requested if (retainPosition) _ = input.SeekIfPossible(currentLocation); // Return the read data return data; } /// /// Read string data from a Stream /// /// Number of characters needed to be a valid string, default 5 /// Position in the source to read from /// Length of the requested data /// String list containing the requested data, null on error #if NET5_0_OR_GREATER /// This reads both Latin1 and UTF-16 strings from the input data #else /// This reads both ASCII and UTF-16 strings from the input data #endif public static List? ReadStringsFrom(this Stream? input, int position, int length, int charLimit = 5) { // Read the data as a byte array first byte[]? data = input.ReadFrom(position, length, retainPosition: true); if (data is null) return null; return data.ReadStringsFrom(charLimit); } #region SeekIfPossible /// /// Seek to a specific point in the stream, if possible /// /// Input stream to try seeking on /// Optional offset to seek to public static long SeekIfPossible(this Stream input, long offset = 0) => input.SeekIfPossible(offset, offset < 0 ? SeekOrigin.End : SeekOrigin.Begin); /// /// Seek to a specific point in the stream, if possible /// /// Input stream to try seeking on /// Optional offset to seek to public static long SeekIfPossible(this Stream input, long offset, SeekOrigin origin) { // If the input is not seekable, just return the current position if (!input.CanSeek) { try { return input.Position; } catch { return -1; } } // Attempt to seek to the offset try { return input.Seek(offset, origin); } catch { return -1; } } #endregion /// /// Check if a segment is valid in the stream /// /// Input stream to validate /// Position in the source /// Length of the data to check /// True if segment could be read fully, false otherwise public static bool SegmentValid(this Stream? input, long offset, long count) { if (input is null) return false; if (offset < 0 || offset > input.Length) return false; if (count < 0 || offset + count > input.Length) return false; return true; } #region SplitToChunks /// /// Split an input file into files of up to bytes /// /// Input file name /// Path to the output directory /// Maximum number of bytes to split on /// True if the file could be split, false otherwise public static bool SplitToChunks(this string input, string? outputDir, int blockSize) { // If the file does not exist if (!File.Exists(input)) return false; try { // Get the input stream using var inputStream = File.Open(input, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); // Get the base filename for output files outputDir ??= Path.GetDirectoryName(input); string baseFilename = Path.GetFileName(input); if (!string.IsNullOrEmpty(outputDir)) baseFilename = Path.Combine(outputDir, baseFilename); // Attempt to split the input return SplitToChunks(inputStream, baseFilename, blockSize); } catch { // Absorb all errors for now return false; } } /// /// Split an input file into files of up to bytes /// /// Input file name /// Path used as a base filename when generating numbered chunks /// Maximum number of bytes to split on /// True if the file could be split, false otherwise /// /// Thrown if is non-positive. /// public static bool SplitToChunks(this Stream input, string baseFilename, int blockSize) { // If the stream is unreadable if (!input.CanRead) return false; // If the block size is invalid if (blockSize <= 0) throw new ArgumentOutOfRangeException(nameof(blockSize)); try { // Create the output directory, if possible string? outputDirectory = Path.GetDirectoryName(Path.GetFullPath(baseFilename)); if (outputDirectory is not null && !Directory.Exists(outputDirectory)) Directory.CreateDirectory(outputDirectory); // Loop while there is data left int part = 0; while (input.Position < input.Length) { // Create the next output file using var partStream = File.Open($"{baseFilename}.{part++}", FileMode.Create, FileAccess.Write, FileShare.None); // Process the next block of data byte[] data = new byte[blockSize]; int actual = input.Read(data, 0, blockSize); partStream.Write(data, 0, actual); partStream.Flush(); } return true; } catch { // Absorb all errors for now return false; } } #endregion #region SplitToEvenOdd /// /// Split an input file into two outputs /// /// Input file name /// Output file name for even blocks, must be distinct from /// Output file name for odd blocks, must be distinct from /// Number of bytes read before switching output /// True if the file could be split, false otherwise /// /// If and point to the same file, then there will be an /// internal exception when trying to create the output files which is absorbed by this method. /// public static bool SplitToEvenOdd(this string input, string even, string odd, int blockSize) { // If the file does not exist if (!File.Exists(input)) return false; try { // Get the input and output streams using var inputStream = File.Open(input, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var evenStream = File.Open(even, FileMode.Create, FileAccess.Write, FileShare.None); using var oddStream = File.Open(odd, FileMode.Create, FileAccess.Write, FileShare.None); // Split the stream return SplitToEvenOdd(inputStream, evenStream, oddStream, blockSize); } catch { // Absorb all errors for now return false; } } /// /// Split an input stream into two output streams /// /// Input stream /// Output stream for even blocks /// Output stream for odd blocks /// Number of bytes read before switching output /// True if the stream could be split, false otherwise /// /// Thrown if is non-positive. /// /// /// If and point to the same stream, then only half /// of the expected output will exist because both streams will not be pointing to the same index. /// public static bool SplitToEvenOdd(this Stream input, Stream even, Stream odd, int blockSize) { // If the stream is unreadable if (!input.CanRead) return false; // If either output is unwritable if (!even.CanWrite || !odd.CanWrite) return false; // If the block size is invalid if (blockSize <= 0) throw new ArgumentOutOfRangeException(nameof(blockSize)); try { // Alternate between inputs during reading bool useEven = true; while (input.Position < input.Length) { byte[] read = new byte[blockSize]; int actual = input.Read(read, 0, blockSize); (useEven ? even : odd).Write(read, 0, actual); (useEven ? even : odd).Flush(); useEven = !useEven; } return true; } catch { // Absorb all errors for now return false; } } #endregion #region Swap /// /// Transform an input file using the given rule /// /// Input file name /// Output file name /// Transform operation to carry out /// True if the file was transformed properly, false otherwise public static bool Swap(this string input, string output, SwapOperation operation) { // If the file does not exist if (!File.Exists(input)) return false; // Create the output directory if it doesn't already string? outputDirectory = Path.GetDirectoryName(Path.GetFullPath(output)); if (outputDirectory is not null && !Directory.Exists(outputDirectory)) Directory.CreateDirectory(outputDirectory); try { // Get the input and output streams using var inputStream = File.Open(input, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var outputStream = File.Open(output, FileMode.Create, FileAccess.Write, FileShare.None); // Transform the stream return Swap(inputStream, outputStream, operation); } catch { // Absorb all errors for now return false; } } /// /// Transform an input stream using the given rule /// /// Input stream /// Output stream /// Transform operation to carry out /// True if the file was transformed properly, false otherwise /// /// Thrown if is not a recognized value. /// public static bool Swap(this Stream input, Stream output, SwapOperation operation) { // If the input is unreadable if (!input.CanRead) return false; // If the output is unwritable if (!output.CanWrite) return false; // If the operation is not defined if (!Enum.IsDefined(typeof(SwapOperation), operation)) return false; try { // Determine the cutoff boundary for the operation long endBoundary = operation switch { SwapOperation.Bitswap => input.Length, SwapOperation.Byteswap => input.Length - (input.Length % 2), SwapOperation.Wordswap => input.Length - (input.Length % 4), SwapOperation.WordByteswap => input.Length - (input.Length % 4), _ => throw new ArgumentOutOfRangeException(nameof(operation)), }; // Loop over the input and process in blocks byte[] buffer = new byte[4]; int pos = 0; while (input.Position < endBoundary) { byte b = (byte)input.ReadByte(); switch (operation) { case SwapOperation.Bitswap: uint r = b; int s = 7; for (b >>= 1; b != 0; b >>= 1) { r <<= 1; r |= (byte)(b & 1); s--; } r <<= s; buffer[pos] = (byte)r; break; case SwapOperation.Byteswap: if (pos % 2 == 1) buffer[pos - 1] = b; else buffer[pos + 1] = b; break; case SwapOperation.Wordswap: buffer[(pos + 2) % 4] = b; break; case SwapOperation.WordByteswap: buffer[3 - pos] = b; break; default: buffer[pos] = b; break; } // Set the buffer position to default write to pos = (pos + 1) % 4; // If the buffer pointer has been reset if (pos == 0) { output.Write(buffer, 0, buffer.Length); output.Flush(); buffer = new byte[4]; } } // If there's anything more in the buffer if (pos > 0) output.Write(buffer, 0, pos); // If the stream still has data if (input.Position < input.Length) { int remaining = (int)(input.Length - input.Position); byte[] bytes = new byte[remaining]; int read = input.Read(bytes, 0, remaining); output.Write(bytes, 0, read); output.Flush(); } return true; } catch { // Absorb all errors for now return false; } } #endregion } }