diff --git a/ArcFormats/ArcFormats.csproj b/ArcFormats/ArcFormats.csproj index 9d481fb6..52fb0ac0 100644 --- a/ArcFormats/ArcFormats.csproj +++ b/ArcFormats/ArcFormats.csproj @@ -535,11 +535,14 @@ - - - - WidgetPAZ.xaml - + + + + CreatePAZWidget.xaml + + + WidgetPAZ.xaml + @@ -1134,14 +1137,18 @@ Designer MSBuild:Compile - - Designer - MSBuild:Compile - - - MSBuild:Compile - Designer - + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + Designer MSBuild:Compile @@ -1320,4 +1327,4 @@ xcopy "$(ProjectDir)\Resources\Formats.dat" "$(TargetDir)\GameData\" /D /Y >N --> - \ No newline at end of file + diff --git a/ArcFormats/Musica/ArcPAZ.cs b/ArcFormats/Musica/ArcPAZ.cs index 178509c7..e4e40c02 100644 --- a/ArcFormats/Musica/ArcPAZ.cs +++ b/ArcFormats/Musica/ArcPAZ.cs @@ -26,6 +26,7 @@ using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; +using System.Text; using GameRes.Compression; using GameRes.Cryptography; using GameRes.Formats.Strings; @@ -133,7 +134,7 @@ namespace GameRes.Formats.Musica public override string Description { get { return "Musica engine resource archive"; } } public override uint Signature { get { return 0; } } public override bool IsHierarchic { get { return true; } } - public override bool CanWrite { get { return false; } } + public override bool CanWrite { get { return true; } } public PazOpener () { @@ -319,7 +320,44 @@ namespace GameRes.Formats.Musica public override ResourceOptions GetDefaultOptions () { - return new PazOptions { Scheme = GetScheme (Properties.Settings.Default.PAZTitle) }; + return new PazOptions { + Scheme = GetScheme (Properties.Settings.Default.PAZTitle), + ArcName = Properties.Settings.Default.PAZArchiveKey, + CompressContents = Properties.Settings.Default.PAZCompressContents, + RetainDirs = Properties.Settings.Default.PAZRetainStructure, + Signature = 0, + }; + } + + public override ResourceOptions GetOptions (object widget) + { + var create = widget as GUI.CreatePAZWidget; + if (null == create) + return base.GetDefaultOptions (); + + string title = create.SelectedTitle ?? string.Empty; + string archive = create.SelectedArchive ?? string.Empty; + bool compress = create.CompressContents; + bool retain = create.RetainStructure; + + Properties.Settings.Default.PAZTitle = title; + Properties.Settings.Default.PAZArchiveKey = archive; + Properties.Settings.Default.PAZCompressContents = compress; + Properties.Settings.Default.PAZRetainStructure = retain; + + var options = new PazOptions { + Scheme = GetScheme (title), + ArcName = archive, + CompressContents = compress, + RetainDirs = retain, + Signature = 0, + }; + return options; + } + + public override object GetCreationWidget () + { + return new GUI.CreatePAZWidget (this); } public override object GetAccessWidget () @@ -327,6 +365,312 @@ namespace GameRes.Formats.Musica return new GUI.WidgetPAZ (this); } + public override void Create (Stream output, IEnumerable list, ResourceOptions options, + EntryCallback callback) + { + if (null == output) + throw new ArgumentNullException ("output"); + + var paz_options = GetOptions (options); + var scheme = paz_options.Scheme; + string arc_name = paz_options.ArcName ?? string.Empty; + PazKey keys = null; + + bool use_crypto = scheme != null; + if (use_crypto) + { + if (string.IsNullOrEmpty (arc_name)) + throw new InvalidOperationException ("Archive key is not specified."); + arc_name = arc_name.ToLowerInvariant (); + if (null == scheme.ArcKeys || !scheme.ArcKeys.TryGetValue (arc_name, out keys)) + throw new InvalidOperationException (string.Format ("Unknown archive key '{0}'.", arc_name)); + if (VideoPazNames.Contains (arc_name)) + throw new NotSupportedException ("Creation of MOV PAZ archives is not supported."); + } + else + { + arc_name = string.Empty; + } + + bool is_audio = use_crypto && AudioPazNames.Contains (arc_name); + bool retain_dirs = paz_options.RetainDirs; + bool compress_contents = paz_options.CompressContents; + byte xor_key = 0; + + var encoding = Encodings.cp932.WithFatalFallback (); + var data_key = use_crypto ? keys.DataKey : null; + var index_key = use_crypto ? keys.IndexKey : null; + int version = scheme != null ? scheme.Version : 0; + + var entries = new List(); + var used_names = new HashSet (StringComparer.OrdinalIgnoreCase); + string temp_path = Path.GetTempFileName (); + long data_offset = 0; + int callback_index = 0; + + try + { + using (var data_stream = new FileStream (temp_path, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) + { + foreach (var entry in list) + { + string stored_name = retain_dirs ? entry.Name.Replace ("\\", "/") : Path.GetFileName (entry.Name); + if (string.IsNullOrWhiteSpace (stored_name)) + continue; + if (!used_names.Add (stored_name)) + continue; + + if (null != callback) + callback (callback_index++, entry, arcStrings.MsgAddingFile); + + var created = CreateEntryData (entry, stored_name, data_stream, ref data_offset, + compress_contents, is_audio, scheme, data_key, xor_key, encoding); + entries.Add (created); + } + } + + if (null != callback) + callback (callback_index++, null, arcStrings.MsgWritingIndex); + + long plain_index_size = CalculateIndexPlainSize (entries); + int aligned_index_size = (int)((plain_index_size + 7) & ~7); + uint index_size = (uint)aligned_index_size; + uint start_offset = version > 0 ? 0x20u : 0u; + long data_start = start_offset + 4 + aligned_index_size; + + foreach (var item in entries) + item.Offset = data_start + item.DataOffset; + + var index_buffer = BuildIndexBuffer (entries, aligned_index_size, index_key, xor_key); + uint stored_index_size = EncodeIndexSize (index_size, xor_key); + + uint signature = version > 0 ? ResolveSignature (scheme, paz_options.Signature) : 0u; + + output.Position = 0; + using (var writer = new BinaryWriter (output, Encoding.ASCII, true)) + { + if (start_offset != 0) + { + writer.Write (signature); + if (start_offset > 4) + writer.Write (new byte[start_offset - 4]); + } + writer.Write (stored_index_size); + writer.Write (index_buffer); + } + + output.Position = data_start; + using (var data_stream = new FileStream (temp_path, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + data_stream.CopyTo (output); + } + } + finally + { + try { File.Delete (temp_path); } catch { } + } + } + + static long CalculateIndexPlainSize (IList entries) + { + long size = 4; + foreach (var entry in entries) + size += entry.NameBytes.Length + 1 + 8 + 4 + 4 + 4 + 4; + return size; + } + + byte[] BuildIndexBuffer (IList entries, int aligned_size, byte[] index_key, byte xor_key) + { + var index_stream = new MemoryStream (Math.Max (aligned_size, 8)); + using (var writer = new BinaryWriter (index_stream, Encoding.ASCII, true)) + { + writer.Write (entries.Count); + foreach (var entry in entries) + { + writer.Write (entry.NameBytes); + writer.Write ((byte)0); + writer.Write (entry.Offset); + writer.Write (entry.UnpackedSize); + writer.Write (entry.Size); + writer.Write (entry.AlignedSize); + writer.Write (entry.IsPacked ? 1 : 0); + } + writer.Flush (); + } + if (index_stream.Length < aligned_size) + index_stream.SetLength (aligned_size); + var buffer = index_stream.ToArray (); + if (buffer.Length > 0 && null != index_key) + { + var crypto = new Blowfish (index_key); + crypto.Encipher (buffer, buffer.Length); + } + if (xor_key != 0) + { + for (int i = 0; i < buffer.Length; ++i) + buffer[i] ^= xor_key; + } + return buffer; + } + + static uint EncodeIndexSize (uint index_size, byte xor_key) + { + if (0 == xor_key) + return index_size; + uint mask = (uint)(xor_key << 24 | xor_key << 16 | xor_key << 8 | xor_key); + return index_size ^ mask; + } + + uint ResolveSignature (PazScheme scheme, uint explicit_value) + { + if (explicit_value != 0) + return explicit_value; + foreach (var pair in KnownSchemes) + { + if (object.ReferenceEquals (pair.Value, scheme)) + return pair.Key; + } + return 0u; + } + + PazCreateEntry CreateEntryData (Entry entry, string stored_name, FileStream data_stream, ref long data_offset, + bool compress_contents, bool is_audio, PazScheme scheme, byte[] data_key, + byte xor_key, Encoding encoding) + { + string file_path = entry.Name; + using (var input = File.Open (file_path, FileMode.Open, FileAccess.Read)) + { + long file_length = input.Length; + if (file_length < 0 || file_length > uint.MaxValue) + throw new FileSizeException (entry.Name); + if (file_length > int.MaxValue) + throw new FileSizeException (entry.Name); + + var raw = new byte[file_length]; + int read = 0; + while (read < raw.Length) + { + int chunk = input.Read (raw, read, raw.Length - read); + if (0 == chunk) + break; + read += chunk; + } + if (read != raw.Length) + throw new EndOfStreamException (entry.Name); + + bool should_compress = compress_contents && ShouldCompressFile (entry) && raw.Length > 0; + byte[] payload = raw; + bool is_packed = false; + if (should_compress) + { + using (var packed_stream = new MemoryStream (raw.Length)) + { + using (var zstream = new ZLibStream (packed_stream, CompressionMode.Compress, CompressionLevel.Level9, true)) + { + zstream.Write (raw, 0, raw.Length); + } + var packed = packed_stream.ToArray (); + if (packed.Length < raw.Length) + { + payload = packed; + is_packed = true; + } + } + } + + var info = new PazCreateEntry (); + info.Name = stored_name; + try + { + info.NameBytes = encoding.GetBytes (stored_name); + } + catch (EncoderFallbackException X) + { + throw new InvalidFileName (stored_name, arcStrings.MsgIllegalCharacters, X); + } + + info.UnpackedSize = (uint)raw.Length; + info.Size = (uint)payload.Length; + info.IsPacked = is_packed; + + byte[] aligned; + if (0 == (payload.Length & 7)) + { + aligned = (byte[])payload.Clone (); + } + else + { + int aligned_size = (payload.Length + 7) & ~7; + aligned = new byte[aligned_size]; + Buffer.BlockCopy (payload, 0, aligned, 0, payload.Length); + } + + int version = scheme != null ? scheme.Version : 0; + if (version > 0) + { + string password = string.Empty; + if (!is_packed && scheme != null && scheme.TypeKeys != null) + password = scheme.GetTypePassword (stored_name, is_audio); + if (!string.IsNullOrEmpty (password)) + { + string key_string = string.Format ("{0} {1:X08} {2}", stored_name.ToLowerInvariant(), info.UnpackedSize, password); + var key_bytes = Encodings.cp932.GetBytes (key_string); + var rc4 = new Rc4Transform (key_bytes); + if (version >= 2) + { + uint crc = Crc32.Compute (key_bytes, 0, key_bytes.Length); + int skip = (int)(crc >> 12) & 0xFF; + for (int i = 0; i < skip; ++i) + rc4.NextByte (); + } + rc4.TransformBlock (aligned, 0, payload.Length, aligned, 0); + } + } + + if (aligned.Length > 0 && null != data_key) + { + var crypto = new Blowfish (data_key); + crypto.Encipher (aligned, aligned.Length); + } + + if (xor_key != 0) + { + for (int i = 0; i < aligned.Length; ++i) + aligned[i] ^= xor_key; + } + + info.AlignedSize = (uint)aligned.Length; + info.DataOffset = data_offset; + + data_stream.Position = data_offset; + data_stream.Write (aligned, 0, aligned.Length); + data_offset += aligned.Length; + + return info; + } + } + + bool ShouldCompressFile (Entry entry) + { + if ("image" == entry.Type || "archive" == entry.Type) + return false; + if (entry.Name.HasExtension (".ogg")) + return false; + return true; + } + + class PazCreateEntry + { + public string Name; + public byte[] NameBytes; + public long DataOffset; + public long Offset; + public uint UnpackedSize; + public uint Size; + public uint AlignedSize; + public bool IsPacked; + } + PazScheme GetScheme (string title) { PazScheme scheme; @@ -395,5 +739,9 @@ namespace GameRes.Formats.Musica public class PazOptions : ResourceOptions { public PazScheme Scheme; + public string ArcName; + public bool CompressContents; + public bool RetainDirs; + public uint Signature; } } diff --git a/ArcFormats/Musica/CreatePAZWidget.xaml b/ArcFormats/Musica/CreatePAZWidget.xaml new file mode 100644 index 00000000..59be7b2a --- /dev/null +++ b/ArcFormats/Musica/CreatePAZWidget.xaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/ArcFormats/Musica/CreatePAZWidget.xaml.cs b/ArcFormats/Musica/CreatePAZWidget.xaml.cs new file mode 100644 index 00000000..a9d8f5c8 --- /dev/null +++ b/ArcFormats/Musica/CreatePAZWidget.xaml.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Linq; +using System.Windows.Controls; +using GameRes.Formats.Musica; +using GameRes.Formats.Properties; +using GameRes.Formats.Strings; + +namespace GameRes.Formats.GUI +{ + /// + /// Interaction logic for CreatePAZWidget.xaml + /// + public partial class CreatePAZWidget : Grid + { + readonly PazOpener m_paz; + + public CreatePAZWidget (PazOpener paz) + { + m_paz = paz; + InitializeComponent (); + Title.SelectionChanged += TitleSelectionChanged; + PopulateTitles (); + UpdateArchiveList (); + } + + void PopulateTitles () + { + var titles = new List { arcStrings.ArcNoEncryption }; + titles.AddRange (m_paz.KnownTitles.Keys.OrderBy (x => x)); + Title.ItemsSource = titles; + string saved_title = Settings.Default.PAZTitle; + if (!string.IsNullOrEmpty (saved_title) && titles.Contains (saved_title)) + Title.SelectedValue = saved_title; + else if (titles.Count > 0) + Title.SelectedIndex = 0; + } + + void TitleSelectionChanged (object sender, SelectionChangedEventArgs e) + { + UpdateArchiveList (); + } + + void UpdateArchiveList () + { + string title = Title.SelectedValue as string; + PazScheme scheme = null; + if (!string.IsNullOrEmpty (title)) + m_paz.KnownTitles.TryGetValue (title, out scheme); + + List arc_keys = (scheme != null && scheme.ArcKeys != null) + ? scheme.ArcKeys.Keys.OrderBy (x => x).ToList () + : new List (); + + Archive.ItemsSource = arc_keys; + string saved_key = Settings.Default.PAZArchiveKey; + if (!string.IsNullOrEmpty (saved_key) && arc_keys.Contains (saved_key)) + Archive.SelectedValue = saved_key; + else if (arc_keys.Count > 0) + Archive.SelectedIndex = 0; + else + Archive.Text = scheme != null ? (saved_key ?? string.Empty) : string.Empty; + + Archive.IsEnabled = scheme != null; + } + + public string SelectedTitle + { + get { return Title.SelectedValue as string ?? Title.Text; } + } + + public string SelectedArchive + { + get + { + if (!Archive.IsEnabled) + return string.Empty; + var selected = Archive.SelectedValue as string; + if (!string.IsNullOrEmpty (selected)) + return selected; + return Archive.Text ?? string.Empty; + } + } + + public bool CompressContents + { + get { return Compress.IsChecked ?? false; } + } + + public bool RetainStructure + { + get { return Retain.IsChecked ?? false; } + } + } +} diff --git a/ArcFormats/Properties/Settings.Designer.cs b/ArcFormats/Properties/Settings.Designer.cs index 7cae9633..11c77d6d 100644 --- a/ArcFormats/Properties/Settings.Designer.cs +++ b/ArcFormats/Properties/Settings.Designer.cs @@ -637,14 +637,50 @@ namespace GameRes.Formats.Properties { [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Configuration.DefaultSettingValueAttribute("")] - public string PAZTitle { - get { - return ((string)(this["PAZTitle"])); - } - set { - this["PAZTitle"] = value; - } - } + public string PAZTitle { + get { + return ((string)(this["PAZTitle"])); + } + set { + this["PAZTitle"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string PAZArchiveKey { + get { + return ((string)(this["PAZArchiveKey"])); + } + set { + this["PAZArchiveKey"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool PAZCompressContents { + get { + return ((bool)(this["PAZCompressContents"])); + } + set { + this["PAZCompressContents"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool PAZRetainStructure { + get { + return ((bool)(this["PAZRetainStructure"])); + } + set { + this["PAZRetainStructure"] = value; + } + } [global::System.Configuration.UserScopedSettingAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] diff --git a/ArcFormats/Properties/Settings.settings b/ArcFormats/Properties/Settings.settings index 4b4c6753..65249a40 100644 --- a/ArcFormats/Properties/Settings.settings +++ b/ArcFormats/Properties/Settings.settings @@ -155,9 +155,18 @@ - - - + + + + + + + + False + + + True + @@ -201,4 +210,4 @@ 932 - \ No newline at end of file + diff --git a/ArcFormats/app.config b/ArcFormats/app.config index 9d9ed542..4b66151b 100644 --- a/ArcFormats/app.config +++ b/ArcFormats/app.config @@ -157,9 +157,18 @@ - - - + + + + + + + + False + + + True +