2014-07-25 04:53:32 +08:00
|
|
|
//! \file ArcSteinsGate.cs
|
|
|
|
//! \date Thu Jul 24 23:36:01 2014
|
|
|
|
//! \brief Nitro+ Steins;Gate archive implementation.
|
|
|
|
//
|
2014-07-29 11:02:49 +08:00
|
|
|
// Copyright (C) 2014 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.
|
|
|
|
//
|
2014-07-25 04:53:32 +08:00
|
|
|
|
|
|
|
using System.IO;
|
|
|
|
using System.Text;
|
2014-07-29 11:02:49 +08:00
|
|
|
using System.Linq;
|
2014-07-25 04:53:32 +08:00
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.ComponentModel.Composition;
|
|
|
|
using GameRes.Formats.Strings;
|
|
|
|
using GameRes.Utility;
|
2014-07-29 11:02:49 +08:00
|
|
|
using GameRes.Formats.Properties;
|
2014-07-25 04:53:32 +08:00
|
|
|
|
|
|
|
namespace GameRes.Formats
|
|
|
|
{
|
2014-07-29 11:02:49 +08:00
|
|
|
public class SteinsGateOptions : ResourceOptions
|
|
|
|
{
|
|
|
|
public Encoding FileNameEncoding { get; set; }
|
|
|
|
}
|
|
|
|
|
2014-07-25 04:53:32 +08:00
|
|
|
[Export(typeof(ArchiveFormat))]
|
|
|
|
public class NpaSteinsGateOpener : ArchiveFormat
|
|
|
|
{
|
2014-07-25 10:21:07 +08:00
|
|
|
public override string Tag { get { return "NPA-SG"; } }
|
2014-07-25 04:53:32 +08:00
|
|
|
public override string Description { get { return arcStrings.NPASteinsGateDescription; } }
|
|
|
|
public override uint Signature { get { return 0; } }
|
|
|
|
public override bool IsHierarchic { get { return true; } }
|
2014-07-29 11:02:49 +08:00
|
|
|
public override bool CanCreate { get { return true; } }
|
2014-07-25 04:53:32 +08:00
|
|
|
|
2014-07-25 10:21:07 +08:00
|
|
|
public NpaSteinsGateOpener ()
|
|
|
|
{
|
|
|
|
Extensions = new string[] { "npa" };
|
|
|
|
}
|
|
|
|
|
2014-07-25 04:53:32 +08:00
|
|
|
internal static readonly byte[] KeyString = {
|
|
|
|
'B'^0xff, 'U'^0xff, 'C'^0xff, 'K'^0xff,
|
|
|
|
'T'^0xff, 'I'^0xff, 'C'^0xff, 'K'^0xff
|
|
|
|
};
|
|
|
|
|
|
|
|
public override ArcFile TryOpen (ArcView file)
|
|
|
|
{
|
|
|
|
int index_size = file.View.ReadInt32 (0);
|
|
|
|
if (index_size < 0x14 || index_size >= file.MaxOffset || index_size > 0xffffff)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
var stream = new SteinsGateEncryptedStream (file, 4, (uint)index_size);
|
2014-07-29 11:02:49 +08:00
|
|
|
using (var header = new BinaryReader (stream))
|
2014-07-25 04:53:32 +08:00
|
|
|
{
|
|
|
|
int entry_count = header.ReadInt32();
|
|
|
|
if (entry_count <= 0)
|
|
|
|
return null;
|
2014-07-29 11:02:49 +08:00
|
|
|
index_size -= 4;
|
2014-07-25 04:53:32 +08:00
|
|
|
int average_entry_size = index_size / entry_count;
|
|
|
|
if (average_entry_size < 0x11)
|
|
|
|
return null;
|
|
|
|
|
2014-07-29 11:02:49 +08:00
|
|
|
var dir = new List<Entry> (entry_count);
|
|
|
|
for (int i = 0; i < entry_count; ++i)
|
2014-07-25 04:53:32 +08:00
|
|
|
{
|
|
|
|
int name_length = header.ReadInt32();
|
|
|
|
if (name_length+0x10 > index_size)
|
|
|
|
return null;
|
2014-07-29 11:02:49 +08:00
|
|
|
byte[] name_raw = header.ReadBytes (name_length);
|
|
|
|
Encoding enc = GuessEncoding (name_raw);
|
|
|
|
string filename = enc.GetString (name_raw);
|
2014-07-25 04:53:32 +08:00
|
|
|
|
|
|
|
var entry = FormatCatalog.Instance.CreateEntry (filename);
|
|
|
|
entry.Size = header.ReadUInt32();
|
|
|
|
entry.Offset = header.ReadInt64();
|
|
|
|
if (!entry.CheckPlacement (file.MaxOffset))
|
|
|
|
return null;
|
|
|
|
dir.Add (entry);
|
|
|
|
|
|
|
|
index_size -= name_length+0x10;
|
|
|
|
}
|
|
|
|
return new ArcFile (file, this, dir);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public override Stream OpenEntry (ArcFile arc, Entry entry)
|
|
|
|
{
|
|
|
|
return new SteinsGateEncryptedStream (arc.File, entry.Offset, entry.Size);
|
|
|
|
}
|
|
|
|
|
|
|
|
internal void Encrypt (byte[] buffer, int offset, int count)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
|
|
{
|
|
|
|
buffer[offset+i] ^= KeyString[i & 7];
|
|
|
|
}
|
|
|
|
}
|
2014-07-29 11:02:49 +08:00
|
|
|
|
|
|
|
Encoding GuessEncoding (byte[] text)
|
|
|
|
{
|
|
|
|
bool has_zero = false;
|
|
|
|
bool has_non_ascii = false;
|
|
|
|
foreach (var symbol in text)
|
|
|
|
{
|
|
|
|
if (0 == symbol)
|
|
|
|
{
|
|
|
|
has_zero = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
else if (symbol > 0x7f)
|
|
|
|
{
|
|
|
|
has_non_ascii = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (has_zero)
|
|
|
|
return Encoding.Unicode;
|
|
|
|
else if (has_non_ascii)
|
|
|
|
return Encodings.cp932;
|
|
|
|
else
|
|
|
|
return Encoding.ASCII;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override ResourceOptions GetDefaultOptions ()
|
|
|
|
{
|
|
|
|
return new SteinsGateOptions {
|
|
|
|
FileNameEncoding = GetEncoding (Settings.Default.SGFileNameEncoding),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
public override object GetCreationWidget ()
|
|
|
|
{
|
|
|
|
return new GUI.CreateSGWidget();
|
|
|
|
}
|
|
|
|
|
|
|
|
Encoding GetEncoding (string name)
|
|
|
|
{
|
|
|
|
if ("shift-jis" == name)
|
|
|
|
return Encodings.cp932;
|
|
|
|
if ("utf-16" == name)
|
|
|
|
return Encoding.Unicode;
|
|
|
|
return Encoding.Default;
|
|
|
|
}
|
|
|
|
|
|
|
|
internal class RawEntry : Entry
|
|
|
|
{
|
|
|
|
public byte[] IndexName;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void Create (Stream output, IEnumerable<Entry> list, ResourceOptions options,
|
|
|
|
EntryCallback callback)
|
|
|
|
{
|
|
|
|
var sg_options = GetOptions<SteinsGateOptions> (options);
|
2014-07-29 15:50:12 +08:00
|
|
|
Encoding encoding = sg_options.FileNameEncoding.WithFatalFallback();
|
2014-07-29 11:02:49 +08:00
|
|
|
long start_pos = output.Position;
|
|
|
|
int callback_count = 0;
|
|
|
|
|
|
|
|
uint index_size = 4;
|
2014-07-29 15:50:12 +08:00
|
|
|
var real_entry_list = new List<RawEntry> (list.Count());
|
2014-07-29 11:02:49 +08:00
|
|
|
var used_names = new HashSet<string>();
|
|
|
|
foreach (var entry in list)
|
|
|
|
{
|
|
|
|
string name = entry.Name.Replace (@"\", "/");
|
|
|
|
if (!used_names.Add (name)) // duplicate name
|
|
|
|
continue;
|
|
|
|
var header_entry = new RawEntry { Name = entry.Name };
|
|
|
|
try
|
|
|
|
{
|
|
|
|
header_entry.IndexName = encoding.GetBytes (name);
|
|
|
|
}
|
|
|
|
catch (EncoderFallbackException X)
|
|
|
|
{
|
|
|
|
throw new InvalidFileName (entry.Name, arcStrings.MsgIllegalCharacters, X);
|
|
|
|
}
|
|
|
|
index_size += (uint)header_entry.IndexName.Length + 16;
|
|
|
|
real_entry_list.Add (header_entry);
|
|
|
|
}
|
|
|
|
output.Seek (4+index_size, SeekOrigin.Current);
|
|
|
|
foreach (var entry in real_entry_list)
|
|
|
|
{
|
|
|
|
using (var input = File.Open (entry.Name, FileMode.Open, FileAccess.Read))
|
|
|
|
{
|
|
|
|
var file_size = input.Length;
|
|
|
|
if (file_size > uint.MaxValue)
|
|
|
|
throw new FileSizeException();
|
|
|
|
entry.Offset = output.Position;
|
|
|
|
entry.Size = (uint)file_size;
|
|
|
|
if (null != callback)
|
|
|
|
callback (callback_count++, entry, arcStrings.MsgAddingFile);
|
|
|
|
using (var stream = new SteinsGateEncryptedStream (output))
|
|
|
|
input.CopyTo (stream);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (null != callback)
|
|
|
|
callback (callback_count++, null, arcStrings.MsgWritingIndex);
|
|
|
|
output.Position = start_pos;
|
|
|
|
output.WriteByte ((byte)(index_size & 0xff));
|
|
|
|
output.WriteByte ((byte)((index_size >> 8) & 0xff));
|
|
|
|
output.WriteByte ((byte)((index_size >> 16) & 0xff));
|
|
|
|
output.WriteByte ((byte)((index_size >> 24) & 0xff));
|
|
|
|
var encrypted_stream = new SteinsGateEncryptedStream (output);
|
|
|
|
using (var header = new BinaryWriter (encrypted_stream))
|
|
|
|
{
|
2014-07-29 15:50:12 +08:00
|
|
|
header.Write (real_entry_list.Count);
|
2014-07-29 11:02:49 +08:00
|
|
|
foreach (var entry in real_entry_list)
|
|
|
|
{
|
|
|
|
header.Write (entry.IndexName.Length);
|
|
|
|
header.Write (entry.IndexName);
|
|
|
|
header.Write ((uint)entry.Size);
|
|
|
|
header.Write ((long)entry.Offset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-07-25 04:53:32 +08:00
|
|
|
}
|
|
|
|
|
2014-07-29 11:02:49 +08:00
|
|
|
public class SteinsGateEncryptedStream : Stream
|
2014-07-25 04:53:32 +08:00
|
|
|
{
|
2014-07-29 11:02:49 +08:00
|
|
|
private Stream m_stream;
|
|
|
|
private long m_base_pos;
|
|
|
|
private bool m_should_dispose;
|
|
|
|
|
|
|
|
public Stream BaseStream { get { return m_stream; } }
|
|
|
|
|
|
|
|
public override bool CanRead { get { return m_stream.CanRead; } }
|
|
|
|
public override bool CanSeek { get { return m_stream.CanSeek; } }
|
|
|
|
public override bool CanWrite { get { return m_stream.CanWrite; } }
|
|
|
|
public override long Length { get { return m_stream.Length - m_base_pos; } }
|
|
|
|
public override long Position
|
|
|
|
{
|
|
|
|
get { return m_stream.Position - m_base_pos; }
|
|
|
|
set { m_stream.Position = m_base_pos + value; }
|
|
|
|
}
|
|
|
|
|
2014-07-25 04:53:32 +08:00
|
|
|
public SteinsGateEncryptedStream (ArcView file, long offset, uint size)
|
|
|
|
{
|
2014-07-29 11:02:49 +08:00
|
|
|
m_stream = file.CreateStream (offset, size);
|
|
|
|
m_should_dispose = true;
|
|
|
|
m_base_pos = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
public SteinsGateEncryptedStream (Stream output)
|
|
|
|
{
|
|
|
|
m_stream = output;
|
|
|
|
m_should_dispose = false;
|
|
|
|
m_base_pos = m_stream.Position;
|
|
|
|
}
|
|
|
|
|
|
|
|
#region System.IO.Stream methods
|
|
|
|
public override void Flush()
|
|
|
|
{
|
|
|
|
m_stream.Flush();
|
|
|
|
}
|
|
|
|
|
|
|
|
public override long Seek (long offset, SeekOrigin origin)
|
|
|
|
{
|
|
|
|
if (SeekOrigin.Begin == origin)
|
|
|
|
offset += m_base_pos;
|
|
|
|
offset = m_stream.Seek (offset, origin);
|
|
|
|
return offset - m_base_pos;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void SetLength (long length)
|
|
|
|
{
|
|
|
|
throw new System.NotSupportedException ("SteinsGateEncryptedStream.SetLength method is not supported");
|
2014-07-25 04:53:32 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
public override int Read (byte[] buffer, int offset, int count)
|
|
|
|
{
|
|
|
|
int position = (int)Position & 7;
|
2014-07-29 11:02:49 +08:00
|
|
|
int read = m_stream.Read (buffer, offset, count);
|
2014-07-25 04:53:32 +08:00
|
|
|
if (read > 0)
|
|
|
|
{
|
|
|
|
for (int i = 0; i < read; ++i)
|
|
|
|
{
|
|
|
|
buffer[offset+i] ^= NpaSteinsGateOpener.KeyString[(position+i)&7];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return read;
|
|
|
|
}
|
|
|
|
|
|
|
|
public override int ReadByte ()
|
|
|
|
{
|
|
|
|
int position = (int)Position & 7;
|
2014-07-29 11:02:49 +08:00
|
|
|
int b = m_stream.ReadByte();
|
2014-07-25 04:53:32 +08:00
|
|
|
if (-1 != b)
|
|
|
|
{
|
|
|
|
b ^= NpaSteinsGateOpener.KeyString[position];
|
|
|
|
}
|
|
|
|
return b;
|
|
|
|
}
|
2014-07-29 11:02:49 +08:00
|
|
|
|
|
|
|
public override void Write (byte[] buffer, int offset, int count)
|
|
|
|
{
|
|
|
|
int position = (int)Position & 7;
|
|
|
|
byte[] write_buf = new byte[count];
|
|
|
|
for (int i = 0; i < count; ++i)
|
|
|
|
{
|
|
|
|
write_buf[i] = (byte)(buffer[offset+i] ^ NpaSteinsGateOpener.KeyString[(position+i)&7]);
|
|
|
|
}
|
|
|
|
m_stream.Write (write_buf, 0, count);
|
|
|
|
}
|
|
|
|
|
|
|
|
public override void WriteByte (byte value)
|
|
|
|
{
|
|
|
|
int position = (int)Position & 7;
|
|
|
|
m_stream.WriteByte ((byte)(value ^ NpaSteinsGateOpener.KeyString[position]));
|
|
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region IDisposable Members
|
|
|
|
bool disposed = false;
|
|
|
|
protected override void Dispose (bool disposing)
|
|
|
|
{
|
|
|
|
if (!disposed)
|
|
|
|
{
|
|
|
|
if (disposing && m_should_dispose)
|
|
|
|
{
|
|
|
|
m_stream.Dispose();
|
|
|
|
}
|
|
|
|
disposed = true;
|
|
|
|
base.Dispose (disposing);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endregion
|
2014-07-25 04:53:32 +08:00
|
|
|
}
|
|
|
|
}
|