diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index e58b2648..9d215848 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -346,6 +346,7 @@ + diff --git a/ArcFormats/TanukiSoft/ArcTAC.cs b/ArcFormats/TanukiSoft/ArcTAC.cs new file mode 100644 index 00000000..fc41b24f --- /dev/null +++ b/ArcFormats/TanukiSoft/ArcTAC.cs @@ -0,0 +1,167 @@ +//! \file ArcTAC.cs +//! \date Wed Jan 25 04:41:35 2017 +//! \brief TanukiSoft resource archive. +// +// Copyright (C) 2017 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.Compression; +using GameRes.Cryptography; + +namespace GameRes.Formats.Tanuki +{ + [Export(typeof(ArchiveFormat))] + public class TacOpener : ArchiveFormat + { + public override string Tag { get { return "TAC"; } } + public override string Description { get { return "TanukiSoft resource archive"; } } + public override uint Signature { get { return 0x63724154; } } // 'TArc' + public override bool IsHierarchic { get { return false; } } + public override bool CanWrite { get { return false; } } + + static readonly byte[] IndexKey = Encoding.ASCII.GetBytes ("TLibArchiveData"); + + public override ArcFile TryOpen (ArcView file) + { + int version; + if (file.View.AsciiEqual (4, "1.00")) + version = 100; + else if (file.View.AsciiEqual (4, "1.10")) + version = 110; + else + return null; + int count = file.View.ReadInt32 (0x14); + if (!IsSaneCount (count)) + return null; + + int bucket_count = file.View.ReadInt32 (0x18); + uint index_size = file.View.ReadUInt32 (0x1C); + long index_offset = version >= 110 ? 0x2C : 0x24; + long base_offset = index_offset + index_size; + var blowfish = new Blowfish (IndexKey); + var packed_bytes = file.View.ReadBytes (index_offset, index_size); + blowfish.Decipher (packed_bytes, packed_bytes.Length & ~7); + + using (var input = new MemoryStream (packed_bytes)) + using (var unpacked = new ZLibStream (input, CompressionMode.Decompress)) + using (var index = new BinaryReader (unpacked)) + { + var dir_table = new List (bucket_count); + for (int i = 0; i < bucket_count; ++i) + { + var entry = new TacBucket(); + entry.Hash = index.ReadUInt16(); + entry.Count = index.ReadUInt16(); + entry.Index = index.ReadInt32(); + dir_table.Add (entry); + } + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + var entry = new TacEntry(); + entry.Hash = index.ReadUInt64(); + entry.IsPacked = index.ReadInt32() != 0; + entry.UnpackedSize = index.ReadUInt32(); + entry.Offset = base_offset + index.ReadUInt32(); + entry.Size = index.ReadUInt32(); + if (!entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + } + var buffer = new byte[8]; + foreach (var bucket in dir_table) + { + for (int i = 0; i < bucket.Count; ++i) + { + var entry = dir[bucket.Index+i] as TacEntry; + entry.Hash = entry.Hash << 16 | bucket.Hash; + entry.Name = string.Format ("{0:X16}", entry.Hash); + if (entry.IsPacked) + continue; + entry.Key = Encoding.ASCII.GetBytes (string.Format ("{0}_tlib_secure_", entry.Hash)); + var bf = new Blowfish (entry.Key); + file.View.Read (entry.Offset, buffer, 0, 8); + bf.Decipher (buffer, 8); + var res = AutoEntry.DetectFileType (buffer.ToUInt32 (0)); + if (res != null) + entry.ChangeType (res); + if ("image" == entry.Type) + entry.EncryptedSize = Math.Min (10240, entry.Size); + else + entry.EncryptedSize = entry.Size; + } + } + return new ArcFile (file, this, dir); + } + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var tent = entry as TacEntry; + if (null == tent) + return base.OpenEntry (arc, entry); + if (tent.IsPacked) + { + var input = arc.File.CreateStream (entry.Offset, entry.Size); + return new ZLibStream (input, CompressionMode.Decompress); + } + var bf = new Blowfish (tent.Key); + if (tent.EncryptedSize < tent.Size) + { + var header = arc.File.View.ReadBytes (tent.Offset, tent.EncryptedSize); + bf.Decipher (header, header.Length); + var rest = arc.File.CreateStream (tent.Offset+tent.EncryptedSize, tent.Size-tent.EncryptedSize); + return new PrefixStream (header, rest); + } + else if (0 == (tent.Size & 7)) + { + var input = arc.File.CreateStream (tent.Offset, tent.Size); + return new InputCryptoStream (input, bf.CreateDecryptor()); + } + else + { + var data = arc.File.View.ReadBytes (tent.Offset, tent.Size); + bf.Decipher (data, data.Length & ~7); + return new BinMemoryStream (data); + } + } + } + + internal class TacEntry : PackedEntry + { + public ulong Hash; + public byte[] Key; + public uint EncryptedSize; + } + + internal class TacBucket + { + public ushort Hash; + public int Count; + public int Index; + } +} diff --git a/supported.html b/supported.html index e4ad9e3f..e0e7963c 100644 --- a/supported.html +++ b/supported.html @@ -1375,6 +1375,10 @@ femme fatale
Kidou Houshinki
*.pcd-No +*.tacTArc1.10
TArc1.00NoTanukiSoft +Shoujo Kyouiku
+Shoukoujo
+

1 Non-encrypted only