implemented XP3 archive creation.

This commit is contained in:
morkt 2014-07-26 23:13:17 +04:00
parent bedba61ff5
commit b72d9194e8
10 changed files with 495 additions and 21 deletions

View File

@ -62,6 +62,9 @@
<Compile Include="ArcXP3.cs" />
<Compile Include="ArcYPF.cs" />
<Compile Include="Blowfish.cs" />
<Compile Include="CreateXP3Widget.xaml.cs">
<DependentUpon>CreateXP3Widget.xaml</DependentUpon>
</Compile>
<Compile Include="ImageHG3.cs" />
<Compile Include="ImageTLG.cs" />
<Compile Include="ImageWCG.cs" />
@ -115,6 +118,10 @@
</None>
</ItemGroup>
<ItemGroup>
<Page Include="CreateXP3Widget.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="WidgetINT.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>

View File

@ -10,6 +10,7 @@ using System.Text;
using System.Linq;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Runtime.InteropServices;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Diagnostics;
@ -47,7 +48,8 @@ namespace GameRes.Formats.KiriKiri
public override string Tag { get { return "XP3"; } }
public override string Description { get { return arcStrings.XP3Description; } }
public override uint Signature { get { return 0x0d335058; } }
public override bool IsHierarchic { get { return false; } }
public override bool IsHierarchic { get { return true; } }
public override bool CanCreate { get { return true; } }
private static readonly ICrypt NoCryptAlgorithm = new NoCrypt();
@ -73,6 +75,9 @@ namespace GameRes.Formats.KiriKiri
return null;
dir_offset = file.View.ReadInt64 (0x20);
}
if (dir_offset >= file.MaxOffset)
return null;
int header_type = file.View.ReadByte (dir_offset);
if (0 != header_type && 1 != header_type)
return null;
@ -193,7 +198,13 @@ namespace GameRes.Formats.KiriKiri
header.BaseStream.Position = next_section_pos;
}
if (!string.IsNullOrEmpty (entry.Name) && entry.Segments.Any())
{
dir.Add (entry);
Trace.WriteLine (string.Format ("{0,-16} {3:X8} {1,11} {2,12}", entry.Name,
entry.IsEncrypted ? "[encrypted]" : "",
entry.Segments.First().IsCompressed ? "[compressed]" : "",
entry.Hash));
}
NextEntry:
header.BaseStream.Position = dir_offset;
}
@ -204,7 +215,7 @@ NextEntry:
public override Stream OpenEntry (ArcFile arc, Entry entry)
{
var xp3_entry = entry as Xp3Entry;
if (null == xp3_entry || !xp3_entry.Segments.Any())
if (null == xp3_entry)
return arc.File.CreateStream (entry.Offset, entry.Size);
if (1 == xp3_entry.Segments.Count && !xp3_entry.IsEncrypted)
{
@ -218,11 +229,16 @@ NextEntry:
return new Xp3Stream (arc.File, xp3_entry);
}
string m_scheme = GetDefaultScheme();
public override ResourceOptions GetOptions ()
{
return new ResourceOptions {
Widget = new GUI.CreateXP3Widget()
};
}
ICrypt QueryCryptAlgorithm ()
{
var widget = new GUI.WidgetXP3 (m_scheme);
var widget = new GUI.WidgetXP3();
var args = new ParametersRequestEventArgs
{
Notice = arcStrings.ArcEncryptedNotice,
@ -232,26 +248,17 @@ NextEntry:
if (!args.InputResult)
throw new OperationCanceledException();
m_scheme = widget.GetScheme();
if (null != m_scheme)
{
ICrypt algorithm;
if (KnownSchemes.TryGetValue (m_scheme, out algorithm))
{
Settings.Default.XP3Scheme = m_scheme;
return algorithm;
}
}
return NoCryptAlgorithm;
return widget.GetScheme();
}
public static string GetDefaultScheme ()
public static ICrypt GetScheme (string scheme)
{
string scheme = Settings.Default.XP3Scheme;
if (!string.IsNullOrEmpty (scheme) && KnownSchemes.ContainsKey (scheme))
return scheme;
else
return arcStrings.ArcNoEncryption;
ICrypt algorithm = NoCryptAlgorithm;
if (!string.IsNullOrEmpty (scheme))
{
KnownSchemes.TryGetValue (scheme, out algorithm);
}
return algorithm;
}
static uint GetFileCheckSum (Stream src)
@ -268,6 +275,263 @@ NextEntry:
}
return sum.Value;
}
static readonly byte[] s_xp3_header = {
(byte)'X', (byte)'P', (byte)'3', 0x0d, 0x0a, 0x20, 0x0a, 0x1a, 0x8b, 0x67, 0x01
};
public override void Create (Stream output, IEnumerable<Entry> list, ResourceOptions options)
{
int version = Settings.Default.XP3Version;
ICrypt scheme = NoCryptAlgorithm;
bool compress_header = Settings.Default.XP3CompressHeader;
bool compress_contents = Settings.Default.XP3CompressContents;
bool retain_dirs = Settings.Default.XP3RetainStructure;
var widget = options.Widget as GUI.CreateXP3Widget;
if (null != widget)
{
scheme = widget.EncryptionWidget.GetScheme();
}
bool use_encryption = scheme != NoCryptAlgorithm;
using (var writer = new BinaryWriter (output, Encoding.ASCII, true))
{
writer.Write (s_xp3_header);
if (2 == version)
{
writer.Write ((long)0x17);
writer.Write ((int)1);
writer.Write ((byte)0x80);
writer.Write ((long)0);
}
long index_pos_offset = writer.BaseStream.Position;
writer.BaseStream.Seek (8, SeekOrigin.Current);
var used_names = new HashSet<string>();
var dir = new List<Xp3Entry>();
long current_offset = writer.BaseStream.Position;
foreach (var entry in list)
{
string name = entry.Name;
if (!retain_dirs)
name = Path.GetFileName (name);
else
name = name.Replace (@"\", "/");
if (!used_names.Add (name))
{
Trace.WriteLine ("duplicate name", entry.Name);
continue;
}
var xp3entry = new Xp3Entry {
Name = name,
IsEncrypted = use_encryption,
Cipher = scheme,
};
bool compress = compress_contents && ShouldCompressFile (entry);
if (!use_encryption)
RawFileCopy (entry.Name, xp3entry, output, compress);
else
EncryptedFileCopy (entry.Name, xp3entry, output, compress);
dir.Add (xp3entry);
}
long index_pos = writer.BaseStream.Position;
writer.BaseStream.Position = index_pos_offset;
writer.Write (index_pos);
writer.BaseStream.Position = index_pos;
using (var header = new BinaryWriter (new MemoryStream (dir.Count*0x58), Encoding.Unicode))
{
long dir_pos = 0;
foreach (var entry in dir)
{
header.BaseStream.Position = dir_pos;
header.Write ((uint)0x656c6946); // "File"
long header_size_pos = header.BaseStream.Position;
header.Write ((long)0);
header.Write ((uint)0x6f666e69); // "info"
header.Write ((long)(4+8+8+2 + entry.Name.Length*2));
header.Write ((uint)(use_encryption ? 0x80000000 : 0));
header.Write ((long)entry.UnpackedSize);
header.Write ((long)entry.Size);
header.Write ((short)entry.Name.Length);
foreach (char c in entry.Name)
header.Write (c);
header.Write ((uint)0x6d676573); // "segm"
header.Write ((long)0x1c);
var segment = entry.Segments.First();
header.Write ((int)(segment.IsCompressed ? 1 : 0));
header.Write ((long)segment.Offset);
header.Write ((long)segment.Size);
header.Write ((long)segment.PackedSize);
header.Write ((uint)0x726c6461); // "adlr"
header.Write ((long)4);
header.Write ((uint)entry.Hash);
dir_pos = header.BaseStream.Position;
long header_size = dir_pos - header_size_pos - 8;
header.BaseStream.Position = header_size_pos;
header.Write (header_size);
}
header.BaseStream.Position = 0;
writer.Write ((byte)(compress_header ? 1 : 0));
long unpacked_dir_size = header.BaseStream.Length;
if (compress_header)
{
long packed_dir_size_pos = writer.BaseStream.Position;
writer.Write ((long)0);
writer.Write (unpacked_dir_size);
long dir_start = writer.BaseStream.Position;
using (var zstream = new ZLibStream (writer.BaseStream, CompressionMode.Compress, true))
header.BaseStream.CopyTo (zstream);
long packed_dir_size = writer.BaseStream.Position - dir_start;
writer.BaseStream.Position = packed_dir_size_pos;
writer.Write (packed_dir_size);
}
else
{
writer.Write (unpacked_dir_size);
header.BaseStream.CopyTo (writer.BaseStream);
}
}
}
output.Seek (0, SeekOrigin.End);
}
void RawFileCopy (string name, Xp3Entry xp3entry, Stream output, bool compress)
{
using (var file = File.Open (name, FileMode.Open, FileAccess.Read))
{
if (file.Length > uint.MaxValue)
throw new FileSizeException();
uint unpacked_size = (uint)file.Length;
xp3entry.UnpackedSize = (uint)unpacked_size;
xp3entry.Size = (uint)unpacked_size;
var segment = new Xp3Segment {
IsCompressed = compress,
Offset = output.Position,
Size = unpacked_size,
PackedSize = unpacked_size
};
if (compress)
{
using (var zstream = new ZLibStream (output, CompressionMode.Compress, true))
{
xp3entry.Hash = CheckedCopy (file, zstream);
zstream.Flush();
segment.PackedSize = (uint)zstream.TotalOut;
}
}
else
{
xp3entry.Hash = CheckedCopy (file, output);
}
xp3entry.Segments.Add (segment);
}
}
void EncryptedFileCopy (string name, Xp3Entry xp3entry, Stream output, bool compress)
{
var file = File.Open (name, FileMode.Open, FileAccess.Read);
using (var map = MemoryMappedFile.CreateFromFile (file, null, 0,
MemoryMappedFileAccess.Read, null, HandleInheritability.None, false))
{
if (file.Length > int.MaxValue)
throw new FileSizeException();
uint unpacked_size = (uint)file.Length;
xp3entry.UnpackedSize = (uint)unpacked_size;
xp3entry.Size = (uint)unpacked_size;
using (var view = map.CreateViewAccessor (0, unpacked_size, MemoryMappedFileAccess.Read))
{
var segment = new Xp3Segment {
IsCompressed = compress,
Offset = output.Position,
Size = unpacked_size,
PackedSize = unpacked_size,
};
xp3entry.Segments.Add (segment);
bool need_output_dispose = false;
if (compress)
{
output = new ZLibStream (output, CompressionMode.Compress, true);
need_output_dispose = true;
}
unsafe
{
byte[] read_buffer = new byte[81920];
byte* ptr = view.GetPointer (0);
try
{
var checksum = new Adler32();
if (!xp3entry.Cipher.HashAfterCrypt)
xp3entry.Hash = checksum.Update (ptr, (int)unpacked_size);
int offset = 0;
int remaining = (int)unpacked_size;
while (remaining > 0)
{
int amount = Math.Min (remaining, read_buffer.Length);
remaining -= amount;
Marshal.Copy ((IntPtr)(ptr+offset), read_buffer, 0, amount);
xp3entry.Cipher.Encrypt (xp3entry, offset, read_buffer, 0, amount);
if (xp3entry.Cipher.HashAfterCrypt)
checksum.Update (read_buffer, 0, amount);
output.Write (read_buffer, 0, amount);
offset += amount;
}
if (xp3entry.Cipher.HashAfterCrypt)
xp3entry.Hash = checksum.Value;
if (compress)
{
output.Flush();
segment.PackedSize = (uint)(output as ZLibStream).TotalOut;
}
}
finally
{
view.SafeMemoryMappedViewHandle.ReleasePointer();
if (need_output_dispose)
output.Dispose();
}
}
}
}
}
uint CheckedCopy (Stream src, Stream dst)
{
var checksum = new Adler32();
var read_buffer = new byte[81920];
for (;;)
{
int read = src.Read (read_buffer, 0, read_buffer.Length);
if (0 == read)
break;
checksum.Update (read_buffer, 0, read);
dst.Write (read_buffer, 0, read);
}
return checksum.Value;
}
bool ShouldCompressFile (Entry entry)
{
if ("image" == entry.Type || "archive" == entry.Type)
return false;
var ext = Path.GetExtension (entry.Name);
if (!string.IsNullOrEmpty (ext) && ext.Equals (".ogg", StringComparison.OrdinalIgnoreCase))
return false;
return true;
}
}
public class Xp3Stream : Stream
@ -392,6 +656,11 @@ NextEntry:
public abstract class ICrypt
{
/// <summary>
/// whether Adler32 checksum should be calculated after contents have been encrypted.
/// </summary>
public virtual bool HashAfterCrypt { get { return false; } }
public abstract byte Decrypt (Xp3Entry entry, long offset, byte value);
public virtual void Decrypt (Xp3Entry entry, long offset, byte[] values, int pos, int count)
@ -424,6 +693,8 @@ NextEntry:
internal class FateCrypt : ICrypt
{
public override bool HashAfterCrypt { get { return true; } }
public override byte Decrypt (Xp3Entry entry, long offset, byte value)
{
byte result = (byte)(value ^ 0x36);

View File

@ -0,0 +1,33 @@
<Grid x:Class="GameRes.Formats.GUI.CreateXP3Widget"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:GameRes.Formats.Strings"
xmlns:p="clr-namespace:GameRes.Formats.Properties"
xmlns:local="clr-namespace:GameRes.Formats.GUI">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<StackPanel Orientation="Vertical" Margin="0,0,10,5">
<Label Content="{x:Static s:arcStrings.XP3LabelVersion}" Target="{Binding ElementName=Version}" Padding="4,0,0,5"/>
<ComboBox Name="Version" Width="80" HorizontalAlignment="Left" SelectedValuePath="Content"
SelectedValue="{Binding Source={x:Static p:Settings.Default}, Path=XP3Version, Mode=TwoWay}">
<ComboBoxItem Content="1"/>
<ComboBoxItem Content="2"/>
</ComboBox>
</StackPanel>
<StackPanel Orientation="Vertical" Margin="0,0,0,5">
<Label Content="{x:Static s:arcStrings.XP3LabelScheme}" Target="{Binding ElementName=EncryptionWidget}" Padding="4,0,0,5"/>
<local:WidgetXP3 x:Name="EncryptionWidget"/>
</StackPanel>
</StackPanel>
<CheckBox Name="CompressHeader" Content="{x:Static s:arcStrings.XP3CompressHeader}" Grid.Row="1" Margin="0,5,0,5"
IsChecked="{Binding Source={x:Static p:Settings.Default}, Path=XP3CompressHeader, Mode=TwoWay}"/>
<CheckBox Name="CompressContents" Content="{x:Static s:arcStrings.XP3CompressContents}" Grid.Row="2" Margin="0,0,0,5"
IsChecked="{Binding Source={x:Static p:Settings.Default}, Path=XP3CompressContents, Mode=TwoWay}"/>
<CheckBox Name="RetainStructure" Content="{x:Static s:arcStrings.XP3RetainStructure}" Grid.Row="3" Margin="0,0,0,0"
IsChecked="{Binding Source={x:Static p:Settings.Default}, Path=XP3RetainStructure, Mode=TwoWay}"/>
</Grid>

View File

@ -0,0 +1,16 @@
using System.Windows;
using System.Windows.Controls;
namespace GameRes.Formats.GUI
{
/// <summary>
/// Interaction logic for CreateXP3Widget.xaml
/// </summary>
public partial class CreateXP3Widget : Grid
{
public CreateXP3Widget ()
{
InitializeComponent ();
}
}
}

View File

@ -69,5 +69,53 @@ namespace GameRes.Formats.Properties {
this["INTEncryption"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("True")]
public bool XP3CompressHeader {
get {
return ((bool)(this["XP3CompressHeader"]));
}
set {
this["XP3CompressHeader"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool XP3CompressContents {
get {
return ((bool)(this["XP3CompressContents"]));
}
set {
this["XP3CompressContents"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("2")]
public int XP3Version {
get {
return ((int)(this["XP3Version"]));
}
set {
this["XP3Version"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("False")]
public bool XP3RetainStructure {
get {
return ((bool)(this["XP3RetainStructure"]));
}
set {
this["XP3RetainStructure"] = value;
}
}
}
}

View File

@ -14,5 +14,17 @@
<Setting Name="INTEncryption" Type="GameRes.Formats.IntEncryptionInfo" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="XP3CompressHeader" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">True</Value>
</Setting>
<Setting Name="XP3CompressContents" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
<Setting Name="XP3Version" Type="System.Int32" Scope="User">
<Value Profile="(Default)">2</Value>
</Setting>
<Setting Name="XP3RetainStructure" Type="System.Boolean" Scope="User">
<Value Profile="(Default)">False</Value>
</Setting>
</Settings>
</SettingsFile>

View File

@ -225,6 +225,24 @@ namespace GameRes.Formats.Strings {
}
}
/// <summary>
/// Looks up a localized string similar to Compress contents.
/// </summary>
public static string XP3CompressContents {
get {
return ResourceManager.GetString("XP3CompressContents", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Compress directory.
/// </summary>
public static string XP3CompressHeader {
get {
return ResourceManager.GetString("XP3CompressHeader", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to KiriKiri game engine resource archive.
/// </summary>
@ -234,6 +252,33 @@ namespace GameRes.Formats.Strings {
}
}
/// <summary>
/// Looks up a localized string similar to Encryption scheme.
/// </summary>
public static string XP3LabelScheme {
get {
return ResourceManager.GetString("XP3LabelScheme", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Version.
/// </summary>
public static string XP3LabelVersion {
get {
return ResourceManager.GetString("XP3LabelVersion", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Retain directory structure.
/// </summary>
public static string XP3RetainStructure {
get {
return ResourceManager.GetString("XP3RetainStructure", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Yu-Ris game engine resource archive.
/// </summary>

View File

@ -174,9 +174,24 @@ predefined encryption scheme.</value>
<data name="XFLDescription" xml:space="preserve">
<value>Liar-soft game resource archive</value>
</data>
<data name="XP3CompressContents" xml:space="preserve">
<value>Compress contents</value>
</data>
<data name="XP3CompressHeader" xml:space="preserve">
<value>Compress directory</value>
</data>
<data name="XP3Description" xml:space="preserve">
<value>KiriKiri game engine resource archive</value>
</data>
<data name="XP3LabelScheme" xml:space="preserve">
<value>Encryption scheme</value>
</data>
<data name="XP3LabelVersion" xml:space="preserve">
<value>Version</value>
</data>
<data name="XP3RetainStructure" xml:space="preserve">
<value>Retain directory structure</value>
</data>
<data name="YPFDescription" xml:space="preserve">
<value>Yu-Ris game engine resource archive</value>
</data>

View File

@ -147,6 +147,21 @@
<data name="MsgIllegalCharacters" xml:space="preserve">
<value>Имя файла содержит недопустимые символы</value>
</data>
<data name="XP3CompressContents" xml:space="preserve">
<value>Сжать содержимое</value>
</data>
<data name="XP3CompressHeader" xml:space="preserve">
<value>Сжать оглавление</value>
</data>
<data name="XP3LabelScheme" xml:space="preserve">
<value>Способ шифрования</value>
</data>
<data name="XP3LabelVersion" xml:space="preserve">
<value>Версия</value>
</data>
<data name="XP3RetainStructure" xml:space="preserve">
<value>Сохранить структуру папок</value>
</data>
<data name="YPFLabelKey" xml:space="preserve">
<value>8-битный ключ шифрования</value>
</data>

View File

@ -16,6 +16,18 @@
<setting name="YPFKey" serializeAs="String">
<value>4294967295</value>
</setting>
<setting name="XP3CompressHeader" serializeAs="String">
<value>True</value>
</setting>
<setting name="XP3CompressContents" serializeAs="String">
<value>False</value>
</setting>
<setting name="XP3Version" serializeAs="String">
<value>2</value>
</setting>
<setting name="XP3RetainStructure" serializeAs="String">
<value>False</value>
</setting>
</GameRes.Formats.Properties.Settings>
</userSettings>
</configuration>