From 40526020730f8aa88c39a7be6429833254ecb9e0 Mon Sep 17 00:00:00 2001 From: morkt Date: Wed, 31 Aug 2016 11:01:31 +0400 Subject: [PATCH] implemented 'arc3' archives. --- ArcFormats/ArcFormats.csproj | 1 + ArcFormats/CaramelBox/ArcARC3.cs | 308 +++++++++++++++++++++++++++++++ supported.html | 4 +- 3 files changed, 312 insertions(+), 1 deletion(-) create mode 100644 ArcFormats/CaramelBox/ArcARC3.cs diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 9b64c2f8..c81f2e03 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -87,6 +87,7 @@ + diff --git a/ArcFormats/CaramelBox/ArcARC3.cs b/ArcFormats/CaramelBox/ArcARC3.cs new file mode 100644 index 00000000..f0e16406 --- /dev/null +++ b/ArcFormats/CaramelBox/ArcARC3.cs @@ -0,0 +1,308 @@ +//! \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; } } + public override bool CanCreate { get { return false; } } + + 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(); + 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) + { + input = new CryptoStream (input, new NotTransform(), CryptoStreamMode.Read); + } + if (!a3ent.IsPacked) + return input; + using (input) + { + var data = UnpackLze (input, a3ent.UnpackedSize); + return new MemoryStream (data); + } + } + + 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 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 (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; + } + } +} diff --git a/supported.html b/supported.html index 08e44163..447d1a36 100644 --- a/supported.html +++ b/supported.html @@ -279,6 +279,7 @@ Zettai Karen! Ojou-sama
Eroge! ~H mo Game mo Kaihatsu Zanmai~
Mamono Musume-tachi to no Rakuen ~Slime & Scylla~
Mashou no Nie 3 ~Hakudaku no Umi ni Shizumu Injoku no Reiki~
+Ryuuyoku no Melodia -Diva with the blessed dragonol-
Sei Monmusu Festival!!
Shin Chikan Ou
Unionism Quartet
@@ -1058,8 +1059,9 @@ Uchuu Keiji Soldivan
*.ttd.FRCNoMorning Binkan Ecchi! ~Futari no Oyatsu wa Tokunou Milk~
-*.binARC4NoCaramel BOX +*.binARC4
arc3NoCaramel BOX Boku no Te no Naka no Rakuen
+Caramel Box Yarukibako
*.fcbfcb1No *.datYOXNoShelf