diff --git a/BizHawk.Client.Common/BizHawk.Client.Common.csproj b/BizHawk.Client.Common/BizHawk.Client.Common.csproj index ef15df436a..b350e94fe5 100644 --- a/BizHawk.Client.Common/BizHawk.Client.Common.csproj +++ b/BizHawk.Client.Common/BizHawk.Client.Common.csproj @@ -157,6 +157,7 @@ + diff --git a/BizHawk.Client.Common/FrameworkFastZipWriter.cs b/BizHawk.Client.Common/FrameworkFastZipWriter.cs new file mode 100644 index 0000000000..8612b66b44 --- /dev/null +++ b/BizHawk.Client.Common/FrameworkFastZipWriter.cs @@ -0,0 +1,296 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BizHawk.Client.Common +{ + /// + /// this almost works, but it loses all of its speed advantages over FrameworkZipWriter from slow CRC calculation. + /// + public class FrameworkFastZipWriter : IZipWriter + { + private Stream _output; + private readonly CompressionLevel _level; + + private byte[] _localHeader; + private List _endBlobs = new List(); + private byte[] _fileHeaderTemplate; + private int _numEntries; + private bool _disposed; + + private class CRC32Stream : Stream + { + // Lookup table for speed. + private static readonly uint[] Crc32Table; + + static CRC32Stream() + { + Crc32Table = new uint[256]; + for (uint i = 0; i < 256; ++i) + { + uint crc = i; + for (int j = 8; j > 0; --j) + { + if ((crc & 1) == 1) + { + crc = (crc >> 1) ^ 0xEDB88320; + } + else + { + crc >>= 1; + } + } + + Crc32Table[i] = crc; + } + } + + private uint _crc = 0xffffffff; + private int _count = 0; + private Stream _baseStream; + + public int Size => _count; + public uint Crc => ~_crc; + + public CRC32Stream(Stream baseStream) + { + _baseStream = baseStream; + } + + private void CalculateByte(byte b) + { + _crc = (_crc >> 8) ^ Crc32Table[b ^ (_crc & 0xff)]; + } + + public override void Write(byte[] buffer, int offset, int count) + { + for (int i = offset; i < offset + count; i++) + { + CalculateByte(buffer[i]); + } + _count += count; + _baseStream.Write(buffer, offset, count); + } + + public override void WriteByte(byte value) + { + CalculateByte(value); + _count++; + _baseStream.WriteByte(value); + } + + public override void Flush() + { + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => true; + + public override long Length + { + get + { + throw new NotImplementedException(); + } + } + + public override long Position + { + get + { + throw new NotImplementedException(); + } + + set + { + throw new NotImplementedException(); + } + } + } + + public FrameworkFastZipWriter(string path, int compressionlevel) + { + _output = new FileStream(path, FileMode.Create, FileAccess.Write); + if (compressionlevel == 0) + throw new NotImplementedException(); + //_level = CompressionLevel.NoCompression; + else if (compressionlevel < 5) + _level = CompressionLevel.Fastest; + else + _level = CompressionLevel.Optimal; + + var dt = DateTime.Now; + var mtime = dt.Second >> 1 + | dt.Minute << 5 + | dt.Hour << 11; + var mdate = dt.Day + | dt.Month << 5 + | (dt.Year - 1980) << 9; + + var modifiedDate = new byte[] + { + (byte)(mtime & 0xff), + (byte)(mtime >> 8), + (byte)(mdate & 0xff), + (byte)(mdate >> 8) + }; + + _localHeader = new byte[] + { + 0x50, 0x4b, 0x03, 0x04, // signature + 0x14, 0x00, // version + 0x08, 0x00, // flags: has data descriptor + 0x08, 0x00, // method: deflate + modifiedDate[0], modifiedDate[1], // mod time + modifiedDate[2], modifiedDate[3], // mod date + 0x00, 0x00, 0x00, 0x00, // crc32 + 0x00, 0x00, 0x00, 0x00, // compressed size + 0x00, 0x00, 0x00, 0x00, // uncompressed size + 0x00, 0x00, // filename length + 0x00, 0x00, // extra field length + }; + + _fileHeaderTemplate = new byte[] + { + 0x50, 0x4b, 0x01, 0x02, // signature + 0x17, 0x03, // ?? + 0x14, 0x00, // version + 0x08, 0x00, // flags: has data descriptor + 0x08, 0x00, // method: deflate + modifiedDate[0], modifiedDate[1], // mod time + modifiedDate[2], modifiedDate[3], // mod date + 0x00, 0x00, 0x00, 0x00, // crc32 + 0x00, 0x00, 0x00, 0x00, // compressed size + 0x00, 0x00, 0x00, 0x00, // uncompressed size + 0x00, 0x00, // filename length + 0x00, 0x00, // extra field length + 0x00, 0x00, // file comment length + 0x00, 0x00, // disk #, + 0x00, 0x00, // internal attributes + 0x00, 0x00, 0x00, 0x00, // external attributes + 0x00, 0x00, 0x00, 0x00, // local header offset + }; + } + + public void Dispose() + { + if (!_disposed) + { + WriteFooter(); + _output.Dispose(); + _output = null; + _disposed = true; + } + } + + public void WriteItem(string name, Action callback) + { + var nameb = Encoding.ASCII.GetBytes(name); + _localHeader[26] = (byte)nameb.Length; + _localHeader[27] = (byte)(nameb.Length >> 8); + + var localHeaderOffset = (int)(_output.Position); + + _output.Write(_localHeader, 0, _localHeader.Length); + _output.Write(nameb, 0, nameb.Length); + + var fileStart = (int)(_output.Position); + + var s2 = new DeflateStream(_output, _level, true); + var s3 = new CRC32Stream(s2); + callback(s3); + s2.Flush(); + + var fileEnd = (int)(_output.Position); + + var crc = s3.Crc; + var compressedSize = fileEnd - fileStart; + var uncompressedSize = s3.Size; + var descriptor = new byte[] + { + (byte)crc, + (byte)(crc >> 8), + (byte)(crc >> 16), + (byte)(crc >> 24), + (byte)compressedSize, + (byte)(compressedSize >> 8), + (byte)(compressedSize >> 16), + (byte)(compressedSize >> 24), + (byte)uncompressedSize, + (byte)(uncompressedSize >> 8), + (byte)(uncompressedSize >> 16), + (byte)(uncompressedSize >> 24) + }; + _output.Write(descriptor, 0, descriptor.Length); + + var fileHeader = (byte[])_fileHeaderTemplate.Clone(); + + fileHeader[28] = (byte)nameb.Length; + fileHeader[29] = (byte)(nameb.Length >> 8); + Array.Copy(descriptor, 0, fileHeader, 16, 12); + fileHeader[42] = (byte)localHeaderOffset; + fileHeader[43] = (byte)(localHeaderOffset >> 8); + fileHeader[44] = (byte)(localHeaderOffset >> 16); + fileHeader[45] = (byte)(localHeaderOffset >> 24); + + _endBlobs.Add(fileHeader); + _endBlobs.Add(nameb); + _numEntries++; + } + + private void WriteFooter() + { + var centralHeaderOffset = (int)(_output.Position); + + foreach (var blob in _endBlobs) + _output.Write(blob, 0, blob.Length); + + var centralHeaderEnd = (int)(_output.Position); + + var centralHeaderSize = centralHeaderEnd - centralHeaderOffset; + + var footer = new byte[] + { + 0x50, 0x4b, 0x05, 0x06, // signature + 0x00, 0x00, // disk number + 0x00, 0x00, // central record disk number + (byte)_numEntries, (byte)(_numEntries >> 8), // number of entries on disk + (byte)_numEntries, (byte)(_numEntries >> 8), // number of entries total + (byte)centralHeaderSize, + (byte)(centralHeaderSize >> 8), + (byte)(centralHeaderSize >> 16), + (byte)(centralHeaderSize >> 24), // central directory size + (byte)centralHeaderOffset, + (byte)(centralHeaderOffset >> 8), + (byte)(centralHeaderOffset >> 16), + (byte)(centralHeaderOffset >> 24), // central directory offset + 0x07, 0x00, // comment length + 0x42, 0x69, 0x7a, 0x48, 0x61, 0x77, 0x6b // comment + }; + + _output.Write(footer, 0, footer.Length); + } + } +}