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
}
}