mirror of
https://github.com/crskycode/GARbro.git
synced 2024-12-29 14:24:13 +08:00
417 lines
16 KiB
C#
417 lines
16 KiB
C#
//! \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<string, object>;
|
|
|
|
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 BinMemoryStream (output, entry.Name);
|
|
}
|
|
|
|
void DetectFileTypes (ArcView file, List<Entry> 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<int, Entry> m_dir = new Dictionary<int, Entry>();
|
|
|
|
public bool HasNames { get; private set; }
|
|
|
|
public IndexReader (ArcView file)
|
|
{
|
|
m_file = file;
|
|
}
|
|
|
|
public List<Entry> 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<TableRow> 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> (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<TableRow> (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,
|
|
}
|
|
}
|