diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 27439839..8013ea75 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -70,7 +70,6 @@ - @@ -105,6 +104,10 @@ + + + WidgetARC.xaml + @@ -364,6 +367,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/ArcFormats/Properties/Settings.Designer.cs b/ArcFormats/Properties/Settings.Designer.cs index fb71087f..8315b86e 100644 --- a/ArcFormats/Properties/Settings.Designer.cs +++ b/ArcFormats/Properties/Settings.Designer.cs @@ -441,5 +441,17 @@ namespace GameRes.Formats.Properties { this["NSATitle"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string RPMScheme { + get { + return ((string)(this["RPMScheme"])); + } + set { + this["RPMScheme"] = value; + } + } } } diff --git a/ArcFormats/Properties/Settings.settings b/ArcFormats/Properties/Settings.settings index 0f10e9af..9973b683 100644 --- a/ArcFormats/Properties/Settings.settings +++ b/ArcFormats/Properties/Settings.settings @@ -107,5 +107,8 @@ + + + \ No newline at end of file diff --git a/ArcFormats/RPM/ArcARC.cs b/ArcFormats/RPM/ArcARC.cs new file mode 100644 index 00000000..f5a9f12f --- /dev/null +++ b/ArcFormats/RPM/ArcARC.cs @@ -0,0 +1,236 @@ +//! \file ArcARC.cs +//! \date Sat Sep 19 22:24:12 2015 +//! \brief RPM engine resource archive. +// +// Copyright (C) 2015 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.Text; +using GameRes.Compression; +using GameRes.Formats.Strings; +using GameRes.Formats.Properties; +using GameRes.Utility; + +namespace GameRes.Formats.Rpm +{ + public class RpmOptions : ResourceOptions + { + public EncryptionScheme Scheme; + } + + [Serializable] + public class EncryptionScheme + { + public string Keyword; + public int NameLength; + + public EncryptionScheme (string key, int name_length = 32) + { + Keyword = key; + NameLength = name_length; + } + } + + [Serializable] + public class ArcScheme : ResourceScheme + { + public Dictionary KnownSchemes; + } + + [Export(typeof(ArchiveFormat))] + public class ArcOpener : ArchiveFormat + { + public override string Tag { get { return "ARC/RPM"; } } + public override string Description { get { return "RPM engine resource archive"; } } + public override uint Signature { get { return 0; } } + public override bool IsHierarchic { get { return false; } } + public override bool CanCreate { get { return false; } } + + public ArcOpener () + { + Extensions = new string[] { "arc" }; + } + + public static Dictionary KnownSchemes = new Dictionary(); + + public override ResourceScheme Scheme + { + get { return new ArcScheme { KnownSchemes = KnownSchemes }; } + set { KnownSchemes = ((ArcScheme)value).KnownSchemes; } + } + + public override ArcFile TryOpen (ArcView file) + { + int count = file.View.ReadInt32 (0); + if (!IsSaneCount (count)) + return null; + uint is_compressed = file.View.ReadUInt32 (4); + if (is_compressed > 1) // should be either 0 or 1 + return null; + var scheme = GuessScheme (file, count); + // additional filename extension check avoids dialog popup on false positives + if (null == scheme && KnownSchemes.Count > 0 && file.Name.EndsWith (".arc", StringComparison.InvariantCultureIgnoreCase)) + scheme = QueryScheme(); + if (null == scheme) + return null; + + // special case for "instdata.arc" archives + if (Path.GetFileName (file.Name).Equals ("instdata.arc", StringComparison.InvariantCultureIgnoreCase)) + scheme = new EncryptionScheme ("inst", scheme.NameLength); + + int index_size = count * (scheme.NameLength + 12); + var index = new byte[index_size]; + if (index_size != file.View.Read (8, index, 0, (uint)index_size)) + return null; + DecryptIndex (index, scheme.Keyword); + + uint data_offset = LittleEndian.ToUInt32 (index, scheme.NameLength + 8); + if (data_offset != 8 + index_size) + return null; + + int index_offset = 0; + var dir = new List (count); + for (int i = 0; i < count; ++i) + { + var name = Binary.GetCString (index, index_offset, scheme.NameLength); + index_offset += scheme.NameLength; + var entry = FormatCatalog.Instance.Create (name); + entry.UnpackedSize = LittleEndian.ToUInt32 (index, index_offset); + entry.Size = LittleEndian.ToUInt32 (index, index_offset+4); + entry.Offset = LittleEndian.ToUInt32 (index, index_offset+8); + entry.IsPacked = is_compressed != 0; + if (entry.Offset < data_offset || !entry.CheckPlacement (file.MaxOffset)) + return null; + dir.Add (entry); + index_offset += 12; + } + return new ArcFile (file, this, dir); + } + + public override Stream OpenEntry (ArcFile arc, Entry entry) + { + var input = arc.File.CreateStream (entry.Offset, entry.Size); + var packed = entry as PackedEntry; + if (null == packed || !packed.IsPacked) + return input; + return new LzssStream (input); + } + + EncryptionScheme GuessScheme (ArcView file, int count) + { + int[] possible_sizes = { 32, 24 }; + byte[] first_entry = new byte[possible_sizes[0] + 12]; + if (first_entry.Length != file.View.Read (8, first_entry, 0, (uint)first_entry.Length)) + return null; + byte[] key_bits = new byte[4]; + byte[] actual_offset = new byte[4]; + foreach (var name_length in possible_sizes) + { + int first_offset = 8 + count * (name_length + 12); + if (first_offset >= file.MaxOffset) + continue; + LittleEndian.Pack (first_offset, actual_offset, 0); + int i; + for (i = 0; i < 4; ++i) + { + key_bits[i] = (byte)(first_entry[name_length+8+i] - actual_offset[i]); + } + + int first_match = ReverseFind (first_entry, name_length-4, key_bits); + if (first_match < 4) + continue; + int second_match = ReverseFind (first_entry, first_match-4, key_bits); + if (second_match <= 0) + continue; + int key_length = first_match - second_match; + byte[] key = new byte[key_length]; + for (i = 0; i < key_length; ++i) + { + byte sym = (byte)-first_entry[second_match+i]; + if (sym < 0x21 || sym > 0x7E) + break; + key[(second_match+i) % key_length] = sym; + } + if (i == key_length) + return new EncryptionScheme (Encoding.ASCII.GetString (key), name_length); + } + return null; + } + + static int ReverseFind (byte[] array, int pos, byte[] pattern) + { + int pattern_end_pos = pattern.Length-1; + int pattern_pos = pattern_end_pos; + for (int i = pos + pattern_pos; i >= 0; --i) + { + if (array[i] == pattern[pattern_pos]) + { + if (0 == pattern_pos) + return i; + --pattern_pos; + } + else if (pattern_end_pos != pattern_pos) + { + i += pattern_end_pos - pattern_pos; + pattern_pos = pattern_end_pos; + } + } + return -1; + } + + private void DecryptIndex (byte[] data, string key) + { + for (int i = 0; i < data.Length; ++i) + { + data[i] += (byte)key[i % key.Length]; + } + } + + public override ResourceOptions GetDefaultOptions () + { + return new RpmOptions { + Scheme = GetScheme (Settings.Default.RPMScheme), + }; + } + + public override object GetAccessWidget () + { + return new WidgetARC(); + } + + EncryptionScheme QueryScheme () + { + var options = Query (arcStrings.RPMEncryptedNotice); + return options.Scheme; + } + + static EncryptionScheme GetScheme (string title) + { + EncryptionScheme scheme = null; + KnownSchemes.TryGetValue (title, out scheme); + return scheme; + } + } +} diff --git a/ArcFormats/RPM/WidgetARC.xaml b/ArcFormats/RPM/WidgetARC.xaml new file mode 100644 index 00000000..0778d80c --- /dev/null +++ b/ArcFormats/RPM/WidgetARC.xaml @@ -0,0 +1,9 @@ + + + diff --git a/ArcFormats/RPM/WidgetARC.xaml.cs b/ArcFormats/RPM/WidgetARC.xaml.cs new file mode 100644 index 00000000..cf65b5dd --- /dev/null +++ b/ArcFormats/RPM/WidgetARC.xaml.cs @@ -0,0 +1,15 @@ +using System.Windows.Controls; + +namespace GameRes.Formats.Rpm +{ + /// + /// Interaction logic for WidgetARC.xaml + /// + public partial class WidgetARC : Grid + { + public WidgetARC () + { + InitializeComponent (); + } + } +} diff --git a/ArcFormats/Strings/arcStrings.Designer.cs b/ArcFormats/Strings/arcStrings.Designer.cs index 1c10853c..60b03f14 100644 --- a/ArcFormats/Strings/arcStrings.Designer.cs +++ b/ArcFormats/Strings/arcStrings.Designer.cs @@ -569,6 +569,17 @@ namespace GameRes.Formats.Strings { } } + /// + /// Looks up a localized string similar to File resembles RPM resource archive, + ///but encryption key guess failed. + ///Choose appropriate encryption scheme.. + /// + public static string RPMEncryptedNotice { + get { + return ResourceManager.GetString("RPMEncryptedNotice", resourceCulture); + } + } + /// /// Looks up a localized string similar to Amaterasu Translations Muv-Luv script file. /// diff --git a/ArcFormats/Strings/arcStrings.resx b/ArcFormats/Strings/arcStrings.resx index 3b2e7205..96b6633f 100644 --- a/ArcFormats/Strings/arcStrings.resx +++ b/ArcFormats/Strings/arcStrings.resx @@ -334,4 +334,9 @@ Choose encryption scheme or enter a passphrase. Choose title or enter a password + + File resembles RPM resource archive, +but encryption key guess failed. +Choose appropriate encryption scheme. + \ No newline at end of file diff --git a/ArcFormats/Strings/arcStrings.ru-RU.resx b/ArcFormats/Strings/arcStrings.ru-RU.resx index 6e898324..0a91045e 100644 --- a/ArcFormats/Strings/arcStrings.ru-RU.resx +++ b/ArcFormats/Strings/arcStrings.ru-RU.resx @@ -257,6 +257,11 @@ 32-битный ключ + + По-видимому, файл является RPM архивом, +однако определить ключ шифрования не удалось. +Выберите подходящий способ шифрования. + Кодировка имён файлов diff --git a/ArcFormats/app.config b/ArcFormats/app.config index 8a384388..3071b2b9 100644 --- a/ArcFormats/app.config +++ b/ArcFormats/app.config @@ -109,6 +109,9 @@ + + + diff --git a/supported.html b/supported.html index 298e783f..46a0719e 100644 --- a/supported.html +++ b/supported.html @@ -317,7 +317,11 @@ Zettai Meikyuu Grimm
Jokei Kazoku ~Inbou~
*.gccG24n
G24m
R24n
R24mNo -*.arc-NoTumugiKimi no Omoi, Sono Negai +*.arc-NoRPM +Kimi no Koe ga Kikoeru
+Kimi no Omoi, Sono Negai
+Sora no Iro, Mizu no Iro
+ *.iksNPSRNoX[iks]Shikkan ~Hazukashimerareta Karada, Oreta Kokoro~ *.wbpARCFORM3 WBUGNoWild Bug Happy Planning