//! \file ArcDX8.cs //! \date 2022 Jun 05 //! \brief DxLib archive version 8. // // Copyright (C) 2022 by morkt // // 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 GameRes.Formats.Strings; using System; using System.CodeDom; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; using System.Runtime.Remoting.Messaging; using System.Windows.Navigation; using static GameRes.Formats.DxLib.Dx8Opener; namespace GameRes.Formats.DxLib { internal class DXA8PackedEntry : PackedEntry { public bool HuffmanCompressed { get; set; } public uint HuffmanSize { get; set; } public uint LZSize { get; set; } } internal class DxArchive8 : DxArchive { public byte huffmanMaxKB; public DxArchive8(ArcView arc, ArchiveFormat impl, ICollection dir, IDxKey enc, int version,byte huffmanKB) : base(arc, impl, dir, enc, version) { huffmanMaxKB = huffmanKB; } } internal class DxHeaderV8 : DxHeader { public DXA8Flags Flags; public byte HuffmanKB; // oddly used only in Compression process not in decompression. //15 bytes of padding. } [Export(typeof(ArchiveFormat))] public class Dx8Opener : DxOpener { public override string Tag { get { return "BIN/DXLIB"; } } public override string Description { get { return "DxLib archive version 8"; } } public override uint Signature { get { return 0x00085844; } } public override bool IsHierarchic { get { return true; } } public override bool CanWrite { get { return false; } } public Dx8Opener () { Extensions = new string[] { "dxa", "hud", "usi", "med", "dat", "bin", "bcx", "wolf" }; Signatures = new[] { 0x00085844u }; } //static readonly byte[] DefaultKey = new byte[] { 0xBE, 0xC8, 0x8A, 0xF5, 0x28, 0x50, 0xC9 }; DxScheme DefaultScheme = new DxScheme { KnownKeys = new List() }; internal enum DXA8Flags : UInt32 { DXA_FLAG_NO_KEY=1, //file is not encrypted DXA_FLAG_NO_HEAD_PRESS=1<<1, //do not compress the header after compressing individual entries } [Serializable] public class DXAOpts : ResourceOptions { public string Keyword; } public override ResourceOptions GetDefaultOptions() { return new DXAOpts { Keyword = Properties.Settings.Default.DXAPassword }; } public override ResourceOptions GetOptions(object widget) { if (widget is GUI.WidgetDXA) { return new DXAOpts { Keyword = ((GUI.WidgetDXA)widget).Password.Text }; } return GetDefaultOptions(); } public override object GetAccessWidget() { return new GUI.WidgetDXA(); } public override ArcFile TryOpen (ArcView file) { var dx = new DxHeaderV8 { IndexSize = file.View.ReadUInt32 (4), BaseOffset = file.View.ReadInt64 (8), IndexOffset = file.View.ReadInt64 (0x10), FileTable = file.View.ReadInt64 (0x18), DirTable = file.View.ReadInt64 (0x20), CodePage = file.View.ReadInt32 (0x28), Flags = (DXA8Flags)file.View.ReadUInt32(0x2C), HuffmanKB = file.View.ReadByte(0x30) }; if (dx.DirTable >= dx.IndexSize || dx.FileTable >= dx.IndexSize) return null; DxKey8 key = null; //FIXME: ReadBytes sets hard cap of filesize to 4GB. var headerBuffer = file.View.ReadBytes(dx.IndexOffset, (uint)(file.MaxOffset-dx.IndexOffset)); bool isencrypted = (dx.Flags & DXA8Flags.DXA_FLAG_NO_KEY) == 0; if (isencrypted) { var keyStr = Query(arcStrings.ZIPEncryptedNotice).Keyword; key = new DxKey8(keyStr); } Decrypt(headerBuffer, 0, headerBuffer.Length, 0, key.Key); //Decrypted but might be compressed if ((dx.Flags & DXA8Flags.DXA_FLAG_NO_HEAD_PRESS) == 0) { byte[] huffmanBuffer = new byte[headerBuffer.Length]; byte[] lzBuffer; headerBuffer.CopyTo(huffmanBuffer, 0); //huffmanBuffer = headerBuffer; HuffmanDecoder decoder = new HuffmanDecoder(huffmanBuffer, (ulong)huffmanBuffer.LongLength); lzBuffer = decoder.Unpack(); MemoryStream lzStream = new MemoryStream(lzBuffer); headerBuffer = Unpack(lzStream); } List entries; //There MAY be the case where the singular file is over 4GB, but it's very rare. using (var reader = IndexReader.Create(dx, 8, new MemoryStream(headerBuffer))) { entries = reader.Read(); } return new DxArchive8(file, this,entries ,key, 8,dx.HuffmanKB); //retu rn null; } public override Stream OpenEntry(ArcFile arc, Entry entry) { Stream input = arc.File.CreateStream(entry.Offset, entry.Size); var dx_arc = arc as DxArchive8; if (null == dx_arc) return input; var dx_ent = (DXA8PackedEntry)entry; long dec_offset = dx_ent.UnpackedSize; var key = dx_arc.Encryption.GetEntryKey(dx_ent.Name); input = new EncryptedStream(input, dec_offset, key); if (!dx_ent.HuffmanCompressed && !dx_ent.IsPacked) return input; //we ruled out the case in which neither compression is applied. We still have 3 cases to go. byte[] tmpBuffer = new byte[dx_ent.Size]; input.Read(tmpBuffer, 0, tmpBuffer.Length); if (dx_ent.HuffmanCompressed) { byte[] buffer = new byte[dx_ent.HuffmanSize]; byte[] outBuffer = new byte[dx_ent.IsPacked ? dx_ent.LZSize : dx_ent.UnpackedSize]; Array.Copy(tmpBuffer, buffer, dx_ent.HuffmanSize); HuffmanDecoder decoder = new HuffmanDecoder(buffer,dx_ent.HuffmanSize); byte[] partTmpBuffer = decoder.Unpack(); //returned buffer might be partial. Check if this is the case. var outBufSize = dx_ent.IsPacked ? dx_ent.LZSize : dx_ent.UnpackedSize; if(dx_arc.huffmanMaxKB != 0xff && outBufSize > dx_arc.huffmanMaxKB * 1024 * 2) { //What we have here is two huffmanMaxKB KB buffers, that constitute the beginning and end of file respectively. Array.Copy(partTmpBuffer,0, outBuffer, 0,dx_arc.huffmanMaxKB*1024); Array.Copy(partTmpBuffer,dx_arc.huffmanMaxKB*1024,outBuffer,outBuffer.Length-dx_arc.huffmanMaxKB*1024,dx_arc.huffmanMaxKB*1024); //uncompressed part goes into middle. Array.Copy(tmpBuffer, dx_ent.HuffmanSize, outBuffer, dx_arc.huffmanMaxKB * 1024, outBufSize - dx_arc.huffmanMaxKB * 1024 * 2); tmpBuffer = outBuffer; } else { //that is all that needs to be done. tmpBuffer = partTmpBuffer; } } if(dx_ent.IsPacked) { byte[] buffer = new byte[dx_ent.LZSize]; tmpBuffer.CopyTo(buffer, 0); var tmpMemStream = new MemoryStream(buffer); tmpBuffer = Unpack(tmpMemStream); } return new BinMemoryStream(tmpBuffer, entry.Name); /* if (!dx_ent.IsPacked) return input; using (input) { var data = Unpack(input); return new BinMemoryStream(data, entry.Name); } */ //return null; } } internal sealed class IndexReaderV8 : IndexReader { readonly int m_entry_size; public IndexReaderV8(DxHeader header, int version, Stream input) : base(header, version, input) { m_entry_size = 0x48; } private class DxDirectory { public long DirOffset; public long ParentDirOffset; public int FileCount; public long FileTable; } DxDirectory ReadDirEntry() { var dir = new DxDirectory { DirOffset = m_input.ReadInt64(), ParentDirOffset = m_input.ReadInt64(), FileCount = (int)m_input.ReadInt64(), FileTable = m_input.ReadInt64() }; return dir; } protected override void ReadFileTable(string root, long table_offset) { m_input.Position = m_header.DirTable + table_offset; var dir = ReadDirEntry(); if (dir.DirOffset != -1 && dir.ParentDirOffset != -1) { m_input.Position = m_header.FileTable + dir.DirOffset; root = Path.Combine(root, ExtractFileName(m_input.ReadInt64())); } long current_pos = m_header.FileTable + dir.FileTable; for (int i = 0; i < dir.FileCount; ++i) { m_input.Position = current_pos; var name_offset = m_input.ReadInt64(); uint attr = (uint)m_input.ReadInt64(); m_input.Seek(0x18, SeekOrigin.Current); var offset = m_input.ReadInt64(); if (0 != (attr & 0x10)) // FILE_ATTRIBUTE_DIRECTORY { if (0 == offset || table_offset == offset) throw new InvalidFormatException("Infinite recursion in DXA directory index"); ReadFileTable(root, offset); } else { var size = m_input.ReadInt64(); var packed_size = m_input.ReadInt64(); var huffman_packed_size = m_input.ReadInt64(); var entry = FormatCatalog.Instance.Create(Path.Combine(root, ExtractFileName(name_offset))); entry.Offset = m_header.BaseOffset + offset; entry.UnpackedSize = (uint)size; entry.IsPacked = -1 != packed_size; entry.HuffmanCompressed = -1 != huffman_packed_size; entry.HuffmanSize = (uint)huffman_packed_size; entry.LZSize = (uint)packed_size; //Huffman compression: huffman_packed_size will not exceed 2*HuffmanKB KB. The rest of data is uncompressed (as far as Huffman compressor is concerned). //Add length of uncompressed data to entry.Size if (entry.HuffmanCompressed) { var outBufSize = entry.IsPacked ? packed_size : size; var dx8_hdr = (DxHeaderV8)m_header; if (outBufSize > dx8_hdr.HuffmanKB * 1024 * 2) { huffman_packed_size += outBufSize - dx8_hdr.HuffmanKB * 1024 * 2; } } if (entry.IsPacked||entry.HuffmanCompressed) entry.Size = (uint)(huffman_packed_size!=-1 ? huffman_packed_size:packed_size); else entry.Size = (uint)size; m_dir.Add(entry); } current_pos += m_entry_size; } } } }