From 39257fb45a9b91f09e34fb3d905813fb71a33046 Mon Sep 17 00:00:00 2001 From: thiagodesacosta Date: Fri, 15 May 2020 22:56:03 -0300 Subject: [PATCH] Added compression flavor/level selection and improved extration speed New options for compression with addition of flavor/level and store uncompressed. Extration code has been updated. --- HPIZ Archiver/MainForm.Designer.cs | 17 +-- HPIZ Archiver/MainForm.cs | 16 ++- HPIZ/Chunk.cs | 175 ++++++++++++-------------- HPIZ/Compression/CompressionFlavor.cs | 16 +++ HPIZ/Compression/LZ77.cs | 2 +- HPIZ/FileEntry.cs | 12 +- HPIZ/HPIZ.projitems | 1 + HPIZ/HpiArchive.cs | 133 +++++++------------- HPIZ/HpiFile.cs | 156 +++++++++++++---------- screenshot.png | Bin 13382 -> 10992 bytes 10 files changed, 263 insertions(+), 265 deletions(-) create mode 100644 HPIZ/Compression/CompressionFlavor.cs diff --git a/HPIZ Archiver/MainForm.Designer.cs b/HPIZ Archiver/MainForm.Designer.cs index db9ca05..13c9859 100644 --- a/HPIZ Archiver/MainForm.Designer.cs +++ b/HPIZ Archiver/MainForm.Designer.cs @@ -114,13 +114,8 @@ private void InitializeComponent() // compressionLevelComboBox // this.compressionLevelComboBox.AutoToolTip = true; + this.compressionLevelComboBox.CausesValidation = false; this.compressionLevelComboBox.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; - this.compressionLevelComboBox.Items.AddRange(new object[] { - "Zopfli i15", - "Zopfli i10", - "Zopfli i5", - "Zopfli i1", - "ZLib Deflate"}); this.compressionLevelComboBox.Name = "compressionLevelComboBox"; this.compressionLevelComboBox.Size = new System.Drawing.Size(160, 28); this.compressionLevelComboBox.ToolTipText = "Select compression level"; @@ -210,7 +205,7 @@ private void InitializeComponent() // progressBar // this.progressBar.Name = "progressBar"; - this.progressBar.Size = new System.Drawing.Size(120, 18); + this.progressBar.Size = new System.Drawing.Size(100, 18); this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Continuous; this.progressBar.Visible = false; // @@ -254,26 +249,26 @@ private void InitializeComponent() // columnHeaderName // this.columnHeaderName.Text = "Full Name"; - this.columnHeaderName.Width = 336; + this.columnHeaderName.Width = 333; // // columnHeaderSize // this.columnHeaderSize.Text = "Size"; this.columnHeaderSize.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; - this.columnHeaderSize.Width = 89; + this.columnHeaderSize.Width = 90; // // columnCompressed // this.columnCompressed.Text = "Compressed"; this.columnCompressed.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; - this.columnCompressed.Width = 88; + this.columnCompressed.Width = 90; // // columnRatio // this.columnRatio.Tag = ""; this.columnRatio.Text = "Ratio"; this.columnRatio.TextAlign = System.Windows.Forms.HorizontalAlignment.Right; - this.columnRatio.Width = 43; + this.columnRatio.Width = 48; // // dialogOpenFolder // diff --git a/HPIZ Archiver/MainForm.cs b/HPIZ Archiver/MainForm.cs index 5ecd517..2290426 100644 --- a/HPIZ Archiver/MainForm.cs +++ b/HPIZ Archiver/MainForm.cs @@ -18,13 +18,11 @@ public MainForm() } private void MainForm_Load(object sender, EventArgs e) { - // Disable, Not Implemented yet - compressionLevelComboBox.SelectedIndex = 1; - compressionLevelComboBox.Enabled = false; + compressionLevelComboBox.ComboBox.DataSource = Enum.GetValues(typeof(CompressionFlavor)); + compressionLevelComboBox.ComboBox.BindingContext = this.BindingContext; + compressionLevelComboBox.SelectedIndex = 4; } - - private void PopulateList(List collection) { listViewFiles.Items.Clear(); @@ -262,6 +260,10 @@ private async void compressCheckedFilesToolStripMenuItem_Click(object sender, Ev int size = Int32.Parse(item.SubItems[1].Text, NumberStyles.AllowThousands); chunkTotal += (size / 65536) + (size % 65536 == 0 ? 0 : 1); } + + CompressionFlavor flavor; + Enum.TryParse(compressionLevelComboBox.Text, out flavor); + progressBar.Maximum = chunkTotal + 1; progressBar.Value = 0; progressBar.Visible = true; @@ -278,7 +280,7 @@ private async void compressCheckedFilesToolStripMenuItem_Click(object sender, Ev var timer = new Stopwatch(); timer.Start(); - await Task.Run(() => HpiFile.CreateFromFileList(fileList, toolStripPathTextBox.Text, dialogSaveHpi.FileName, progress)); + await Task.Run(() => HpiFile.CreateFromFileList(fileList, toolStripPathTextBox.Text, dialogSaveHpi.FileName, progress, flavor)); timer.Stop(); @@ -286,8 +288,10 @@ private async void compressCheckedFilesToolStripMenuItem_Click(object sender, Ev timer.Elapsed.Seconds, timer.Elapsed.Milliseconds); secondStatusLabel.Text = dialogSaveHpi.FileName; toolStrip.Enabled = true; + toolStripCompressButton.Enabled = false; } } + } } \ No newline at end of file diff --git a/HPIZ/Chunk.cs b/HPIZ/Chunk.cs index 52a26f2..fc19018 100644 --- a/HPIZ/Chunk.cs +++ b/HPIZ/Chunk.cs @@ -1,125 +1,114 @@ using CompressSharper.Zopfli; -using HPIZ.Compression; using System; using System.IO; using System.IO.Compression; -using System.Linq; -using System.Windows.Forms; namespace HPIZ { - public class Chunk + internal static class Chunk { private const int Header = 0x48535153; //SQSH (SQUASH) private const byte DefaultVersion = 2; //Always 2? - public const int SizeOfChunk = 19; - - public CompressionMethod FlagCompression; - public bool IsObfuscated; - public int CompressedSize; // the length of the compressed data - public int DecompressedSize; // the length of the decompressed data - public byte[] Data; - public Chunk(byte[] chunkData) - { - BinaryReader hr = new BinaryReader(new MemoryStream(chunkData)); - int headerMark = hr.ReadInt32(); - if (headerMark != Chunk.Header) throw new InvalidDataException("Invalid Chunk Header"); - - int version = hr.ReadByte(); - if (version != DefaultVersion) throw new NotImplementedException("Unsuported Chunk Version"); + public const int MinSize = 19; + public const int MaxSize = 65536; + private const byte NoObfuscation = 0; - FlagCompression = (CompressionMethod)hr.ReadByte(); - if (FlagCompression != CompressionMethod.LZ77 && FlagCompression != CompressionMethod.ZLib) - throw new Exception("Unknown compression method in Chunk header"); + public static int ObtainDecompressedSize(byte[] chunk) + { + return BitConverter.ToInt32(chunk, 11); + } - IsObfuscated = hr.ReadBoolean(); - CompressedSize = hr.ReadInt32(); - DecompressedSize = hr.ReadInt32(); + internal static byte[] Compress(byte[] bytesToCompress, CompressionFlavor flavor) + { + if (bytesToCompress == null) + throw new InvalidDataException("Cannot compress null array"); - if (FlagCompression == CompressionMethod.None && CompressedSize != DecompressedSize) - throw new Exception("Chunk size inconsistent with decompressed and compressed sizes"); + if (flavor == CompressionFlavor.StoreUncompressed) + throw new InvalidOperationException("Chunk format cannot be used for uncompressed data"); - int checksum = hr.ReadInt32(); + MemoryStream output = new MemoryStream(bytesToCompress.Length); + BinaryWriter writer = new BinaryWriter(output); - Data = new byte[CompressedSize]; - hr.Read(Data, 0, CompressedSize); + writer.Write(Chunk.Header); + writer.Write(Chunk.DefaultVersion); + writer.Write((byte)CompressionMethod.ZLib); + writer.Write(NoObfuscation); + using (MemoryStream compressedStream = new MemoryStream(bytesToCompress.Length)) + { + compressedStream.WriteByte(0x78); //ZLib header first byte + compressedStream.WriteByte(0xDA); //ZLib header second byte - if (ComputeChecksum() != checksum) throw new InvalidDataException("Bad Chunk Checksum"); + switch (flavor) + { + case CompressionFlavor.ZLibDeflate: + using (DeflateStream deflateStream = new DeflateStream(compressedStream, CompressionMode.Compress, true)) + deflateStream.Write(bytesToCompress, 0, bytesToCompress.Length); + break; + case CompressionFlavor.i5ZopfliDeflate: + case CompressionFlavor.i10ZopfliDeflate: + case CompressionFlavor.i15ZopfliDeflate: + ZopfliDeflater zstream = new ZopfliDeflater(compressedStream); + zstream.NumberOfIterations = (int)flavor; + zstream.MasterBlockSize = 0; + zstream.Deflate(bytesToCompress, true); + break; + default: + throw new InvalidOperationException("Unknow compression flavor"); + } + var compressedDataArray = compressedStream.ToArray(); //Change to stream + int checksum = ComputeChecksum(compressedDataArray); //Change to stream - if (IsObfuscated) - for (int j = 0; j < CompressedSize; ++j) - Data[j] = (byte)((Data[j] - j) ^ j); + writer.Write(compressedDataArray.Length); + writer.Write(bytesToCompress.Length); + writer.Write(checksum); + writer.Write(compressedDataArray); + } + return output.ToArray(); } - public Chunk(byte[] data, bool toREMOVE) + internal static byte[] Decompress(MemoryStream bytesToDecompress) { - Data = data; - FlagCompression = CompressionMethod.None; - IsObfuscated = false; - CompressedSize = data.Length; - DecompressedSize = data.Length; - } + BinaryReader reader = new BinaryReader(bytesToDecompress); + int headerMark = reader.ReadInt32(); + if (headerMark != Chunk.Header) throw new InvalidDataException("Invalid Chunk Header"); - private int ComputeChecksum() - { - int sum = 0; - for (int i = 0; i < Data.Length; ++i) - sum += Data[i]; - return sum; - } + int version = reader.ReadByte(); + if (version != DefaultVersion) throw new NotImplementedException("Unsuported Chunk Version"); - public void WriteBytes(BinaryWriter writer) - { - writer.Write(Chunk.Header); - writer.Write(Chunk.DefaultVersion); - writer.Write((byte)FlagCompression); - writer.Write(IsObfuscated); - writer.Write(CompressedSize); - writer.Write(DecompressedSize); - writer.Write(ComputeChecksum()); - writer.Write(Data); - } + CompressionMethod FlagCompression = (CompressionMethod)reader.ReadByte(); + if (FlagCompression != CompressionMethod.LZ77 && FlagCompression != CompressionMethod.ZLib) + throw new InvalidOperationException("Unknown compression method in Chunk header"); - public void Compress(bool useZopfli) - { + bool IsObfuscated = reader.ReadBoolean(); + int CompressedSize = reader.ReadInt32(); + int DecompressedSize = reader.ReadInt32(); + int checksum = reader.ReadInt32(); - using (MemoryStream ms = new MemoryStream(Data.Length)) - { - ms.WriteByte(0x78); //ZLib header first byte - ms.WriteByte(0xDA); //ZLib header second byte - if (useZopfli) - { - ZopfliDeflater zstream = new ZopfliDeflater(ms); + byte[] compressedData = reader.ReadBytes(CompressedSize); - zstream.NumberOfIterations = 10; - zstream.MasterBlockSize = 0; - zstream.Deflate(Data, true); - } - else - using (DeflateStream deflateStream = new DeflateStream(ms, CompressionMode.Compress, true)) - deflateStream.Write(Data, 0, Data.Length); - - Data = ms.ToArray(); - CompressedSize = Data.Length; - FlagCompression = CompressionMethod.ZLib; - } - } + if (ComputeChecksum(compressedData) != checksum) throw new InvalidDataException("Bad Chunk Checksum"); - public void Decompress() - { - var outputBuffer = new byte[DecompressedSize]; + if (IsObfuscated) + for (int j = 0; j < CompressedSize; ++j) + compressedData[j] = (byte)((compressedData[j] - j) ^ j); + + byte[] outputBuffer = new byte[DecompressedSize]; if (FlagCompression == CompressionMethod.LZ77) - { - LZ77.Decompress(Data, outputBuffer); - Data = outputBuffer; - } + LZ77.Decompress(compressedData, outputBuffer); + if (FlagCompression == CompressionMethod.ZLib) - { - ZLibDeflater.Decompress(Data, outputBuffer); - Data = outputBuffer; - } - FlagCompression = CompressionMethod.None; + ZLibDeflater.Decompress(compressedData, outputBuffer); + + return outputBuffer; + } + + private static int ComputeChecksum(byte[] data) + { + int sum = 0; + for (int i = 0; i < data.Length; ++i) + sum += data[i]; + return sum; } } diff --git a/HPIZ/Compression/CompressionFlavor.cs b/HPIZ/Compression/CompressionFlavor.cs new file mode 100644 index 0000000..9273b54 --- /dev/null +++ b/HPIZ/Compression/CompressionFlavor.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace HPIZ +{ + public enum CompressionFlavor + { + StoreUncompressed = 0, + ZLibDeflate = 1, + i5ZopfliDeflate = 5, + i10ZopfliDeflate = 10, + i15ZopfliDeflate = 15 + + } +} diff --git a/HPIZ/Compression/LZ77.cs b/HPIZ/Compression/LZ77.cs index 58e3493..57ee72e 100644 --- a/HPIZ/Compression/LZ77.cs +++ b/HPIZ/Compression/LZ77.cs @@ -3,7 +3,7 @@ using System.IO; using System.Text; -namespace HPIZ.Compression +namespace HPIZ { public static class LZ77 { diff --git a/HPIZ/FileEntry.cs b/HPIZ/FileEntry.cs index 31cb417..747880e 100644 --- a/HPIZ/FileEntry.cs +++ b/HPIZ/FileEntry.cs @@ -10,7 +10,6 @@ public class FileEntry public CompressionMethod FlagCompression; public int[] ChunkSizes; - public FileEntry(BinaryReader reader) { OffsetOfCompressedData = reader.ReadInt32(); @@ -22,10 +21,19 @@ public FileEntry() { } + public FileEntry(int uncompressedSize, CompressionMethod flagCompression, int[] chunkSizes) + { + UncompressedSize = uncompressedSize; + FlagCompression = flagCompression; + ChunkSizes = chunkSizes; + } public int CompressedSizeCount() { - return ChunkSizes.Sum(); + if (FlagCompression == CompressionMethod.None) + return UncompressedSize; + else + return ChunkSizes.Sum() + ChunkSizes.Length * 4 + Chunk.MinSize; } public float Ratio() diff --git a/HPIZ/HPIZ.projitems b/HPIZ/HPIZ.projitems index 64a8497..2e244a6 100644 --- a/HPIZ/HPIZ.projitems +++ b/HPIZ/HPIZ.projitems @@ -10,6 +10,7 @@ + diff --git a/HPIZ/HpiArchive.cs b/HPIZ/HpiArchive.cs index 8c7bbf6..db7ea40 100644 --- a/HPIZ/HpiArchive.cs +++ b/HPIZ/HpiArchive.cs @@ -10,9 +10,9 @@ namespace HPIZ { public class HpiArchive : IDisposable { - private const int HeaderMarker = 0x49504148; //HAPI Header - private const int DefaultVersion = 0x00010000; - private const int NoObfuscationKey = 0; + internal const int HeaderMarker = 0x49504148; //HAPI Header + internal const int DefaultVersion = 0x00010000; + internal const int NoObfuscationKey = 0; internal readonly Stream archiveStream; private readonly int obfuscationKey; @@ -66,19 +66,25 @@ public HpiArchive(Stream stream) archiveReader = new BinaryReader(archiveStream); + foreach (var entry in entriesDictionary.Keys) { - int chunkCount = entriesDictionary[entry].CalculateChunkQuantity(); - archiveStream.Position = entriesDictionary[entry].OffsetOfCompressedData; - var buffer = archiveReader.ReadBytes(chunkCount * 4); - - if (obfuscationKey != 0) - Clarify(buffer, entriesDictionary[entry].OffsetOfCompressedData); - - var size = new int[chunkCount]; - Buffer.BlockCopy(buffer, 0, size, 0, buffer.Length); + if (entriesDictionary[entry].FlagCompression != CompressionMethod.None) + { + int chunkCount = entriesDictionary[entry].CalculateChunkQuantity(); + archiveStream.Position = entriesDictionary[entry].OffsetOfCompressedData; + var buffer = archiveReader.ReadBytes(chunkCount * 4); + + if (obfuscationKey != 0) + Clarify(buffer, entriesDictionary[entry].OffsetOfCompressedData); - entriesDictionary[entry].ChunkSizes = size; + var size = new int[chunkCount]; + Buffer.BlockCopy(buffer, 0, size, 0, buffer.Length); + + entriesDictionary[entry].ChunkSizes = size; + } + else + entriesDictionary[entry].ChunkSizes = new int[] { entriesDictionary[entry].UncompressedSize}; //To optimize } } @@ -104,7 +110,7 @@ private void GetEntries(int NumberOfEntries, int EntryListOffset, BinaryReader r } } - private static int GetDirectorySize(DirectoryTree tree) + internal static int GetDirectorySize(DirectoryTree tree) { int totalSize = 8; @@ -120,7 +126,7 @@ private static int GetDirectorySize(DirectoryTree tree) return totalSize; } - private static void SetEntries(DirectoryTree tree, BinaryWriter bw, Queue sequence, int directorySize) + internal static void SetEntries(DirectoryTree tree, BinaryWriter bw, Queue sequence, int directorySize) { bw.Write(tree.Count); //Root Entries number in directory @@ -155,39 +161,7 @@ private static void SetEntries(DirectoryTree tree, BinaryWriter bw, Queue> allFiles) - { - var obsCollection = new DirectoryTree(); - foreach (var item in allFiles.Keys) - obsCollection.AddEntry(item); - - Queue sequence; - - Stream serial = HpiFile.SerializeChunks(allFiles, out sequence); - - BinaryWriter bw = new BinaryWriter(new MemoryStream()); - - bw.Write(HeaderMarker); - bw.Write(DefaultVersion); - - int directorySize = GetDirectorySize(obsCollection) + 20; - bw.Write(directorySize); - - bw.Write(NoObfuscationKey); - - int directoryStart = 20; - bw.Write(directoryStart); //Directory Start Pos20 point to next - - SetEntries(obsCollection, bw, sequence, directorySize); - - serial.Position = 0; - bw.BaseStream.Position = bw.BaseStream.Length; - serial.CopyTo(bw.BaseStream); - - bw.Write("Copyright " + DateTime.Now.Year.ToString() + " Cavedog Entertainment"); //Endfile mandatory string - - return bw.BaseStream; - } + private static string ReadStringCP437NullTerminated(BinaryReader reader) @@ -200,7 +174,7 @@ private static string ReadStringCP437NullTerminated(BinaryReader reader) bytes.Enqueue(b); b = reader.ReadByte(); } - var asciiCharList = codePage437.GetChars(bytes.ToArray()); + var characters = codePage437.GetChars(bytes.ToArray()); char[] invalids = { '\"', '<', '>', '|', '\0', ':', '*', '?', '\\', '/' , (Char)1, (Char)2, (Char)3, (Char)4, (Char)5, (Char)6, (Char)7, (Char)8, @@ -208,16 +182,15 @@ private static string ReadStringCP437NullTerminated(BinaryReader reader) (Char)17, (Char)18, (Char)19, (Char)20, (Char)21,(Char)22, (Char)23, (Char)24, (Char)25, (Char)26, (Char)27, (Char)28, (Char)29, (Char)30, (Char)31 }; - for (int i = 0; i < asciiCharList.Length; i++) + for (int i = 0; i < characters.Length; i++) { - if (invalids.Contains((asciiCharList[i]))) - asciiCharList[i] = '_'; //Replace invalid char with underscore + if (invalids.Contains((characters[i]))) + characters[i] = '_'; //Replace invalid char with underscore } - return new string(asciiCharList); + return new string(characters); } - private static void WriteStringCP437NullTerminated(BinaryWriter reader, string text) { Encoding codePage437 = Encoding.GetEncoding(437); @@ -225,9 +198,11 @@ private static void WriteStringCP437NullTerminated(BinaryWriter reader, string t reader.Write(byte.MinValue); //Zero byte to end string } - void Clarify(byte[] obfuscatedBytes, int position) + void Clarify(byte[] obfuscatedBytes, int position, int start = 0, int end = 0) { - for (int i = 0; i < obfuscatedBytes.Length; ++i) + if (end == 0) end = obfuscatedBytes.Length; + else end = end + start; + for (int i = start; i < end; ++i) { obfuscatedBytes[i] = (byte) ~(position ^ obfuscationKey ^ obfuscatedBytes[i]); position++; @@ -236,56 +211,44 @@ void Clarify(byte[] obfuscatedBytes, int position) public byte[] Extract(FileEntry file) { + BinaryReader reader = new BinaryReader(archiveStream); if (file.FlagCompression == CompressionMethod.None) { reader.BaseStream.Position = file.OffsetOfCompressedData; - var buffer = reader.ReadBytes(file.UncompressedSize); + var uncompressedOutput = reader.ReadBytes(file.UncompressedSize); if (obfuscationKey != 0) - Clarify(buffer, file.OffsetOfCompressedData); + Clarify(uncompressedOutput, file.OffsetOfCompressedData); - return buffer; + return uncompressedOutput; } if(file.FlagCompression != CompressionMethod.LZ77 && file.FlagCompression != CompressionMethod.ZLib) throw new Exception("Unknown compression method in file entry"); var chunkCount = file.ChunkSizes.Length; - reader.BaseStream.Position = file.OffsetOfCompressedData + (chunkCount * 4); - - //Set chunks array sizes and split stream to chunks and save stream positions - var chunkBuffer = new List(chunkCount); - var positions = new Queue(chunkCount); - for (int i = 0; i < chunkCount; i++) - { - positions.Enqueue( (int) reader.BaseStream.Position); - chunkBuffer.Add(new byte[file.ChunkSizes[i]]); - reader.Read(chunkBuffer[i], 0, chunkBuffer[i].Length); - } + var readPositions = new int[chunkCount]; + for (int i = 1; i < chunkCount; i++) + readPositions[i] = readPositions[i - 1] + file.ChunkSizes[i - 1]; + + int strReadPositions = file.OffsetOfCompressedData + (chunkCount * 4); + reader.BaseStream.Position = strReadPositions; + var buffer = reader.ReadBytes(file.ChunkSizes.Sum()); - var outputChunks = new Chunk[chunkCount]; + var outBytes = new byte[file.UncompressedSize]; + // Parallelize chunk decompression Parallel.For(0, chunkCount, i => { if (obfuscationKey != 0) - Clarify(chunkBuffer[i], positions.Dequeue()); - - outputChunks[i] = new Chunk(chunkBuffer[i]); - outputChunks[i].Decompress(); - }); + Clarify(buffer, readPositions[i] + strReadPositions, readPositions[i], file.ChunkSizes[i]); - var outBytes = new byte[file.UncompressedSize]; - int copyPosition = 0; - for (int i = 0; i < chunkCount; i++) - { - Array.Copy(outputChunks[i].Data, 0, outBytes, copyPosition, outputChunks[i].Data.Length); - copyPosition += outputChunks[i].Data.Length; - } + var decompressedChunk = Chunk.Decompress(new MemoryStream(buffer, readPositions[i], file.ChunkSizes[i])); - if(file.UncompressedSize != copyPosition) - throw new Exception("Bad output size"); + Buffer.BlockCopy(decompressedChunk, 0, outBytes, i * Chunk.MaxSize, decompressedChunk.Length); + }); // Parallel.For return outBytes; } diff --git a/HPIZ/HpiFile.cs b/HPIZ/HpiFile.cs index 145db61..69f3175 100644 --- a/HPIZ/HpiFile.cs +++ b/HPIZ/HpiFile.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; +using System.Drawing; using System.IO; +using System.Linq; using System.Threading.Tasks; +using System.Windows.Forms; namespace HPIZ { @@ -19,11 +23,11 @@ public static HpiArchive Create(string archiveFileName) throw new System.NotImplementedException(); } - public static void CreateFromDirectory(string sourceDirectoryFullName, string destinationArchiveFileName) + public static void CreateFromDirectory(string sourceDirectoryFullName, string destinationArchiveFileName, CompressionFlavor flavor) { destinationArchiveFileName = Path.GetFullPath(destinationArchiveFileName); var fileList = GetDirectoryFileList(sourceDirectoryFullName); - CreateFromFileList(fileList, sourceDirectoryFullName, destinationArchiveFileName, null); + CreateFromFileList(fileList, sourceDirectoryFullName, destinationArchiveFileName, null, flavor); } public static SortedSet GetDirectoryFileList(string sourceDirectoryFullName) @@ -42,90 +46,108 @@ public static SortedSet GetDirectoryFileList(string sourceDirectoryFullN return fileList; } - - public static void CreateFromFileList(SortedSet fileList, string sourceDirectoryFullName, string destinationArchiveFileName, IProgress progress) + public static void CreateFromFileList(SortedSet fileList, string sourceDirectoryFullName, string destinationArchiveFileName, IProgress progress, CompressionFlavor flavor) { - var allFiles = FilesToChunks(fileList, sourceDirectoryFullName); + int totalFiles = fileList.Count; + var fileNameArray = fileList.ToArray(); + var entrys = new FileEntry[totalFiles]; + var chunkBuffer = new byte[totalFiles][][]; - Parallel.ForEach(fileList, file => - { - for (int index = 0; index < allFiles[file].Count; index++) - { - allFiles[file][index].Compress(true); - if (progress != null) - progress.Report(file + ":Chunk#" + index.ToString()); - } + for (int i = 0; i < totalFiles; i++) + { - }); + string fullName = Path.Combine(sourceDirectoryFullName, fileNameArray[i]); + var fileSize = (int)new FileInfo(fullName).Length; + if (fileSize > Int32.MaxValue) + throw new Exception("File is too large: " + fileNameArray[i] + "Maximum allowed size is 2GB (2 147 483 647 bytes)."); + byte[] buffer = File.ReadAllBytes(fullName); + if (flavor != CompressionFlavor.StoreUncompressed && fileSize > 128) //Skip compression of small files + { + int chunkCount = (buffer.Length / Chunk.MaxSize) + (buffer.Length % Chunk.MaxSize == 0 ? 0 : 1); + chunkBuffer[i] = new byte[chunkCount][]; + var chunkSizes = new int[chunkCount]; - using (var fileStream = File.Create(destinationArchiveFileName)) - { - var hpifile = HpiArchive.Encode(allFiles); - hpifile.Position = 0; - hpifile.CopyTo(fileStream); - } + // Parallelize chunk compression + Parallel.For(0, chunkCount, j => + { + int size = Chunk.MaxSize; + if (j + 1 == chunkCount && buffer.Length != Chunk.MaxSize) size = buffer.Length % Chunk.MaxSize; //Last loop - } - public static Stream SerializeChunks(SortedDictionary> chunks, out Queue sequence) - { - var bw = new BinaryWriter(new MemoryStream()); - sequence = new Queue(chunks.Count); - foreach (var file in chunks) - { - - int totalUncompressedSize = 0; - int position = (int) bw.BaseStream.Position; - foreach (var chunk in file.Value) - bw.Write(chunk.Data.Length + Chunk.SizeOfChunk); + chunkBuffer[i][j] = Chunk.Compress(new MemoryStream(buffer, j * Chunk.MaxSize, size).ToArray(), flavor); + + chunkSizes[j] = chunkBuffer[i][j].Length; + + if (progress != null) + progress.Report(fileNameArray[i] + ":Chunk#" + j.ToString()); - foreach (var chunk in file.Value) + }); // Parallel.For + + entrys[i] = new FileEntry(fileSize, CompressionMethod.ZLib, chunkSizes); + } + else { - chunk.WriteBytes(bw); - totalUncompressedSize += chunk.DecompressedSize; + entrys[i] = new FileEntry(fileSize, CompressionMethod.None, null); + chunkBuffer[i] = new byte[1][]; + chunkBuffer[i][0] = buffer; + if (progress != null) + progress.Report(fileNameArray[i]); } - FileEntry fd = new FileEntry(); - fd.UncompressedSize = totalUncompressedSize; - fd.OffsetOfCompressedData = position; - fd.FlagCompression = CompressionMethod.ZLib; - sequence.Enqueue(fd); + } + + var serialWriter = new BinaryWriter(new MemoryStream()); + var sequence = new Queue(totalFiles); + for (int i = 0; i < totalFiles; i++) + { + entrys[i].OffsetOfCompressedData = (int) serialWriter.BaseStream.Position; + sequence.Enqueue(entrys[i]); + if(entrys[i].FlagCompression != CompressionMethod.None) + foreach (var size in entrys[i].ChunkSizes) + serialWriter.Write(size); + + foreach (var chunk in chunkBuffer[i]) + serialWriter.Write(chunk); } - return bw.BaseStream; - } - private static SortedDictionary> FilesToChunks(SortedSet fileList, string sourceDirectoryFullName) - { - var output = new SortedDictionary>(); - int chunkSize = 65536; + + var tree = new DirectoryTree(); + foreach (var item in fileNameArray) + tree.AddEntry(item); - foreach (var file in fileList) - { - string fullName = Path.Combine(sourceDirectoryFullName, file); - using (FileStream fs = new FileStream(fullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, chunkSize, FileOptions.SequentialScan)) - { - if (fs.Length > Int32.MaxValue) throw new Exception("File is too large." + file + "Maximum total size is 2GB (2 147 483 647 bytes)."); - int dataLenght = (int) fs.Length; - int chunkCount = (dataLenght / chunkSize) + (dataLenght % chunkSize == 0 ? 0 : 1); - int actualChunkSize = chunkSize; - var listChunk = new List(chunkCount); + + BinaryWriter bw = new BinaryWriter(new MemoryStream()); - for (int i = 0; i < chunkCount; i++) - { - if (chunkSize > dataLenght) actualChunkSize = dataLenght; - byte[] buffer = new byte[actualChunkSize]; - dataLenght -= fs.Read(buffer, 0, actualChunkSize); - listChunk.Add(new Chunk(buffer, true)); - } + bw.Write(HPIZ.HpiArchive.HeaderMarker); + bw.Write(HpiArchive.DefaultVersion); - output.Add(file, listChunk); - } + int directorySize = HpiArchive.GetDirectorySize(tree) + 20; + bw.Write(directorySize); + + bw.Write(HpiArchive.NoObfuscationKey); + + int directoryStart = 20; + bw.Write(directoryStart); //Directory Start at Pos20, always start it next + + HpiArchive.SetEntries(tree, bw, sequence, directorySize); + + serialWriter.BaseStream.Position = 0; + bw.BaseStream.Position = bw.BaseStream.Length; + serialWriter.BaseStream.CopyTo(bw.BaseStream); + + bw.Write("Copyright " + DateTime.Now.Year.ToString() + " Cavedog Entertainment"); //Endfile mandatory string + + + + using (var fileStream = File.Create(destinationArchiveFileName)) + { + bw.BaseStream.Position = 0; + bw.BaseStream.CopyTo(fileStream); } - return output; - } + } public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName) { diff --git a/screenshot.png b/screenshot.png index fc61801a50148b0abe315a649b8528877c6329dd..882f0e7036acdf8f609d492f3733cfcf579ff621 100644 GIT binary patch literal 10992 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 BQ8NGl literal 13382 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 U?;(B0AmH;z=_#^E;d$`?0e$67NB{r;