diff --git a/README.MD b/README.MD index b0159be..69cff38 100644 --- a/README.MD +++ b/README.MD @@ -92,6 +92,14 @@ Custom `Stream` implementations that are required for specialized use: - `ReadOnlyCompositeStream`: A readonly stream implementation that wraps multiple source streams in a set order - `ViewStream`: A readonly stream implementation representing a view into source data +### `SabreTools.IO.Transform` + +File and stream implementations of common data transformations: + +- Combine using either ordered concatenation or interleaving +- Split by even/odd chunks or based on block size +- Convert data either by bit-swapping, byte-swapping, word-swapping, or word/byte-swapping + ### `SabreTools.Text.Compare` Classes focused on string comparison by natural sorting. For example, "5" would be sorted before "100". diff --git a/SabreTools.IO.Test/Transform/CombineTests.cs b/SabreTools.IO.Test/Transform/CombineTests.cs new file mode 100644 index 0000000..b7cc6f9 --- /dev/null +++ b/SabreTools.IO.Test/Transform/CombineTests.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.IO; +using SabreTools.IO.Transform; +using Xunit; + +namespace SabreTools.IO.Test.Transform +{ + public class CombineTests + { + #region Concatenate + + [Fact] + public void Concatenate_EmptyList_False() + { + List paths = []; + string output = string.Empty; + bool actual = Combine.Concatenate(paths, output); + Assert.False(actual); + } + + [Fact] + public void Concatenate_InvalidOutput_False() + { + List paths = ["a"]; + string output = string.Empty; + bool actual = Combine.Concatenate(paths, output); + Assert.False(actual); + } + + [Fact] + public void Concatenate_FilledList_True() + { + List paths = [ + Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"), + Path.Combine(Environment.CurrentDirectory, "TestData", "file-to-compress.bin"), + ]; + string output = Guid.NewGuid().ToString(); + bool actual = Combine.Concatenate(paths, output); + Assert.True(actual); + + string text = File.ReadAllText(output); + Assert.Equal("This doesn't match anythingThis is just a file that has a known set of hashes to make sure that everything with hashing is still working as anticipated.", text); + + File.Delete(output); + } + + #endregion + + #region Interleave + + [Fact] + public void Interleave_EvenNotExists_False() + { + string even = "NOT A REAL PATH"; + string odd = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string output = Guid.NewGuid().ToString(); + + bool actual = Combine.Interleave(even, odd, output, BlockSize.Byte); + Assert.False(actual); + } + + [Fact] + public void Interleave_OddNotExists_False() + { + string even = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string odd = "NOT A REAL PATH"; + string output = Guid.NewGuid().ToString(); + + bool actual = Combine.Interleave(even, odd, output, BlockSize.Byte); + Assert.False(actual); + } + + [Fact] + public void Interleave_InvalidType_False() + { + string even = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string odd = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string output = Guid.NewGuid().ToString(); + + bool actual = Combine.Interleave(even, odd, output, (BlockSize)int.MaxValue); + Assert.False(actual); + } + + [Theory] + [InlineData(BlockSize.Byte, "TThhiiss ddooeessnn''tt mmaattcchh aannyytthhiinngg")] + [InlineData(BlockSize.Word, "ThThisis d doeoesnsn't't m matatchch a anynyththiningg")] + [InlineData(BlockSize.Dword, "ThisThis doe doesn'tsn't mat match ach anythnythinging")] + [InlineData(BlockSize.Qword, "This doeThis doesn't matsn't match anythch anythinging")] + public void Interleave_SameLength_True(BlockSize type, string expected) + { + string even = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string odd = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string output = Guid.NewGuid().ToString(); + + bool actual = Combine.Interleave(even, odd, output, type); + Assert.True(actual); + + string text = File.ReadAllText(output); + Assert.Equal(expected, text); + + File.Delete(output); + } + + [Fact] + public void Interleave_DifferentLength_True() + { + string even = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string odd = Path.Combine(Environment.CurrentDirectory, "TestData", "file-to-compress.bin"); + + string output = Guid.NewGuid().ToString(); + + bool actual = Combine.Interleave(even, odd, output, BlockSize.Byte); + Assert.True(actual); + + string text = File.ReadAllText(output); + Assert.Equal("TThhiiss diose sjnu'stt maa tfcihl ea ntyhtahti nhgas a known set of hashes to make sure that everything with hashing is still working as anticipated.", text); + + File.Delete(output); + } + + #endregion + } +} diff --git a/SabreTools.IO.Test/Transform/SplitTests.cs b/SabreTools.IO.Test/Transform/SplitTests.cs new file mode 100644 index 0000000..5247f53 --- /dev/null +++ b/SabreTools.IO.Test/Transform/SplitTests.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using SabreTools.IO.Transform; +using Xunit; + +namespace SabreTools.IO.Test.Transform +{ + public class SplitTests + { + #region BlockSplit + + [Fact] + public void BlockSplit_EmptyFileName_False() + { + string input = string.Empty; + string outputDir = string.Empty; + bool actual = Split.BlockSplit(input, outputDir, BlockSize.Byte); + Assert.False(actual); + } + + [Fact] + public void BlockSplit_InvalidFile_False() + { + string input = "INVALID"; + string outputDir = string.Empty; + bool actual = Split.BlockSplit(input, outputDir, BlockSize.Byte); + Assert.False(actual); + } + + [Fact] + public void BlockSplit_InvalidType_False() + { + string input = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string outputDir = Guid.NewGuid().ToString(); + + bool actual = Split.BlockSplit(input, outputDir, (BlockSize)int.MaxValue); + Assert.False(actual); + } + + [Theory] + [InlineData(BlockSize.Byte, "Ti os' ac ntig", "hsdentmthayhn")] + [InlineData(BlockSize.Word, "Th dsn mchnyin", "isoe'tat athg")] + [InlineData(BlockSize.Dword, "Thissn'tch aing", " doe matnyth")] + [InlineData(BlockSize.Qword, "This doech anyth", "sn't mating")] + public void BlockSplit_ValidFile_True(BlockSize type, string expectedEven, string expectedOdd) + { + string input = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string outputDir = Guid.NewGuid().ToString(); + + bool actual = Split.BlockSplit(input, outputDir, type); + Assert.True(actual); + + string baseFilename = Path.GetFileName(input); + string text = File.ReadAllText(Path.Combine(outputDir, $"{baseFilename}.even")); + Assert.Equal(expectedEven, text); + text = File.ReadAllText(Path.Combine(outputDir, $"{baseFilename}.odd")); + Assert.Equal(expectedOdd, text); + + File.Delete($"{baseFilename}.even"); + File.Delete($"{baseFilename}.odd"); + } + + #endregion + + #region SizeSplit + + [Fact] + public void SizeSplit_EmptyFileName_False() + { + string input = string.Empty; + string outputDir = string.Empty; + int size = 1; + + bool actual = Split.SizeSplit(input, outputDir, size); + Assert.False(actual); + } + + [Fact] + public void SizeSplit_InvalidFile_False() + { + string input = "INVALID"; + string outputDir = string.Empty; + int size = 1; + + bool actual = Split.SizeSplit(input, outputDir, size); + Assert.False(actual); + } + + [Fact] + public void SizeSplit_InvalidSize_False() + { + string input = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string outputDir = string.Empty; + int size = 0; + + bool actual = Split.SizeSplit(input, outputDir, size); + Assert.False(actual); + } + + [Fact] + public void SizeSplit_Valid_True() + { + string input = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string outputDir = Guid.NewGuid().ToString(); + int size = 16; + + bool actual = Split.SizeSplit(input, outputDir, size); + Assert.True(actual); + + Assert.Equal(2, Directory.GetFiles(outputDir).Length); + + string baseFilename = Path.GetFileName(input); + string text = File.ReadAllText(Path.Combine(outputDir, $"{baseFilename}.0")); + Assert.Equal("This doesn't mat", text); + text = File.ReadAllText(Path.Combine(outputDir, $"{baseFilename}.1")); + Assert.Equal("ch anything", text); + + File.Delete($"{baseFilename}.0"); + File.Delete($"{baseFilename}.1"); + } + + #endregion + } +} diff --git a/SabreTools.IO.Test/Transform/SwapTests.cs b/SabreTools.IO.Test/Transform/SwapTests.cs new file mode 100644 index 0000000..e3e074b --- /dev/null +++ b/SabreTools.IO.Test/Transform/SwapTests.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using SabreTools.IO.Extensions; +using SabreTools.IO.Transform; +using Xunit; + +namespace SabreTools.IO.Test.Transform +{ + public class SwapTests + { + #region Process + + [Fact] + public void Process_EmptyFileName_False() + { + string input = string.Empty; + string output = string.Empty; + bool actual = Swap.Process(input, output, Operation.Byteswap); + Assert.False(actual); + } + + [Fact] + public void Process_InvalidFile_False() + { + string input = "INVALID"; + string output = string.Empty; + bool actual = Swap.Process(input, output, Operation.Byteswap); + Assert.False(actual); + } + + [Fact] + public void Process_InvalidType_False() + { + string input = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string output = Guid.NewGuid().ToString(); + + bool actual = Swap.Process(input, output, (Operation)int.MaxValue); + Assert.False(actual); + } + + [Fact] + public void Process_Valid_True() + { + string input = Path.Combine(Environment.CurrentDirectory, "TestData", "ascii.txt"); + string output = Guid.NewGuid().ToString(); + + // Bitswap + bool actual = Swap.Process(input, output, Operation.Bitswap); + Assert.True(actual); + byte[] actualBytes = File.ReadAllBytes(output); + Assert.True(new byte[] { 0x2A, 0x16, 0x96, 0xCE, 0x04, 0x26, 0xF6, 0xA6, 0xCE, 0x76, 0xE4, 0x2E, 0x04, 0xB6, 0x86, 0x2E, 0xC6, 0x16, 0x04, 0x86, 0x76, 0x9E, 0x2E, 0x16, 0x96, 0x76, 0xE6 }.EqualsExactly(actualBytes)); + + // Byteswap + actual = Swap.Process(input, output, Operation.Byteswap); + Assert.True(actual); + actualBytes = File.ReadAllBytes(output); + Assert.True(new byte[] { 0x68, 0x54, 0x73, 0x69, 0x64, 0x20, 0x65, 0x6F, 0x6E, 0x73, 0x74, 0x27, 0x6D, 0x20, 0x74, 0x61, 0x68, 0x63, 0x61, 0x20, 0x79, 0x6E, 0x68, 0x74, 0x6E, 0x69, 0x67 }.EqualsExactly(actualBytes)); + + // Wordswap + actual = Swap.Process(input, output, Operation.Wordswap); + Assert.True(actual); + actualBytes = File.ReadAllBytes(output); + Assert.True(new byte[] { 0x69, 0x73, 0x54, 0x68, 0x6F, 0x65, 0x20, 0x64, 0x27, 0x74, 0x73, 0x6E, 0x61, 0x74, 0x20, 0x6D, 0x20, 0x61, 0x63, 0x68, 0x74, 0x68, 0x6E, 0x79, 0x69, 0x6E, 0x67 }.EqualsExactly(actualBytes)); + + // WordByteswap + actual = Swap.Process(input, output, Operation.WordByteswap); + Assert.True(actual); + actualBytes = File.ReadAllBytes(output); + Assert.True(new byte[] { 0x73, 0x69, 0x68, 0x54, 0x65, 0x6F, 0x64, 0x20, 0x74, 0x27, 0x6E, 0x73, 0x74, 0x61, 0x6D, 0x20, 0x61, 0x20, 0x68, 0x63, 0x68, 0x74, 0x79, 0x6E, 0x69, 0x6E, 0x67 }.EqualsExactly(actualBytes)); + + File.Delete(output); + } + + #endregion + } +} diff --git a/SabreTools.IO/Transform/Combine.cs b/SabreTools.IO/Transform/Combine.cs new file mode 100644 index 0000000..5dc5995 --- /dev/null +++ b/SabreTools.IO/Transform/Combine.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace SabreTools.IO.Transform +{ + /// + /// Helpers to combine inputs + /// + public static class Combine + { + /// + /// Concatenate all files in the order provided, if possible + /// + /// List of paths to combine + /// Path to the output file + /// True if the files were concatenated successfully, false otherwise + public static bool Concatenate(List paths, string output) + { + // If the path list is empty + if (paths.Count == 0) + return false; + + // If the output filename is invalid + if (string.IsNullOrEmpty(output)) + return false; + + try + { + // Try to build the new output file + using var ofs = File.Open(output, FileMode.Create, FileAccess.Write, FileShare.None); + + for (int i = 0; i < paths.Count; i++) + { + // Get the next file + string next = paths[i]; + if (!File.Exists(next)) + break; + + // Copy the next input to the output + using var ifs = File.Open(next, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + // Write in blocks + int read = 0; + do + { + byte[] buffer = new byte[3 * 1024 * 1024]; + + read = ifs.Read(buffer, 0, buffer.Length); + if (read == 0) + break; + + ofs.Write(buffer, 0, read); + ofs.Flush(); + } while (read > 0); + } + + return true; + } + catch + { + // Absorb the exception right now + return false; + } + } + + /// + /// Interleave two files into a single output + /// + /// First file to interleave + /// Second file to interleave + /// Path to the output file + /// representing how to process the inputs + /// True if the files were interleaved successfully, false otherwise + public static bool Interleave(string even, string odd, string output, BlockSize type) + { + // If either file does not exist + if (!File.Exists(even) || !File.Exists(odd)) + return false; + + try + { + // Get the input 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); + + // Interleave the streams + using var interleaved = Interleave(evenStream, oddStream, type); + if (interleaved == null) + return false; + + // Open the output file + using var outputStream = File.Open(output, FileMode.Create, FileAccess.Write, FileShare.None); + + // Write the interleaved data + interleaved.CopyTo(outputStream); + outputStream.Flush(); + } + catch + { + // Absorb all errors for now + return false; + } + + return true; + } + + /// + /// Interleave two streams into a single output + /// + /// First stream to interleave + /// Second stream to interleave + /// Path to the output file + /// representing how to process the inputs + /// A filled stream on success, null otherwise + public static Stream? Interleave(Stream even, Stream odd, BlockSize type) + { + // If either stream is unreadable + if (!even.CanRead || !odd.CanRead) + return null; + + // Get the number of bytes to process + int byteCount = type switch + { + BlockSize.Byte => 1, + BlockSize.Word => 2, + BlockSize.Dword => 4, + BlockSize.Qword => 8, + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + + try + { + // Create an output stream + var outputStream = new MemoryStream(); + + // Alternate between inputs during reading + bool useEven = true; + while (even.Position < even.Length || odd.Position < odd.Length) + { + byte[] read = new byte[byteCount]; + int actual = (useEven ? even : odd).Read(read, 0, byteCount); + outputStream.Write(read, 0, actual); + outputStream.Flush(); + useEven = !useEven; + } + + outputStream.Seek(0, SeekOrigin.Begin); + return outputStream; + } + catch + { + // Absorb all errors for now + return null; + } + } + } +} diff --git a/SabreTools.IO/Transform/Enums.cs b/SabreTools.IO/Transform/Enums.cs new file mode 100644 index 0000000..8c696ea --- /dev/null +++ b/SabreTools.IO/Transform/Enums.cs @@ -0,0 +1,54 @@ +namespace SabreTools.IO.Transform +{ + /// + /// Determines the block size of an operation + /// + public enum BlockSize + { + /// + /// 1 byte blocks + /// + Byte = 1, + + /// + /// 2 byte blocks + /// + Word = 2, + + /// + /// 4 byte blocks + /// + Dword = 4, + + /// + /// 8 byte blocks + /// + Qword = 8, + } + + /// + /// Determines the swapping operation + /// + public enum Operation + { + /// + /// Reverse endianness of each byte + /// + Bitswap, + + /// + /// Swap every 1 byte + /// + Byteswap, + + /// + /// Swap every 2 bytes + /// + Wordswap, + + /// + /// Swap every 2 bytes and bytes within the 2 bytes + /// + WordByteswap, + } +} diff --git a/SabreTools.IO/Transform/Split.cs b/SabreTools.IO/Transform/Split.cs new file mode 100644 index 0000000..ce5529f --- /dev/null +++ b/SabreTools.IO/Transform/Split.cs @@ -0,0 +1,177 @@ +using System; +using System.IO; + +namespace SabreTools.IO.Transform +{ + /// + /// Helpers to split inputs + /// + public static class Split + { + /// + /// Split an input file into two outputs + /// + /// Input file name + /// Path to the output directory + /// representing how to process the inputs + /// True if the file could be split, false otherwise + public static bool BlockSplit(string input, string? outputDir, BlockSize type) + { + // 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); + + // Split the stream + if (!BlockSplit(inputStream, type, out Stream? evenStream, out Stream? oddStream)) + return false; + else if (evenStream == null || oddStream == null) + return false; + + // 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); + + // Create the output directory, if possible + if (outputDir != null && !Directory.Exists(outputDir)) + Directory.CreateDirectory(outputDir); + + // Open the output files + using var outEvenStream = File.Open($"{baseFilename}.even", FileMode.Create, FileAccess.Write, FileShare.None); + using var outOddStream = File.Open($"{baseFilename}.odd", FileMode.Create, FileAccess.Write, FileShare.None); + + // Write the split data + evenStream.CopyTo(outEvenStream); + outEvenStream.Flush(); + oddStream.CopyTo(outOddStream); + outOddStream.Flush(); + } + catch + { + // Absorb all errors for now + return false; + } + + return true; + } + + /// + /// Split an input stream into two output streams + /// + /// Input stream + /// representing how to process the inputs + /// Even block output stream on success, null otherwise + /// Odd block output stream on success, null otherwise + /// True if the stream could be split, false otherwise + public static bool BlockSplit(Stream input, BlockSize type, out Stream? even, out Stream? odd) + { + // Set default values for the outputs + even = null; + odd = null; + + // If the stream is unreadable + if (!input.CanRead) + return false; + + // Get the number of bytes to process + int byteCount = type switch + { + BlockSize.Byte => 1, + BlockSize.Word => 2, + BlockSize.Dword => 4, + BlockSize.Qword => 8, + _ => throw new ArgumentOutOfRangeException(nameof(type)), + }; + + try + { + // Create the output streams + even = new MemoryStream(); + odd = new MemoryStream(); + + // Alternate between inputs during reading + bool useEven = true; + while (input.Position < input.Length) + { + byte[] read = new byte[byteCount]; + int actual = input.Read(read, 0, byteCount); + (useEven ? even : odd).Write(read, 0, actual); + (useEven ? even : odd).Flush(); + useEven = !useEven; + } + + even.Seek(0, SeekOrigin.Begin); + odd.Seek(0, SeekOrigin.Begin); + return true; + } + catch + { + // Absorb all errors for now + even = null; + odd = null; + return false; + } + } + + /// + /// 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 SizeSplit(string input, string? outputDir, int size) + { + // If the file does not exist + if (!File.Exists(input)) + return false; + + // If the size is invalid + if (size <= 0) + 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); + + // Create the output directory, if possible + if (outputDir != null && !Directory.Exists(outputDir)) + Directory.CreateDirectory(outputDir); + + // Loop while there is data left + int part = 0; + while (inputStream.Position < inputStream.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[size]; + int actual = inputStream.Read(data, 0, size); + partStream.Write(data, 0, actual); + partStream.Flush(); + } + + return true; + } + catch + { + // Absorb all errors for now + return false; + } + } + } +} diff --git a/SabreTools.IO/Transform/Swap.cs b/SabreTools.IO/Transform/Swap.cs new file mode 100644 index 0000000..ce9553c --- /dev/null +++ b/SabreTools.IO/Transform/Swap.cs @@ -0,0 +1,162 @@ +using System; +using System.IO; +using SabreTools.IO.Extensions; + +namespace SabreTools.IO.Transform +{ + /// + /// Helpers to perform swapping operations + /// + public static class 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 Process(string input, string output, Operation 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 != null && !Directory.Exists(outputDirectory)) + Directory.CreateDirectory(outputDirectory); + + try + { + // Get the input stream + using var inputStream = File.Open(input, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + + // Transform the stream + var transformed = Process(inputStream, operation); + if (transformed == null) + return false; + + // Open the output file + using var outputStream = File.Open(output, FileMode.Create, FileAccess.Write, FileShare.None); + + // Write the transformed data + transformed.CopyTo(outputStream); + outputStream.Flush(); + } + catch + { + // Absorb all errors for now + return false; + } + + return true; + } + + /// + /// Transform an input stream using the given rule + /// + /// Input stream + /// Transform operation to carry out + /// True if the file was transformed properly, false otherwise + public static Stream? Process(Stream input, Operation operation) + { + // If the stream is unreadable + if (!input.CanRead) + return null; + + // If the operation is not defined + if (!Enum.IsDefined(typeof(Operation), operation)) + return null; + + try + { + // Create an output stream + var output = new MemoryStream(); + + // Determine the cutoff boundary for the operation + long endBoundary = operation switch + { + Operation.Bitswap => input.Length, + Operation.Byteswap => input.Length - (input.Length % 2), + Operation.Wordswap => input.Length - (input.Length % 4), + Operation.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 = input.ReadByteValue(); + switch (operation) + { + case Operation.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 Operation.Byteswap: + if (pos % 2 == 1) + buffer[pos - 1] = b; + else + buffer[pos + 1] = b; + + break; + case Operation.Wordswap: + buffer[(pos + 2) % 4] = b; + break; + case Operation.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); + output.Flush(); + buffer = new byte[4]; + } + } + + // If there's anything more in the buffer + for (int i = 0; i < pos; i++) + { + output.Write(buffer[i]); + } + + // If the stream still has data + if (input.Position < input.Length) + { + byte[] bytes = input.ReadBytes((int)(input.Length - input.Position)); + output.Write(bytes); + output.Flush(); + } + + output.Seek(0, SeekOrigin.Begin); + return output; + } + catch + { + // Absorb all errors for now + return null; + } + } + } +}