mirror of
https://github.com/crskycode/GARbro.git
synced 2025-01-11 04:29:15 +08:00
(KiriKiri.ICrypt): added virtual method EntryReadFilter.
This commit is contained in:
parent
b713a01031
commit
d7d49c010b
@ -112,6 +112,7 @@
|
|||||||
<Compile Include="Kaas\AudioKAAS.cs" />
|
<Compile Include="Kaas\AudioKAAS.cs" />
|
||||||
<Compile Include="Kaguya\ArcAN21.cs" />
|
<Compile Include="Kaguya\ArcAN21.cs" />
|
||||||
<Compile Include="Kaguya\ArcUF.cs" />
|
<Compile Include="Kaguya\ArcUF.cs" />
|
||||||
|
<Compile Include="KiriKiri\CzCrypt.cs" />
|
||||||
<Compile Include="Kurumi\ImageGRA.cs" />
|
<Compile Include="Kurumi\ImageGRA.cs" />
|
||||||
<Compile Include="Leaf\ArcPX.cs" />
|
<Compile Include="Leaf\ArcPX.cs" />
|
||||||
<Compile Include="LunaSoft\ArcPAC.cs" />
|
<Compile Include="LunaSoft\ArcPAC.cs" />
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//! \date Wed Jul 16 13:58:17 2014
|
//! \date Wed Jul 16 13:58:17 2014
|
||||||
//! \brief KiriKiri engine archive implementation.
|
//! \brief KiriKiri engine archive implementation.
|
||||||
//
|
//
|
||||||
// Copyright (C) 2014-2016 by morkt
|
// Copyright (C) 2014-2017 by morkt
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
// of this software and associated documentation files (the "Software"), to
|
// of this software and associated documentation files (the "Software"), to
|
||||||
@ -344,172 +344,7 @@ NextEntry:
|
|||||||
else
|
else
|
||||||
input = new Xp3Stream (arc.File, xp3_entry);
|
input = new Xp3Stream (arc.File, xp3_entry);
|
||||||
|
|
||||||
if (xp3_entry.UnpackedSize <= 5 || "audio" == entry.Type)
|
return xp3_entry.Cipher.EntryReadFilter (xp3_entry, input);
|
||||||
return input;
|
|
||||||
|
|
||||||
var header = new byte[5];
|
|
||||||
input.Read (header, 0, 5);
|
|
||||||
if (0x184D2204 == header.ToInt32 (0)) // LZ4 magic
|
|
||||||
{
|
|
||||||
// assume no scripts are compressed using LZ4, return decompressed stream right away
|
|
||||||
return DecompressLz4 (xp3_entry, header, input);
|
|
||||||
}
|
|
||||||
else if (0xA590D7FD == header.ToUInt32 (0)) // cZLIB magic
|
|
||||||
{
|
|
||||||
return DecompressCz (xp3_entry, header, input);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (0xFE == header[0] && 0xFE == header[1] && header[2] < 3 && 0xFF == header[3] && 0xFE == header[4])
|
|
||||||
return DecryptScript (header[2], input, xp3_entry.UnpackedSize);
|
|
||||||
|
|
||||||
if (!input.CanSeek)
|
|
||||||
return new PrefixStream (header, input);
|
|
||||||
input.Position = 0;
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream DecompressLz4 (Xp3Entry entry, byte[] header, Stream input)
|
|
||||||
{
|
|
||||||
if (header.Length != 5)
|
|
||||||
throw new ArgumentException ("Invalid header length for DecompressLz4", "header");
|
|
||||||
var info = new Lz4FrameInfo (header[4]);
|
|
||||||
info.SetBlockSize (input.ReadByte());
|
|
||||||
if (info.HasContentLength)
|
|
||||||
{
|
|
||||||
input.Read (header, 0, 4);
|
|
||||||
long length = header.ToUInt32 (0);
|
|
||||||
input.Read (header, 0, 4);
|
|
||||||
length |= (long)header.ToUInt32 (0) << 32;
|
|
||||||
info.OriginalLength = length;
|
|
||||||
entry.UnpackedSize = (uint)length;
|
|
||||||
entry.IsPacked = true;
|
|
||||||
}
|
|
||||||
if (info.HasDictionary)
|
|
||||||
{
|
|
||||||
input.Read (header, 0, 4);
|
|
||||||
info.DictionaryId = header.ToInt32 (0);
|
|
||||||
}
|
|
||||||
input.ReadByte(); // skip descriptor checksum
|
|
||||||
return new Lz4Stream (input, info);
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream DecompressCz (Xp3Entry entry, byte[] src_header, Stream input)
|
|
||||||
{
|
|
||||||
var header = new byte[15];
|
|
||||||
Buffer.BlockCopy (src_header, 0, header, 0, Math.Min (header.Length, src_header.Length));
|
|
||||||
if (header.Length > src_header.Length)
|
|
||||||
input.Read (header, src_header.Length, header.Length - src_header.Length);
|
|
||||||
header[4] ^= 0x11;
|
|
||||||
header[5] ^= 0x7F;
|
|
||||||
header[6] ^= 0x9A;
|
|
||||||
byte key = header[4];
|
|
||||||
int unpacked_size = CzDecryptInt (header, 7, key);
|
|
||||||
int packed_size = CzDecryptInt (header, 11, key);
|
|
||||||
var data = new byte[packed_size];
|
|
||||||
input.Read (data, 0, packed_size);
|
|
||||||
input.Dispose();
|
|
||||||
data = CzDecryptData (data);
|
|
||||||
input = new BinMemoryStream (data);
|
|
||||||
if ('C' == header[4])
|
|
||||||
input = new ZLibStream (input, CompressionMode.Decompress);
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int CzDecryptInt (byte[] data, int offset, byte key)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 4; ++i)
|
|
||||||
{
|
|
||||||
data[offset+i] ^= (byte)(key ^ CzHeaderKey[i]);
|
|
||||||
}
|
|
||||||
return data.ToInt32 (offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
static byte[] CzDecryptData (byte[] data)
|
|
||||||
{
|
|
||||||
int padded_size = data.Length - 5;
|
|
||||||
int original_size = padded_size - (data[padded_size+1] ^ data[padded_size]);
|
|
||||||
uint iv_seed = data.ToUInt32 (padded_size+1) ^ 0xBFBFBFBFu;
|
|
||||||
using (var aes = Aes.Create())
|
|
||||||
{
|
|
||||||
aes.Mode = CipherMode.CBC;
|
|
||||||
aes.Padding = PaddingMode.Zeros;
|
|
||||||
aes.Key = CzDefaultKey;
|
|
||||||
aes.IV = CzCreateIV (iv_seed);
|
|
||||||
using (var enc = new MemoryStream (data, 0, padded_size))
|
|
||||||
using (var dec = new InputCryptoStream (enc, aes.CreateDecryptor()))
|
|
||||||
{
|
|
||||||
var original = new byte[original_size];
|
|
||||||
dec.Read (original, 0, original_size);
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static byte[] CzCreateIV (uint seed)
|
|
||||||
{
|
|
||||||
var state = new uint[4];
|
|
||||||
state[0] = 123456789; // field_0
|
|
||||||
state[1] = 972436830; // field_4
|
|
||||||
state[2] = 524018621; // field_8
|
|
||||||
state[3] = seed; // field_C
|
|
||||||
var iv = new byte[16];
|
|
||||||
for (int i = 0; i < 16; ++i)
|
|
||||||
{
|
|
||||||
uint a = state[3];
|
|
||||||
uint b = state[0] ^ (state[0] << 11);
|
|
||||||
state[0] = state[1];
|
|
||||||
state[1] = state[2];
|
|
||||||
state[2] = a;
|
|
||||||
state[3] = b ^ a ^ ((b ^ (a >> 11)) >> 8);
|
|
||||||
iv[i] = (byte)state[3];
|
|
||||||
}
|
|
||||||
return iv;
|
|
||||||
}
|
|
||||||
|
|
||||||
static readonly byte[] CzHeaderKey = { 0x9D, 0x1D, 0x9A, 0xF2 };
|
|
||||||
static readonly byte[] CzDefaultKey = {
|
|
||||||
0x91, 0x10, 0xFC, 0x75, 0x45, 0x8F, 0xB5, 0xE6, 0xFE, 0xAC, 0xBA, 0x44, 0x76, 0x58, 0xC2, 0x1A
|
|
||||||
};
|
|
||||||
|
|
||||||
Stream DecryptScript (int enc_type, Stream input, uint unpacked_size)
|
|
||||||
{
|
|
||||||
using (var reader = new BinaryReader (input, Encoding.Unicode, true))
|
|
||||||
{
|
|
||||||
if (2 == enc_type)
|
|
||||||
{
|
|
||||||
reader.ReadInt64(); // packed_size
|
|
||||||
reader.ReadInt64(); // unpacked_size
|
|
||||||
return new ZLibStream (input, CompressionMode.Decompress);
|
|
||||||
}
|
|
||||||
var output = new MemoryStream ((int)unpacked_size+2);
|
|
||||||
using (var writer = new BinaryWriter (output, Encoding.Unicode, true))
|
|
||||||
{
|
|
||||||
writer.Write ('\xFEFF'); // BOM
|
|
||||||
int c;
|
|
||||||
if (1 == enc_type)
|
|
||||||
{
|
|
||||||
while ((c = reader.Read()) != -1)
|
|
||||||
{
|
|
||||||
c = (c & 0xAAAA) >> 1 | (c & 0x5555) << 1;
|
|
||||||
writer.Write ((char)c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
while ((c = reader.Read()) != -1)
|
|
||||||
{
|
|
||||||
if (c >= 0x20)
|
|
||||||
{
|
|
||||||
c = c ^ (((c & 0xFE) << 8) ^ 1);
|
|
||||||
writer.Write ((char)c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output.Position = 0;
|
|
||||||
input.Dispose();
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ResourceOptions GetDefaultOptions ()
|
public override ResourceOptions GetDefaultOptions ()
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
//! \date Thu Feb 04 12:08:40 2016
|
//! \date Thu Feb 04 12:08:40 2016
|
||||||
//! \brief KiriKiri engine encryption algorithms.
|
//! \brief KiriKiri engine encryption algorithms.
|
||||||
//
|
//
|
||||||
// Copyright (C) 2016 by morkt
|
// Copyright (C) 2016-2017 by morkt
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
// of this software and associated documentation files (the "Software"), to
|
// of this software and associated documentation files (the "Software"), to
|
||||||
@ -27,6 +27,8 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using GameRes.Compression;
|
||||||
using GameRes.Utility;
|
using GameRes.Utility;
|
||||||
|
|
||||||
namespace GameRes.Formats.KiriKiri
|
namespace GameRes.Formats.KiriKiri
|
||||||
@ -83,6 +85,96 @@ namespace GameRes.Formats.KiriKiri
|
|||||||
else
|
else
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Post-process entry stream.
|
||||||
|
/// </summary>
|
||||||
|
public virtual Stream EntryReadFilter (Xp3Entry entry, Stream input)
|
||||||
|
{
|
||||||
|
if (entry.UnpackedSize <= 5 || "audio" == entry.Type)
|
||||||
|
return input;
|
||||||
|
|
||||||
|
var header = new byte[5];
|
||||||
|
input.Read (header, 0, 5);
|
||||||
|
if (0x184D2204 == header.ToInt32 (0)) // LZ4 magic
|
||||||
|
{
|
||||||
|
// assume no scripts are compressed using LZ4, return decompressed stream right away
|
||||||
|
return DecompressLz4 (entry, header, input);
|
||||||
|
}
|
||||||
|
if (0xFE == header[0] && 0xFE == header[1] && header[2] < 3 && 0xFF == header[3] && 0xFE == header[4])
|
||||||
|
return DecryptScript (header[2], input, entry.UnpackedSize);
|
||||||
|
|
||||||
|
if (!input.CanSeek)
|
||||||
|
return new PrefixStream (header, input);
|
||||||
|
input.Position = 0;
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Stream DecompressLz4 (Xp3Entry entry, byte[] header, Stream input)
|
||||||
|
{
|
||||||
|
if (header.Length != 5)
|
||||||
|
throw new ArgumentException ("Invalid header length for DecompressLz4", "header");
|
||||||
|
var info = new Lz4FrameInfo (header[4]);
|
||||||
|
info.SetBlockSize (input.ReadByte());
|
||||||
|
if (info.HasContentLength)
|
||||||
|
{
|
||||||
|
input.Read (header, 0, 4);
|
||||||
|
long length = header.ToUInt32 (0);
|
||||||
|
input.Read (header, 0, 4);
|
||||||
|
length |= (long)header.ToUInt32 (0) << 32;
|
||||||
|
info.OriginalLength = length;
|
||||||
|
entry.UnpackedSize = (uint)length;
|
||||||
|
entry.IsPacked = true;
|
||||||
|
}
|
||||||
|
if (info.HasDictionary)
|
||||||
|
{
|
||||||
|
input.Read (header, 0, 4);
|
||||||
|
info.DictionaryId = header.ToInt32 (0);
|
||||||
|
}
|
||||||
|
input.ReadByte(); // skip descriptor checksum
|
||||||
|
return new Lz4Stream (input, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Stream DecryptScript (int enc_type, Stream input, uint unpacked_size)
|
||||||
|
{
|
||||||
|
using (var reader = new BinaryReader (input, Encoding.Unicode, true))
|
||||||
|
{
|
||||||
|
if (2 == enc_type)
|
||||||
|
{
|
||||||
|
reader.ReadInt64(); // packed_size
|
||||||
|
reader.ReadInt64(); // unpacked_size
|
||||||
|
return new ZLibStream (input, CompressionMode.Decompress);
|
||||||
|
}
|
||||||
|
var output = new MemoryStream ((int)unpacked_size+2);
|
||||||
|
using (var writer = new BinaryWriter (output, Encoding.Unicode, true))
|
||||||
|
{
|
||||||
|
writer.Write ('\xFEFF'); // BOM
|
||||||
|
int c;
|
||||||
|
if (1 == enc_type)
|
||||||
|
{
|
||||||
|
while ((c = reader.Read()) != -1)
|
||||||
|
{
|
||||||
|
c = (c & 0xAAAA) >> 1 | (c & 0x5555) << 1;
|
||||||
|
writer.Write ((char)c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
while ((c = reader.Read()) != -1)
|
||||||
|
{
|
||||||
|
if (c >= 0x20)
|
||||||
|
{
|
||||||
|
c = c ^ (((c & 0xFE) << 8) ^ 1);
|
||||||
|
writer.Write ((char)c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.Position = 0;
|
||||||
|
input.Dispose();
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
@ -984,7 +1076,7 @@ namespace GameRes.Formats.KiriKiri
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class KissCrypt : ICrypt
|
public class KissCrypt : CzCrypt
|
||||||
{
|
{
|
||||||
public override void Decrypt (Xp3Entry entry, long offset, byte[] data, int pos, int count)
|
public override void Decrypt (Xp3Entry entry, long offset, byte[] data, int pos, int count)
|
||||||
{
|
{
|
||||||
|
133
ArcFormats/KiriKiri/CzCrypt.cs
Normal file
133
ArcFormats/KiriKiri/CzCrypt.cs
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
//! \file CzCrypt.cs
|
||||||
|
//! \date 2017 Sep 27
|
||||||
|
//! \brief implementation of cZLIB extraction filter for KiriKiri engine.
|
||||||
|
//
|
||||||
|
// 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.IO;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using GameRes.Compression;
|
||||||
|
|
||||||
|
namespace GameRes.Formats.KiriKiri
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// cZLIB entry filter.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public abstract class CzCrypt : ICrypt
|
||||||
|
{
|
||||||
|
const uint CzMagic = 0xA590D7FDu;
|
||||||
|
const uint CzIvSeed = 0xBFBFBFBFu;
|
||||||
|
|
||||||
|
static readonly byte[] CzHeaderKey = { 0x9D, 0x1D, 0x9A, 0xF2 };
|
||||||
|
static readonly byte[] CzDefaultKey = {
|
||||||
|
0x91, 0x10, 0xFC, 0x75, 0x45, 0x8F, 0xB5, 0xE6, 0xFE, 0xAC, 0xBA, 0x44, 0x76, 0x58, 0xC2, 0x1A
|
||||||
|
};
|
||||||
|
|
||||||
|
public override Stream EntryReadFilter (Xp3Entry entry, Stream input)
|
||||||
|
{
|
||||||
|
if (entry.UnpackedSize <= 15 || "audio" == entry.Type)
|
||||||
|
return input;
|
||||||
|
|
||||||
|
var header = new byte[15];
|
||||||
|
input.Read (header, 0, 15);
|
||||||
|
if (CzMagic == header.ToUInt32 (0))
|
||||||
|
{
|
||||||
|
var type = new char[3] {
|
||||||
|
(char)(header[4] ^ 0x11),
|
||||||
|
(char)(header[5] ^ 0x7F),
|
||||||
|
(char)(header[6] ^ 0x9A)
|
||||||
|
};
|
||||||
|
byte key = (byte)type[0];
|
||||||
|
int unpacked_size = CzDecryptInt (header, 7, key);
|
||||||
|
int packed_size = CzDecryptInt (header, 11, key);
|
||||||
|
if (packed_size < entry.UnpackedSize && 0 == ((packed_size-5) & 0xF))
|
||||||
|
{
|
||||||
|
var data = new byte[packed_size];
|
||||||
|
input.Read (data, 0, packed_size);
|
||||||
|
input.Dispose();
|
||||||
|
data = CzDecryptData (data);
|
||||||
|
input = new BinMemoryStream (data);
|
||||||
|
if ('C' == type[0])
|
||||||
|
input = new ZLibStream (input, CompressionMode.Decompress);
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!input.CanSeek)
|
||||||
|
return new PrefixStream (header, input);
|
||||||
|
input.Position = 0;
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int CzDecryptInt (byte[] data, int offset, byte key)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; ++i)
|
||||||
|
{
|
||||||
|
data[offset+i] ^= (byte)(key ^ CzHeaderKey[i]);
|
||||||
|
}
|
||||||
|
return data.ToInt32 (offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] CzDecryptData (byte[] data)
|
||||||
|
{
|
||||||
|
int padded_size = data.Length - 5;
|
||||||
|
int original_size = padded_size - (data[padded_size+1] ^ data[padded_size]);
|
||||||
|
uint iv_seed = data.ToUInt32 (padded_size+1) ^ CzIvSeed;
|
||||||
|
using (var aes = Aes.Create())
|
||||||
|
{
|
||||||
|
aes.Mode = CipherMode.CBC;
|
||||||
|
aes.Padding = PaddingMode.Zeros;
|
||||||
|
aes.Key = CzDefaultKey;
|
||||||
|
aes.IV = CzCreateIV (iv_seed);
|
||||||
|
using (var enc = new MemoryStream (data, 0, padded_size))
|
||||||
|
using (var dec = new InputCryptoStream (enc, aes.CreateDecryptor()))
|
||||||
|
{
|
||||||
|
var original = new byte[original_size];
|
||||||
|
dec.Read (original, 0, original_size);
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static byte[] CzCreateIV (uint seed)
|
||||||
|
{
|
||||||
|
var state = new uint[4];
|
||||||
|
state[0] = 123456789; // field_0
|
||||||
|
state[1] = 972436830; // field_4
|
||||||
|
state[2] = 524018621; // field_8
|
||||||
|
state[3] = seed; // field_C
|
||||||
|
var iv = new byte[16];
|
||||||
|
for (int i = 0; i < 16; ++i)
|
||||||
|
{
|
||||||
|
uint a = state[3];
|
||||||
|
uint b = state[0] ^ (state[0] << 11);
|
||||||
|
state[0] = state[1];
|
||||||
|
state[1] = state[2];
|
||||||
|
state[2] = a;
|
||||||
|
state[3] = b ^ a ^ ((b ^ (a >> 11)) >> 8);
|
||||||
|
iv[i] = (byte)state[3];
|
||||||
|
}
|
||||||
|
return iv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user