//! \file ArcCPK.cs //! \date Mon Feb 29 12:39:36 2016 //! \brief CRI Middleware resource archive. // // Copyright (C) 2016 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 System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; using System.Text; using GameRes.Utility; namespace GameRes.Formats.Cri { using TableRow = Dictionary; internal class CpkEntry : PackedEntry { public int Id; } [Export(typeof(ArchiveFormat))] public class CpkOpener : ArchiveFormat { public override string Tag { get { return "CPK"; } } public override string Description { get { return "CRI Middleware resource archive"; } } public override uint Signature { get { return 0x204B5043; } } // 'CPK ' public override bool IsHierarchic { get { return false; } } public override bool CanWrite { get { return false; } } public override ArcFile TryOpen (ArcView file) { var reader = new IndexReader (file); var dir = reader.ReadIndex(); if (null == dir || !dir.Any()) return null; if (!reader.HasNames) DetectFileTypes (file, dir); return new ArcFile (file, this, dir); } public override Stream OpenEntry (ArcFile arc, Entry entry) { if (entry.Size < 0x10 || !arc.File.View.AsciiEqual (entry.Offset, "CRILAYLA")) return base.OpenEntry (arc, entry); var unpacked_size = arc.File.View.ReadInt32 (entry.Offset+8); var packed_size = arc.File.View.ReadUInt32 (entry.Offset+12); if (unpacked_size < 0 || packed_size > entry.Size - 0x10) return base.OpenEntry (arc, entry); uint prefix_size = entry.Size - (0x10+packed_size); var output = new byte[unpacked_size+prefix_size]; var packed = arc.File.View.ReadBytes (entry.Offset+0x10, packed_size); Array.Reverse (packed); using (var mem = new MemoryStream (packed)) using (var input = new MsbBitStream (mem)) { byte[] sizes = { 2, 3, 5, 8 }; int dst = (int)prefix_size; while (dst < output.Length) { if (0 == input.GetNextBit()) { output[dst++] = (byte)input.GetBits (8); continue; } int count = 3; int offset = input.GetBits (13) + 3; int rank = 0; int bits, step; do { bits = sizes[rank]; step = input.GetBits (bits); count += step; if (rank < 3) rank++; } while (((1 << bits) - 1) == step); Binary.CopyOverlapped (output, dst-offset, dst, count); dst += count; } } Array.Reverse (output, (int)prefix_size, unpacked_size); arc.File.View.Read (entry.Offset+0x10+packed_size, output, 0, prefix_size); return new MemoryStream (output); } void DetectFileTypes (ArcView file, List dir) { foreach (var entry in dir) { var offset = entry.Offset; var signature = file.View.ReadUInt32 (offset); if (entry.Size > 0x10 && 0x4C495243 == signature) // 'CRIL' { uint packed_size = file.View.ReadUInt32 (offset+12); if (packed_size < entry.Size - 0x10) { signature = file.View.ReadUInt32 (offset+0x10+packed_size); if (0x10 == signature) signature = file.View.ReadUInt32 (offset+0x10+packed_size+signature); } } var res = AutoEntry.DetectFileType (signature); if (null != res) { entry.Type = res.Type; entry.Name = Path.ChangeExtension (entry.Name, res.Extensions.FirstOrDefault()); } } } } internal class IndexReader { ArcView m_file; Deserializer m_des = new Deserializer(); long m_content_offset; Dictionary m_dir = new Dictionary(); public bool HasNames { get; private set; } public IndexReader (ArcView file) { m_file = file; } public List ReadIndex () { var chunk = ReadUTFChunk (4); var header = m_des.DeserializeUTFChunk (chunk).First(); m_content_offset = (long)header["ContentOffset"]; HasNames = header.ContainsKey ("TocOffset"); if (HasNames) { ReadToc ((long)header["TocOffset"]); } if (header.ContainsKey ("ItocOffset")) { var align = (uint)(int)header["Align"]; ReadItoc ((long)header["ItocOffset"], align); } return m_dir.Values.ToList(); } void ReadToc (long toc_offset) { var base_offset = Math.Min (m_content_offset, toc_offset); if (!m_file.View.AsciiEqual (toc_offset, "TOC ")) throw new InvalidFormatException(); var chunk = ReadUTFChunk (toc_offset+4); var table = m_des.DeserializeUTFChunk (chunk); foreach (var row in table) { var entry = new CpkEntry { Id = (int)row["ID"], Offset = (long)row["FileOffset"] + base_offset, Size = (uint)(int)row["FileSize"], }; if (row.ContainsKey ("ExtractSize")) entry.UnpackedSize = (uint)(int)row["ExtractSize"]; else entry.UnpackedSize = entry.Size; entry.IsPacked = entry.Size != entry.UnpackedSize; var name = (string)row["FileName"]; if (row.ContainsKey ("DirName")) name = Path.Combine ((string)row["DirName"], name); entry.Name = name; entry.Type = FormatCatalog.Instance.GetTypeFromName (name); m_dir[entry.Id] = entry; } } void ReadItoc (long toc_offset, uint align) { if (!m_file.View.AsciiEqual (toc_offset, "ITOC")) throw new InvalidFormatException(); var chunk = ReadUTFChunk (toc_offset+4); var itoc = m_des.DeserializeUTFChunk (chunk).FirstOrDefault(); if (null == itoc || !itoc.ContainsKey ("DataL")) return; var dataL = m_des.DeserializeUTFChunk ((byte[])itoc["DataL"]); var dataH = m_des.DeserializeUTFChunk ((byte[])itoc["DataH"]); foreach (var row in dataL.Concat (dataH)) { int id = (int)row["ID"]; var entry = GetEntryById (id); entry.Size = (uint)(int)row["FileSize"]; if (row.ContainsKey ("ExtractSize")) entry.UnpackedSize = (uint)(int)row["ExtractSize"]; else entry.UnpackedSize = entry.Size; entry.IsPacked = entry.Size != entry.UnpackedSize; } long current_offset = m_content_offset; foreach (var id in m_dir.Keys.OrderBy (x => x)) { var entry = m_dir[id]; entry.Offset = current_offset; current_offset += entry.Size; if (align != 0) { var tail = entry.Size % align; if (tail > 0) current_offset += align - tail; } if (string.IsNullOrEmpty (entry.Name)) entry.Name = id.ToString ("D5"); } } CpkEntry GetEntryById (int id) { Entry entry; if (!m_dir.TryGetValue (id, out entry)) { entry = new CpkEntry { Id = id }; m_dir[id] = entry; } return entry as CpkEntry; } byte[] ReadUTFChunk (long offset) { long chunk_size = m_file.View.ReadInt64 (offset+4); if (chunk_size < 0 || chunk_size > int.MaxValue) throw new FileSizeException(); var chunk = m_file.View.ReadBytes (offset+12, (uint)chunk_size); if (chunk.Length < chunk_size) throw new EndOfStreamException ("Unexpected end of file"); if (!Binary.AsciiEqual (chunk, 0, "@UTF")) DecryptUTFChunk (chunk); return chunk; } internal static void DecryptUTFChunk (byte[] chunk) { int key = 0x655F; for (int i = 0; i < chunk.Length; i++) { chunk[i] ^= (byte)key; key *= 0x4115; } } } internal class Deserializer { byte[] m_chunk; public List DeserializeUTFChunk (byte[] chunk) { m_chunk = chunk; if (!Binary.AsciiEqual (m_chunk, 0, "@UTF")) throw new InvalidFormatException(); var chunk_length = BigEndian.ToInt32 (m_chunk, 4); using (var mem = new MemoryStream (m_chunk, 8, chunk_length)) using (var input = new BigEndianReader (mem)) { int rows_offset = input.ReadInt32(); int strings_offset = input.ReadInt32() + 8; int data_offset = input.ReadInt32() + 8; input.Skip (4); int column_count = input.ReadInt16(); int row_length = input.ReadInt16(); int row_count = input.ReadInt32(); var columns = new List (column_count); for (int i = 0; i < column_count; ++i) { byte flags = input.ReadByte(); if (0 == flags) { input.Skip (3); flags = input.ReadByte(); } int name_offset = strings_offset + input.ReadInt32(); var column = new Column { Flags = (TableFlags)flags, Name = ReadString (name_offset), }; columns.Add (column); } var table = new List (row_count); int next_offset = rows_offset; for (int i = 0; i < row_count; ++i) { input.Position = next_offset; next_offset += row_length; var row = new TableRow (column_count); table.Add (row); foreach (var column in columns) { var storage = column.Flags & TableFlags.StorageMask; if (TableFlags.StorageNone == storage || TableFlags.StorageZero == storage || TableFlags.StorageConstant == storage) continue; switch (column.Flags & TableFlags.TypeMask) { case TableFlags.TypeByte: row[column.Name] = (int)input.ReadByte(); break; case TableFlags.TypeSByte: row[column.Name] = (int)input.ReadSByte(); break; case TableFlags.TypeUInt16: row[column.Name] = (int)input.ReadUInt16(); break; case TableFlags.TypeInt16: row[column.Name] = (int)input.ReadInt16(); break; case TableFlags.TypeUInt32: case TableFlags.TypeInt32: row[column.Name] = input.ReadInt32(); break; case TableFlags.TypeUInt64: case TableFlags.TypeInt64: row[column.Name] = input.ReadInt64(); break; case TableFlags.TypeFloat32: row[column.Name] = input.ReadSingle(); break; case TableFlags.TypeString: { int offset = strings_offset + input.ReadInt32(); row[column.Name] = ReadString (offset); break; } case TableFlags.TypeData: { int offset = data_offset + input.ReadInt32(); int length = input.ReadInt32(); row[column.Name] = m_chunk.Skip (offset).Take (length).ToArray(); break; } default: throw new NotSupportedException(); } } } return table; } } string ReadString (int offset) { return Binary.GetCString (m_chunk, offset, 0xFF, Encoding.UTF8); } } internal class Column { public TableFlags Flags; public string Name; } [Flags] internal enum TableFlags : byte { StorageMask = 0xF0, StorageNone = 0x00, StorageZero = 0x10, StorageConstant = 0x30, TypeMask = 0x0F, TypeByte = 0x00, TypeSByte = 0x01, TypeUInt16 = 0x02, TypeInt16 = 0x03, TypeUInt32 = 0x04, TypeInt32 = 0x05, TypeUInt64 = 0x06, TypeInt64 = 0x07, TypeFloat32 = 0x08, TypeFloat64 = 0x09, TypeString = 0x0A, TypeData = 0x0B, } }