2016-08-31 11:01:31 +04:00
|
|
|
|
//! \file ArcARC3.cs
|
|
|
|
|
//! \date Tue Aug 30 10:57:30 2016
|
|
|
|
|
//! \brief Caramel BOX 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.Security.Cryptography;
|
|
|
|
|
using GameRes.Utility;
|
|
|
|
|
|
|
|
|
|
namespace GameRes.Formats.CaramelBox
|
|
|
|
|
{
|
|
|
|
|
internal class Arc3Entry : PackedEntry
|
|
|
|
|
{
|
|
|
|
|
public uint Flags;
|
|
|
|
|
public bool IsEncrypted;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Export(typeof(ArchiveFormat))]
|
|
|
|
|
public class Arc3Opener : ArchiveFormat
|
|
|
|
|
{
|
|
|
|
|
public override string Tag { get { return "ARC3"; } }
|
|
|
|
|
public override string Description { get { return "Caramel BOX resource archive"; } }
|
|
|
|
|
public override uint Signature { get { return 0x33637261; } } // 'arc3'
|
|
|
|
|
public override bool IsHierarchic { get { return false; } }
|
2016-10-11 00:05:22 +04:00
|
|
|
|
public override bool CanWrite { get { return false; } }
|
2016-08-31 11:01:31 +04:00
|
|
|
|
|
|
|
|
|
public Arc3Opener ()
|
|
|
|
|
{
|
|
|
|
|
Extensions = new string[] { "bin", "ar3" };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override ArcFile TryOpen (ArcView file)
|
|
|
|
|
{
|
|
|
|
|
int version = Binary.BigEndian (file.View.ReadInt32 (4));
|
|
|
|
|
uint cluster_size = Binary.BigEndian (file.View.ReadUInt32 (0x08));
|
|
|
|
|
uint base_offset = Binary.BigEndian (file.View.ReadUInt32 (0x0C));
|
|
|
|
|
uint index_offset = Binary.BigEndian (file.View.ReadUInt32 (0x18));
|
|
|
|
|
uint index_size = Binary.BigEndian (file.View.ReadUInt32 (0x1C));
|
|
|
|
|
if (0 == index_size)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
bool new_name = false;
|
|
|
|
|
var dir = new List<Entry>();
|
|
|
|
|
var name_buffer = new byte[0x10];
|
|
|
|
|
long current_offset = (long)index_offset * cluster_size;
|
|
|
|
|
long index_end = current_offset + index_size;
|
|
|
|
|
if (index_end > file.MaxOffset)
|
|
|
|
|
return null;
|
|
|
|
|
|
|
|
|
|
// --- read index ---
|
|
|
|
|
|
|
|
|
|
uint last_entry_offset = 0x7FFFFFFF;
|
|
|
|
|
int current_name_length = 0;
|
|
|
|
|
Arc3Entry prev_entry = null;
|
|
|
|
|
Arc3Entry long_info = null;
|
|
|
|
|
while (current_offset < index_end)
|
|
|
|
|
{
|
|
|
|
|
byte name_length = file.View.ReadByte (current_offset++);
|
|
|
|
|
int name_offset = name_length >> 4;
|
|
|
|
|
name_length &= 0xF;
|
|
|
|
|
if (name_offset != 0xF)
|
|
|
|
|
{
|
|
|
|
|
file.View.Read (current_offset, name_buffer, name_offset, name_length);
|
|
|
|
|
current_offset += name_length;
|
|
|
|
|
current_name_length = name_offset+name_length;
|
|
|
|
|
}
|
|
|
|
|
else if (0xF == name_length)
|
|
|
|
|
{
|
|
|
|
|
name_buffer[current_name_length-1]++;
|
|
|
|
|
}
|
|
|
|
|
else if (name_length != 0)
|
|
|
|
|
{
|
|
|
|
|
file.View.Read (current_offset, name_buffer, 0, name_length);
|
|
|
|
|
current_offset += name_length;
|
|
|
|
|
current_name_length = name_length;
|
|
|
|
|
new_name = true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
uint offset = BigEndian24 (file.View.ReadUInt32 (current_offset));
|
|
|
|
|
offset = (uint)Math.Abs ((int)(offset - index_offset));
|
|
|
|
|
if (offset < last_entry_offset)
|
|
|
|
|
last_entry_offset = offset;
|
|
|
|
|
|
|
|
|
|
if (prev_entry != null)
|
|
|
|
|
prev_entry.Offset = (long)(last_entry_offset + base_offset) * cluster_size;
|
|
|
|
|
}
|
|
|
|
|
last_entry_offset = BigEndian24 (file.View.ReadUInt32 (current_offset));
|
|
|
|
|
current_offset += 3;
|
|
|
|
|
if (new_name)
|
|
|
|
|
{
|
|
|
|
|
current_offset += 3;
|
|
|
|
|
new_name = false;
|
|
|
|
|
}
|
|
|
|
|
string name;
|
|
|
|
|
if (current_name_length > 3)
|
|
|
|
|
{
|
|
|
|
|
name = Encodings.cp932.GetString (name_buffer, 3, current_name_length-3);
|
|
|
|
|
string ext = Encodings.cp932.GetString (name_buffer, 0, 3);
|
|
|
|
|
name = name + '.' + ext;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
name = Encodings.cp932.GetString (name_buffer, 0, current_name_length);
|
|
|
|
|
|
|
|
|
|
var entry = new Arc3Entry { Name = name };
|
|
|
|
|
entry.Offset = (long)(last_entry_offset + base_offset) * cluster_size;
|
|
|
|
|
if (entry.Offset >= file.MaxOffset)
|
|
|
|
|
return null;
|
|
|
|
|
dir.Add (entry);
|
|
|
|
|
prev_entry = entry;
|
|
|
|
|
if (null == long_info && "longinfo.$$$" == name)
|
|
|
|
|
long_info = entry;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- read attributes ---
|
|
|
|
|
|
|
|
|
|
foreach (Arc3Entry entry in dir)
|
|
|
|
|
{
|
|
|
|
|
entry.Size = Binary.BigEndian (file.View.ReadUInt32 (entry.Offset+8));
|
|
|
|
|
entry.Flags = Binary.BigEndian (file.View.ReadUInt32 (entry.Offset+0x14));
|
|
|
|
|
entry.UnpackedSize = entry.Size;
|
|
|
|
|
entry.IsEncrypted = 2 == entry.Flags;
|
|
|
|
|
entry.Offset += 0x20;
|
|
|
|
|
uint signature = file.View.ReadUInt32 (entry.Offset);
|
|
|
|
|
if (entry.IsEncrypted)
|
|
|
|
|
signature = ~signature;
|
|
|
|
|
entry.IsPacked = (signature & 0xFFFF) == 0x7A6C; // 'lz'
|
|
|
|
|
if (entry.IsPacked)
|
|
|
|
|
{
|
|
|
|
|
entry.UnpackedSize = Binary.BigEndian (file.View.ReadUInt32 (entry.Offset+2));
|
|
|
|
|
if (entry.IsEncrypted)
|
|
|
|
|
entry.UnpackedSize ^= 0xFFFFFFFF;
|
|
|
|
|
entry.Offset += 6;
|
|
|
|
|
entry.Size -= 6;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
var res = AutoEntry.DetectFileType (signature);
|
|
|
|
|
if (res != null)
|
|
|
|
|
entry.Type = res.Type;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
var arc = new ArcFile (file, this, dir);
|
|
|
|
|
try // read long filenames stored within 'longinfo.$$$', if available
|
|
|
|
|
{
|
|
|
|
|
if (version > 1 && long_info != null)
|
|
|
|
|
{
|
|
|
|
|
var name_map = ReadNameMap (arc, long_info);
|
|
|
|
|
foreach (var entry in dir)
|
|
|
|
|
{
|
|
|
|
|
string orig_name;
|
|
|
|
|
if (name_map.TryGetValue (entry.Name, out orig_name))
|
|
|
|
|
entry.Name = orig_name;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch { /* ignore 'longinfo.$$$' read errors */ }
|
|
|
|
|
|
|
|
|
|
foreach (var entry in dir.Where (e => e.Name.Contains ('*')))
|
|
|
|
|
entry.Name = entry.Name.Replace ('*', '*');
|
|
|
|
|
return arc;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override Stream OpenEntry (ArcFile arc, Entry entry)
|
|
|
|
|
{
|
|
|
|
|
var a3ent = entry as Arc3Entry;
|
|
|
|
|
Stream input = arc.File.CreateStream (entry.Offset, entry.Size);
|
|
|
|
|
if (null == entry)
|
|
|
|
|
return input;
|
|
|
|
|
if (a3ent.IsEncrypted)
|
|
|
|
|
{
|
2016-12-25 06:51:33 +04:00
|
|
|
|
input = new InputCryptoStream (input, new NotTransform());
|
2016-08-31 11:01:31 +04:00
|
|
|
|
}
|
|
|
|
|
if (!a3ent.IsPacked)
|
|
|
|
|
return input;
|
|
|
|
|
using (input)
|
|
|
|
|
{
|
|
|
|
|
var data = UnpackLze (input, a3ent.UnpackedSize);
|
2016-10-16 09:22:53 +04:00
|
|
|
|
return new BinMemoryStream (data, entry.Name);
|
2016-08-31 11:01:31 +04:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint BigEndian24 (uint x)
|
|
|
|
|
{
|
|
|
|
|
return (x & 0xFFu) << 16 | x & 0xFF00u | (x >> 16) & 0xFFu;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
byte[] UnpackLze (Stream input, uint unpacked_size)
|
|
|
|
|
{
|
|
|
|
|
var data = new byte[unpacked_size];
|
|
|
|
|
var header = new byte[4];
|
|
|
|
|
int dst = 0;
|
|
|
|
|
using (var bits = new MsbBitStream (input, true))
|
|
|
|
|
{
|
|
|
|
|
while (dst < data.Length)
|
|
|
|
|
{
|
|
|
|
|
if (4 != input.Read (header, 0, 4))
|
|
|
|
|
break;
|
|
|
|
|
if (!Binary.AsciiEqual (header, "ze"))
|
|
|
|
|
throw new InvalidFormatException ("Malformed compressed stream");
|
|
|
|
|
int chunk_length = BigEndian.ToUInt16 (header, 2);
|
|
|
|
|
bits.Reset();
|
|
|
|
|
UnpackZeChunk (bits, data, dst, chunk_length);
|
|
|
|
|
dst += chunk_length;
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void UnpackZeChunk (IBitStream bits, byte[] output, int dst, int unpacked_size)
|
|
|
|
|
{
|
|
|
|
|
int output_end = dst + unpacked_size;
|
|
|
|
|
while (dst < output_end)
|
|
|
|
|
{
|
|
|
|
|
int count = LzeGetInteger (bits);
|
|
|
|
|
if (-1 == count)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
while (--count > 0)
|
|
|
|
|
{
|
|
|
|
|
int data = bits.GetBits (8);
|
|
|
|
|
if (-1 == data)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
if (dst < output_end)
|
|
|
|
|
output[dst++] = (byte)data;
|
|
|
|
|
}
|
|
|
|
|
if (count > 0 || dst >= output_end)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
int offset = LzeGetInteger (bits);
|
|
|
|
|
if (-1 == offset)
|
|
|
|
|
break;
|
|
|
|
|
count = LzeGetInteger (bits);
|
|
|
|
|
if (-1 == count)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
Binary.CopyOverlapped (output, dst-offset, dst, count);
|
|
|
|
|
dst += count;
|
|
|
|
|
}
|
|
|
|
|
if (dst < output_end)
|
|
|
|
|
throw new EndOfStreamException ("Premature end of compressed stream");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int LzeGetInteger (IBitStream bits)
|
|
|
|
|
{
|
|
|
|
|
int length = 0;
|
|
|
|
|
for (int i = 0; i < 16; ++i)
|
|
|
|
|
{
|
|
|
|
|
if (0 != bits.GetNextBit())
|
|
|
|
|
break;
|
|
|
|
|
++length;
|
|
|
|
|
}
|
|
|
|
|
int v = 1 << length;
|
|
|
|
|
if (length > 0)
|
|
|
|
|
v |= bits.GetBits (length);
|
|
|
|
|
return v;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
IDictionary<string, string> ReadNameMap (ArcFile file, Arc3Entry entry)
|
|
|
|
|
{
|
|
|
|
|
byte[] table = new byte[entry.UnpackedSize];
|
|
|
|
|
using (var input = OpenEntry (file, entry))
|
|
|
|
|
{
|
|
|
|
|
input.Read (table, 0, table.Length);
|
|
|
|
|
}
|
|
|
|
|
int count = LittleEndian.ToInt32 (table, 0);
|
|
|
|
|
if (!IsSaneCount (count))
|
|
|
|
|
throw new InvalidFormatException ("Invalid longinfo map format");
|
|
|
|
|
var map = new Dictionary<string, string> (count);
|
|
|
|
|
int index_pos = 4;
|
|
|
|
|
int names_pos = index_pos + count * 8;
|
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
|
|
|
{
|
|
|
|
|
int key_pos = names_pos + LittleEndian.ToInt32 (table, index_pos);
|
|
|
|
|
int value_pos = names_pos + LittleEndian.ToInt32 (table, index_pos+4);
|
|
|
|
|
index_pos += 8;
|
|
|
|
|
string key = Binary.GetCString (table, key_pos, table.Length - key_pos);
|
|
|
|
|
map[key] = Binary.GetCString (table, value_pos, table.Length - value_pos);
|
|
|
|
|
}
|
|
|
|
|
return map;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|