diff --git a/src/SharpCompress/Compressor/ADC/ADCBase.cs b/src/SharpCompress/Compressor/ADC/ADCBase.cs new file mode 100644 index 00000000..c16be093 --- /dev/null +++ b/src/SharpCompress/Compressor/ADC/ADCBase.cs @@ -0,0 +1,210 @@ +// +// ADC.cs +// +// Author: +// Natalia Portillo +// +// Copyright (c) 2016 © Claunia.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using System.IO; + +namespace SharpCompress.Compressor.ADC +{ + /// + /// Provides static methods for decompressing Apple Data Compression data + /// + public static class ADCBase + { + const int Plain = 1; + const int TwoByte = 2; + const int ThreeByte = 3; + + static int GetChunkType(byte byt) + { + if ((byt & 0x80) == 0x80) + return Plain; + if ((byt & 0x40) == 0x40) + return ThreeByte; + return TwoByte; + } + + static int GetChunkSize(byte byt) + { + switch (GetChunkType(byt)) + { + case Plain: + return (byt & 0x7F) + 1; + case TwoByte: + return ((byt & 0x3F) >> 2) + 3; + case ThreeByte: + return (byt & 0x3F) + 4; + default: + return -1; + } + } + + static int GetOffset(byte[] chunk, int position) + { + switch (GetChunkType(chunk[position])) + { + case Plain: + return 0; + case TwoByte: + return ((chunk[position] & 0x03) << 8) + chunk[position + 1]; + case ThreeByte: + return (chunk[position + 1] << 8) + chunk[position + 2]; + default: + return -1; + } + } + + /// + /// Decompresses a byte buffer that's compressed with ADC + /// + /// Compressed buffer + /// Buffer to hold decompressed data + /// Max size for decompressed data + /// How many bytes are stored on + public static int Decompress(byte[] input, out byte[] output, int bufferSize = 262144) + { + return Decompress(new MemoryStream(input), out output, bufferSize); + } + + /// + /// Decompresses a stream that's compressed with ADC + /// + /// Stream containing compressed data + /// Buffer to hold decompressed data + /// Max size for decompressed data + /// How many bytes are stored on + public static int Decompress(Stream input, out byte[] output, int bufferSize = 262144) + { + output = null; + + if (input == null || input.Length == 0) + return 0; + + int start = (int)input.Position; + int position = (int)input.Position; + int chunkSize; + int offset; + int chunkType; + byte[] buffer = new byte[bufferSize]; + int outPosition = 0; + bool full = false; + MemoryStream tempMs; + + while (position < input.Length) + { + int readByte = input.ReadByte(); + if (readByte == -1) + break; + + chunkType = GetChunkType((byte)readByte); + + switch (chunkType) + { + case Plain: + chunkSize = GetChunkSize((byte)readByte); + if (outPosition + chunkSize > bufferSize) + { + full = true; + break; + } + input.Read(buffer, outPosition, chunkSize); + outPosition += chunkSize; + position += chunkSize + 1; + break; + case TwoByte: + tempMs = new MemoryStream(); + chunkSize = GetChunkSize((byte)readByte); + tempMs.WriteByte((byte)readByte); + tempMs.WriteByte((byte)input.ReadByte()); + offset = GetOffset(tempMs.ToArray(), 0); + if (outPosition + chunkSize > bufferSize) + { + full = true; + break; + } + if (offset == 0) + { + byte lastByte = buffer[outPosition - 1]; + for (int i = 0; i < chunkSize; i++) + { + buffer[outPosition] = lastByte; + outPosition++; + } + position += 2; + } + else + { + for (int i = 0; i < chunkSize; i++) + { + buffer[outPosition] = buffer[outPosition - offset - 1]; + outPosition++; + } + position += 2; + } + break; + case ThreeByte: + tempMs = new MemoryStream(); + chunkSize = GetChunkSize((byte)readByte); + tempMs.WriteByte((byte)readByte); + tempMs.WriteByte((byte)input.ReadByte()); + tempMs.WriteByte((byte)input.ReadByte()); + offset = GetOffset(tempMs.ToArray(), 0); + if (outPosition + chunkSize > bufferSize) + { + full = true; + break; + } + if (offset == 0) + { + byte lastByte = buffer[outPosition - 1]; + for (int i = 0; i < chunkSize; i++) + { + buffer[outPosition] = lastByte; + outPosition++; + } + position += 3; + } + else + { + for (int i = 0; i < chunkSize; i++) + { + buffer[outPosition] = buffer[outPosition - offset - 1]; + outPosition++; + } + position += 3; + } + break; + } + + if (full) + break; + } + + output = new byte[outPosition]; + Array.Copy(buffer, 0, output, 0, outPosition); + return position - start; + } + } +} diff --git a/src/SharpCompress/Compressor/ADC/ADCStream.cs b/src/SharpCompress/Compressor/ADC/ADCStream.cs new file mode 100644 index 00000000..1c3e9120 --- /dev/null +++ b/src/SharpCompress/Compressor/ADC/ADCStream.cs @@ -0,0 +1,169 @@ +// +// ADC.cs +// +// Author: +// Natalia Portillo +// +// Copyright (c) 2016 © Claunia.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using System; +using System.IO; + +namespace SharpCompress.Compressor.ADC +{ + /// + /// Provides a forward readable only stream that decompresses ADC data + /// + public class ADCStream : Stream + { + /// + /// This stream holds the compressed data + /// + private readonly Stream stream; + /// + /// Is this instance disposed? + /// + private bool isDisposed; + /// + /// Position in decompressed data + /// + private long position = 0; + /// + /// Buffer with currently used chunk of decompressed data + /// + private byte[] outBuffer; + /// + /// Position in buffer of decompressed data + /// + private int outPosition; + + /// + /// Initializates a stream that decompresses ADC data on the fly + /// + /// Stream that contains the compressed data + /// Must be set to because compression is not implemented + public ADCStream(Stream stream, CompressionMode compressionMode = CompressionMode.Decompress) + { + if (compressionMode == CompressionMode.Compress) + throw new NotSupportedException(); + + this.stream = stream; + } + + public override bool CanRead + { + get { return stream.CanRead; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { return position; } + set { throw new NotSupportedException(); } + } + + public override void Flush() + { + } + + protected override void Dispose(bool disposing) + { + if (isDisposed) + return; + isDisposed = true; + base.Dispose(disposing); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count == 0) + return 0; + if (buffer == null) + throw new ArgumentNullException("buffer"); + if (count < 0) + throw new ArgumentOutOfRangeException("count"); + if (offset < buffer.GetLowerBound(0)) + throw new ArgumentOutOfRangeException("offset"); + if ((offset + count) > buffer.GetLength(0)) + throw new ArgumentOutOfRangeException("count"); + + int size = -1; + + if (outBuffer == null) + { + size = ADCBase.Decompress(stream, out outBuffer); + outPosition = 0; + } + + int inPosition = offset; + int toCopy = count; + int copied = 0; + + while (outPosition + toCopy >= outBuffer.Length) + { + int piece = outBuffer.Length - outPosition; + Array.Copy(outBuffer, outPosition, buffer, inPosition, piece); + inPosition += piece; + copied += piece; + position += piece; + toCopy -= piece; + size = ADCBase.Decompress(stream, out outBuffer); + outPosition = 0; + if (size == 0 || outBuffer == null || outBuffer.Length == 0) + return copied; + } + + Array.Copy(outBuffer, outPosition, buffer, inPosition, toCopy); + outPosition += toCopy; + position += toCopy; + copied += toCopy; + return copied; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} diff --git a/test/SharpCompress.Test/ADCTest.cs b/test/SharpCompress.Test/ADCTest.cs new file mode 100644 index 00000000..17ce518f --- /dev/null +++ b/test/SharpCompress.Test/ADCTest.cs @@ -0,0 +1,127 @@ +// +// ADC.cs +// +// Author: +// Natalia Portillo +// +// Copyright (c) 2016 © Claunia.com +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +using SharpCompress.Compressor.ADC; +using System.IO; +using Xunit; + +namespace SharpCompress.Test +{ + public class ADCTest : TestBase + { + [Fact] + public void TestBuffer() + { + using (FileStream decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin"))) + { + byte[] decompressed = new byte[decFs.Length]; + decFs.Read(decompressed, 0, decompressed.Length); + + using (FileStream cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"))) + { + byte[] compressed = new byte[cmpFs.Length]; + cmpFs.Read(compressed, 0, compressed.Length); + byte[] test; + + ADCBase.Decompress(compressed, out test); + + Assert.Equal(decompressed, test); + } + } + } + + [Fact] + public void TestBaseStream() + { + using (FileStream decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin"))) + { + byte[] decompressed = new byte[decFs.Length]; + decFs.Read(decompressed, 0, decompressed.Length); + + using (FileStream cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"))) + { + byte[] test; + + ADCBase.Decompress(cmpFs, out test); + + Assert.Equal(decompressed, test); + } + } + } + + [Fact] + public void TestADCStreamWholeChunk() + { + using (FileStream decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin"))) + { + byte[] decompressed = new byte[decFs.Length]; + decFs.Read(decompressed, 0, decompressed.Length); + + using (FileStream cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"))) + { + using (ADCStream decStream = new ADCStream(cmpFs, Compressor.CompressionMode.Decompress)) + { + byte[] test = new byte[262144]; + + decStream.Read(test, 0, test.Length); + + Assert.Equal(decompressed, test); + } + } + } + } + + [Fact] + public void TestADCStream() + { + using (FileStream decFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_decompressed.bin"))) + { + byte[] decompressed = new byte[decFs.Length]; + decFs.Read(decompressed, 0, decompressed.Length); + + using (FileStream cmpFs = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "adc_compressed.bin"))) + { + using (ADCStream decStream = new ADCStream(cmpFs, Compressor.CompressionMode.Decompress)) + { + using (MemoryStream decMs = new MemoryStream()) + { + byte[] test = new byte[512]; + int count = 0; + + do + { + count = decStream.Read(test, 0, test.Length); + decMs.Write(test, 0, count); + } + while (count > 0); + + Assert.Equal(decompressed, decMs.ToArray()); + } + } + } + } + } + } +} diff --git a/test/TestArchives/Archives/adc_compressed.bin b/test/TestArchives/Archives/adc_compressed.bin new file mode 100644 index 00000000..4cdff175 Binary files /dev/null and b/test/TestArchives/Archives/adc_compressed.bin differ diff --git a/test/TestArchives/Archives/adc_decompressed.bin b/test/TestArchives/Archives/adc_decompressed.bin new file mode 100644 index 00000000..4257cb7f Binary files /dev/null and b/test/TestArchives/Archives/adc_decompressed.bin differ