From 04bafb8b7053a9448c8c65abeac8f61a5b8f10a8 Mon Sep 17 00:00:00 2001 From: Li Date: Fri, 14 Apr 2023 03:52:57 +0000 Subject: [PATCH 01/31] Initial commit --- LICENSE | 10 ++++++++++ README.md | 3 +++ 2 files changed, 13 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cde4ac6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means. + +In jurisdictions that recognize copyright laws, the author or authors of this software dedicate any and all copyright interest in the software to the public domain. We make this dedication for the benefit of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of relinquishment in perpetuity of all present and future rights to this software under copyright law. + +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 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. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..43ae617 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Chovy-Sign-v2 + +complete rewrite of the original chovy project, with PS1 support. and less dependancies \ No newline at end of file From 7b6830a40b98451f2776b43e166ef54fa86b6fa3 Mon Sep 17 00:00:00 2001 From: Li Date: Fri, 14 Apr 2023 15:55:11 +1200 Subject: [PATCH 02/31] Initial commit --- .gitignore | 21 + PbpResign/PbpResign.csproj | 19 + PbpResign/Program.cs | 1371 +++++++++ PbpResign/Sfo.cs | 78 + PopsBuilder/Atrac3/Atrac3ToolEncoder.cs | 155 + PopsBuilder/Atrac3/IAtracEncoderBase.cs | 13 + PopsBuilder/Cue/CueIndex.cs | 107 + PopsBuilder/Cue/CueReader.cs | 382 +++ PopsBuilder/Cue/CueStream.cs | 152 + PopsBuilder/Cue/CueTrack.cs | 65 + PopsBuilder/Cue/TrackType.cs | 14 + PopsBuilder/Pops/DiscCompressor.cs | 234 ++ PopsBuilder/Pops/DiscInfo.cs | 52 + PopsBuilder/Pops/EccRemoverStream.cs | 226 ++ PopsBuilder/Pops/PopsImg.cs | 105 + PopsBuilder/Pops/PsIsoImg.cs | 65 + PopsBuilder/Pops/PsTitleImg.cs | 153 + PopsBuilder/PopsBuilder.csproj | 28 + PopsBuilder/Psp/NpDrmPsar.cs | 34 + PopsBuilder/Psp/PbpBuilder.cs | 74 + PopsBuilder/Resources.Designer.cs | 103 + PopsBuilder/Resources.resx | 133 + PopsBuilder/Resources/DATAPSPSD.ELF | Bin 0 -> 14658 bytes PopsBuilder/Resources/DATAPSPSDCFG.BIN | Bin 0 -> 1040 bytes PopsBuilder/Resources/SIMPLE.PNG | Bin 0 -> 22964 bytes PopsBuilder/Resources/STARTDAT.PNG | Bin 0 -> 9283 bytes PopsBuilder/Resources/Thumbs.db | Bin 0 -> 14848 bytes PopsBuilder/StreamUtil.cs | 116 + PopsBuilder/xXEccD3str0yerXx.cs | 55 + PspCrypto/AMCTRL.cs | 710 +++++ PspCrypto/AesHelper.cs | 250 ++ PspCrypto/AtracCrypto.cs | 126 + PspCrypto/DNASHelper.cs | 172 ++ PspCrypto/DNASStream.cs | 636 ++++ PspCrypto/ECDsaHelper.cs | 127 + PspCrypto/Interop/Interop.Bignum.cs | 65 + PspCrypto/Interop/Interop.ERR.cs | 93 + .../Interop/Interop.EcDsa.ImportExport.cs | 71 + PspCrypto/Interop/Interop.EcDsa.cs | 78 + PspCrypto/Interop/Interop.EcKey.cs | 17 + PspCrypto/KIRKEngine.cs | 1113 +++++++ PspCrypto/KeyVault.cs | 231 ++ PspCrypto/Libraries.cs | 8 + PspCrypto/Lz.cs | 53 + PspCrypto/Lzrc.cs | 836 ++++++ PspCrypto/PspCrypto.csproj | 30 + PspCrypto/PspParameter.cs | 12 + PspCrypto/Resource.Designer.cs | 73 + PspCrypto/Resource.resx | 124 + PspCrypto/RijndaelMod.cs | 38 + PspCrypto/RijndaelModTransform.cs | 1099 +++++++ PspCrypto/SHA224Managed_OLD.cs | 272 ++ PspCrypto/SafeHandles/SafeBignumHandle.cs | 32 + PspCrypto/SafeHandles/SafeEcKeyHandle.cs | 45 + PspCrypto/SceDdrdb.cs | 28 + PspCrypto/SceMemlmd.cs | 513 ++++ PspCrypto/SceMesgLed.cs | 2662 +++++++++++++++++ PspCrypto/SceNpDrm.cs | 1286 ++++++++ .../Cryptography/Asn1Reader/AsnValueReader.cs | 222 ++ .../AsymmetricAlgorithmHelpers.cs | 137 + .../Security/Cryptography/ECDsaManaged.cs | 171 ++ .../Cryptography/EbootPbpKCalculator.cs | 103 + PspCrypto/Security/Cryptography/HMACCommon.cs | 116 + .../Cryptography/HMACManagedHashProvider.cs | 125 + PspCrypto/Security/Cryptography/HMACSHA224.cs | 156 + .../Cryptography/HashAlgorithmNames.cs | 13 + .../Security/Cryptography/HashProvider.cs | 79 + .../Cryptography/HashProviderDispenser.cs | 53 + PspCrypto/Security/Cryptography/SHA224.cs | 146 + .../Cryptography/SHAManagedHashProvider.cs | 376 +++ PspCrypto/Structs.cs | 264 ++ PspCrypto/Utils.cs | 85 + PspCrypto/__sce_discinfo | Bin 0 -> 37632 bytes PspCryptoHelper.dll | Bin 0 -> 2596352 bytes PspTest.sln | 43 + PsvImage/AesHelper.cs | 42 + PsvImage/CmaKeys.cs | 38 + PsvImage/PSVIMGBuilder.cs | 119 + PsvImage/PsvImage.csproj | 8 + PsvImage/PsvImgStream.cs | 43 + PsvImage/PsvImgStructs.cs | 154 + PsvImage/PsvmdBuilder.cs | 22 + PsvImage/Utils.cs | 62 + 83 files changed, 17132 insertions(+) create mode 100644 .gitignore create mode 100644 PbpResign/PbpResign.csproj create mode 100644 PbpResign/Program.cs create mode 100644 PbpResign/Sfo.cs create mode 100644 PopsBuilder/Atrac3/Atrac3ToolEncoder.cs create mode 100644 PopsBuilder/Atrac3/IAtracEncoderBase.cs create mode 100644 PopsBuilder/Cue/CueIndex.cs create mode 100644 PopsBuilder/Cue/CueReader.cs create mode 100644 PopsBuilder/Cue/CueStream.cs create mode 100644 PopsBuilder/Cue/CueTrack.cs create mode 100644 PopsBuilder/Cue/TrackType.cs create mode 100644 PopsBuilder/Pops/DiscCompressor.cs create mode 100644 PopsBuilder/Pops/DiscInfo.cs create mode 100644 PopsBuilder/Pops/EccRemoverStream.cs create mode 100644 PopsBuilder/Pops/PopsImg.cs create mode 100644 PopsBuilder/Pops/PsIsoImg.cs create mode 100644 PopsBuilder/Pops/PsTitleImg.cs create mode 100644 PopsBuilder/PopsBuilder.csproj create mode 100644 PopsBuilder/Psp/NpDrmPsar.cs create mode 100644 PopsBuilder/Psp/PbpBuilder.cs create mode 100644 PopsBuilder/Resources.Designer.cs create mode 100644 PopsBuilder/Resources.resx create mode 100644 PopsBuilder/Resources/DATAPSPSD.ELF create mode 100644 PopsBuilder/Resources/DATAPSPSDCFG.BIN create mode 100644 PopsBuilder/Resources/SIMPLE.PNG create mode 100644 PopsBuilder/Resources/STARTDAT.PNG create mode 100644 PopsBuilder/Resources/Thumbs.db create mode 100644 PopsBuilder/StreamUtil.cs create mode 100644 PopsBuilder/xXEccD3str0yerXx.cs create mode 100644 PspCrypto/AMCTRL.cs create mode 100644 PspCrypto/AesHelper.cs create mode 100644 PspCrypto/AtracCrypto.cs create mode 100644 PspCrypto/DNASHelper.cs create mode 100644 PspCrypto/DNASStream.cs create mode 100644 PspCrypto/ECDsaHelper.cs create mode 100644 PspCrypto/Interop/Interop.Bignum.cs create mode 100644 PspCrypto/Interop/Interop.ERR.cs create mode 100644 PspCrypto/Interop/Interop.EcDsa.ImportExport.cs create mode 100644 PspCrypto/Interop/Interop.EcDsa.cs create mode 100644 PspCrypto/Interop/Interop.EcKey.cs create mode 100644 PspCrypto/KIRKEngine.cs create mode 100644 PspCrypto/KeyVault.cs create mode 100644 PspCrypto/Libraries.cs create mode 100644 PspCrypto/Lz.cs create mode 100644 PspCrypto/Lzrc.cs create mode 100644 PspCrypto/PspCrypto.csproj create mode 100644 PspCrypto/PspParameter.cs create mode 100644 PspCrypto/Resource.Designer.cs create mode 100644 PspCrypto/Resource.resx create mode 100644 PspCrypto/RijndaelMod.cs create mode 100644 PspCrypto/RijndaelModTransform.cs create mode 100644 PspCrypto/SHA224Managed_OLD.cs create mode 100644 PspCrypto/SafeHandles/SafeBignumHandle.cs create mode 100644 PspCrypto/SafeHandles/SafeEcKeyHandle.cs create mode 100644 PspCrypto/SceDdrdb.cs create mode 100644 PspCrypto/SceMemlmd.cs create mode 100644 PspCrypto/SceMesgLed.cs create mode 100644 PspCrypto/SceNpDrm.cs create mode 100644 PspCrypto/Security/Cryptography/Asn1Reader/AsnValueReader.cs create mode 100644 PspCrypto/Security/Cryptography/AsymmetricAlgorithmHelpers.cs create mode 100644 PspCrypto/Security/Cryptography/ECDsaManaged.cs create mode 100644 PspCrypto/Security/Cryptography/EbootPbpKCalculator.cs create mode 100644 PspCrypto/Security/Cryptography/HMACCommon.cs create mode 100644 PspCrypto/Security/Cryptography/HMACManagedHashProvider.cs create mode 100644 PspCrypto/Security/Cryptography/HMACSHA224.cs create mode 100644 PspCrypto/Security/Cryptography/HashAlgorithmNames.cs create mode 100644 PspCrypto/Security/Cryptography/HashProvider.cs create mode 100644 PspCrypto/Security/Cryptography/HashProviderDispenser.cs create mode 100644 PspCrypto/Security/Cryptography/SHA224.cs create mode 100644 PspCrypto/Security/Cryptography/SHAManagedHashProvider.cs create mode 100644 PspCrypto/Structs.cs create mode 100644 PspCrypto/Utils.cs create mode 100644 PspCrypto/__sce_discinfo create mode 100644 PspCryptoHelper.dll create mode 100644 PspTest.sln create mode 100644 PsvImage/AesHelper.cs create mode 100644 PsvImage/CmaKeys.cs create mode 100644 PsvImage/PSVIMGBuilder.cs create mode 100644 PsvImage/PsvImage.csproj create mode 100644 PsvImage/PsvImgStream.cs create mode 100644 PsvImage/PsvImgStructs.cs create mode 100644 PsvImage/PsvmdBuilder.cs create mode 100644 PsvImage/Utils.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bbb701 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +*/bin/* +*/obj/* +.vs/* +*.7z + + +PbpResign/bin/* +PbpResign/obj/* + +PopsBuilder/bin/* +PopsBuilder/obj/* + + +PspCrypto/bin/* +PspCrypto/obj/* + +PsvImage/bin/* +PsvImage/obj/* + +UnicornTest/* +UnicornManaged/* \ No newline at end of file diff --git a/PbpResign/PbpResign.csproj b/PbpResign/PbpResign.csproj new file mode 100644 index 0000000..c8fe916 --- /dev/null +++ b/PbpResign/PbpResign.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + true + + + + + + + + + + + + + diff --git a/PbpResign/Program.cs b/PbpResign/Program.cs new file mode 100644 index 0000000..e054307 --- /dev/null +++ b/PbpResign/Program.cs @@ -0,0 +1,1371 @@ +using CommunityToolkit.HighPerformance; +using Ionic.Zlib; +using PopsBuilder.Pops; +using PopsBuilder.Psp; +using PspCrypto; +using PsvImage; +using System; +using System.Buffers.Binary; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace PbpResign +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct PbpHeader + { + public int Sig; + public int Ver; + public int ParamOff; + public int Icon0Off; + public int Icon1Off; + public int Pic0Off; + public int Pic1Off; + public int Snd0Off; + public int DataPspOff; + public int DataPsarOff; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + unsafe struct NpUmdImgBody + { + public ushort SectorSize; // 0x0800 + public ushort Unk2; // 0xE000 + public uint Unk4; + public uint Unk8; + public uint Unk12; + public uint Unk16; + public uint LbaStart; + public uint Unk24; + public uint NSectors; + public uint Unk32; + public uint LbaEnd; + public uint Unk40; + public uint BlockEntryOffset; + private fixed byte discId_[0x10]; + public Span DiscId + { + get + { + fixed (byte* ptr = discId_) + { + return new Span(ptr, 0x10); + } + } + } + public ushort HeaderStartOffset; + public ushort HeaderStartOffset1; + public uint ThreadPriority; + public byte Unk72; + public byte BBMacParam; + public byte Unk74; + public byte Unk75; + public uint Unk76; + public uint Unk80; + public uint Unk84; + public uint Unk88; + public uint Unk92; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + unsafe struct NpUmdImgHdr + { + public int Magic0; + public int Magic1; + public int NpFlags; + public int BlockBasis; + private fixed byte contentId_[0x30]; + public Span ContentId + { + get + { + fixed (byte* ptr = contentId_) + { + return new Span(ptr, 0x30); + } + } + } + public NpUmdImgBody Body; + private fixed byte headerKey_[0x10]; + public Span HeaderKey + { + get + { + fixed (byte* ptr = headerKey_) + { + return new Span(ptr, 0x10); + } + } + } + private fixed byte dataKey_[0x10]; + public Span DataKey + { + get + { + fixed (byte* ptr = dataKey_) + { + return new Span(ptr, 0x10); + } + } + } + private fixed byte headerHash_[0x10]; + public Span HeaderHash + { + get + { + fixed (byte* ptr = headerHash_) + { + return new Span(ptr, 0x10); + } + } + } + private fixed byte Pad[0x8]; + private fixed byte eCDsaSig_[0x28]; + public Span ECDsaSig + { + get + { + fixed (byte* ptr = eCDsaSig_) + { + return new Span(ptr, 0x28); + } + } + } + } + + unsafe struct NpBlock + { + private fixed byte mac_[0x10]; + + public Span Mac + { + get + { + fixed (byte* ptr = mac_) + { + return new Span(ptr, 0x10); + } + } + } + + public int Offset; + public int Size; + public int Unk1; + public int Unk2; + } + + [StructLayout(LayoutKind.Sequential)] + struct CDDA_ENTRY + { + public int offset; + public int size; + public int padding; + public int key; + } + + [StructLayout(LayoutKind.Sequential)] + unsafe struct ISO_ENTRY + { + public int offset; + public ushort size; + public short marker; // 0x01 or 0x00 + private fixed byte checksum_[0x10]; // First 0x10 bytes of sha1 sum of 0x10 disc sectors + + public Span checksum + { + get + { + fixed (byte* ptr = checksum_) + { + return new Span(ptr, 0x10); + } + } + } + public fixed byte padding[0x8]; + } + + [StructLayout(LayoutKind.Sequential)] + struct STARTDAT_HEADER + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] magic; // STARTDAT + public uint unk1; // 0x01 + public uint unk2; // 0x01 + public int header_size; + public int data_size; + } + + [StructLayout(LayoutKind.Sequential)] + unsafe struct SIMPLE_HEADER + { + private fixed byte magic_[8]; // SIMPLE__ + + public Span magic + { + get + { + fixed (byte* ptr = magic_) + { + return new Span(ptr, 8); + } + } + } + public uint unk1; // 0x64 + public uint unk2; // 0x01 + public int data_size; + public int unk3; // 0 or chcksm + public int unk4; // 0 or chcksm + } + + class Program + { + + private static byte[] Idps = { 0x00, 0x00, 0x00, 0x01, 0x01, 0x03, 0x00, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; + + private static string CId = "JP0177-NPJH50145_00-VALKYRIA2DLC002B"; + + private static KeyGen _keyGen; + + private static readonly Memory VersionKey = new byte[16]; + private static readonly Memory NewVersionKey = new byte[16]; + + //struct VersionKey + //{ + // public byte[] Fixed; + // public byte[] Type2; + // public byte[] Type3; + //} + + //private static readonly Dictionary Keys = new() + //{ + // { + // "JP0177-NPJH50145_00-VALKYRIA2DLC002B", + // new VersionKey + // { + // Fixed = new byte[] { 0x38, 0x20, 0xD0, 0x11, 0x07, 0xA3, 0xFF, 0x3E, 0x0A, 0x4C, 0x20, 0x85, 0x39, 0x10, 0xB5, 0x54 }, + // Type2 = new byte[] { 0x80, 0x2C, 0x03, 0xB8, 0xB9, 0x1E, 0xB6, 0xF8, 0xE8, 0xF6, 0xB8, 0x54, 0xAD, 0x8C, 0x0E, 0x25 }, + // Type3 = new byte[] { 0x90, 0xC2, 0x03, 0x02, 0x27, 0x90, 0x7C, 0x0C, 0x7A, 0xCD, 0x83, 0x30, 0x28, 0x13, 0x90, 0x83 } + // } + // }, + // { + // "EP9000-NPEG00005_00-0000000000000001", + // new VersionKey + // { + // Fixed = new byte[] { 0x5A, 0xB0, 0xB5, 0xE2, 0xC3, 0x2E, 0xE3, 0xBA, 0xFE, 0xF8, 0x0A, 0xDE, 0x35, 0xBD, 0x78, 0x88 }, + // Type2 = new byte[] { 0x5A, 0xB0, 0xB5, 0xE2, 0xC3, 0x2E, 0xE3, 0xBA, 0xFE, 0xF8, 0x0A, 0xDE, 0x35, 0xBD, 0x78, 0x88 }, + // Type3 = new byte[] { 0x0B, 0x84, 0x50, 0xE0, 0x63, 0x52, 0x36, 0x74, 0x01, 0x1C, 0x6B, 0x2B, 0x94, 0x82, 0x9F, 0x7A } + + // } + // } + //}; + + + static readonly byte[] multi_iso_magic = { + 0x50, // P + 0x53, // S + 0x54, // T + 0x49, // I + 0x54, // T + 0x4C, // L + 0x45, // E + 0x49, // I + 0x4D, // M + 0x47, // G + 0x30, // 0 + 0x30, // 0 + 0x30, // 0 + 0x30, // 0 + 0x30, // 0 + 0x30 // 0 + }; + + static readonly byte[] iso_magic = { + 0x50, // P + 0x53, // S + 0x49, // I + 0x53, // S + 0x4F, // O + 0x49, // I + 0x4D, // M + 0x47, // G + 0x30, // 0 + 0x30, // 0 + 0x30, // 0 + 0x30 // 0 + }; + + static T ReadStruct(BinaryReader reader) where T : struct + { + byte[] buff = reader.ReadBytes(Marshal.SizeOf()); + GCHandle handle = GCHandle.Alloc(buff, GCHandleType.Pinned); + T t = Marshal.PtrToStructure(handle.AddrOfPinnedObject()); + handle.Free(); + return t; + } + + static T ReadStruct(Stream stream) where T : struct + { + Span buff = stackalloc byte[Unsafe.SizeOf()]; + stream.Read(buff); + T t = MemoryMarshal.AsRef(buff); + return t; + } + + + static bool CopyNormalData(Stream input, Stream output, int offset, int size) + { + if (size == 0) + { + return true; + } + if (offset + size < input.Length) + { + input.Seek(offset, SeekOrigin.Begin); + var buff = new byte[size]; + var len = input.Read(buff); + if (len != size) + { + return false; + } + + output.Seek(offset, SeekOrigin.Begin); + output.Write(buff); + return true; + } + return false; + } + + static void XorTable(Span tp) + { + tp[4] ^= tp[3] ^ tp[2]; + tp[5] ^= tp[2] ^ tp[1]; + tp[6] ^= tp[0] ^ tp[3]; + tp[7] ^= tp[1] ^ tp[0]; + } + + static bool CopyNpUmdImg(Stream input, Stream output, PbpHeader pbpHdr, Span psarBuff, NpUmdImgHdr npHdr) + { + Span buff = stackalloc byte[0x800]; // + int len; + Span digest = stackalloc byte[0x14]; + SceDdrdb.sceDdrdbHash(psarBuff, 0xd8, digest); + Span point = stackalloc byte[Marshal.SizeOf()]; + KeyVault.Px2.AsSpan().CopyTo(point); + KeyVault.Py2.AsSpan().CopyTo(point[0x14..]); + var ret = SceDdrdb.sceDdrdbSigvry(point, digest, npHdr.ECDsaSig); + if (ret != 0) + { + return false; + } + + var vkey = new byte[0x10]; + Span mkey = stackalloc byte[Marshal.SizeOf()]; + AMCTRL.sceDrmBBMacInit(mkey, 3); + AMCTRL.sceDrmBBMacUpdate(mkey, psarBuff, 0xc0); + AMCTRL.bbmac_getkey(mkey, npHdr.HeaderHash, vkey); + + AMCTRL.sceDrmBBCipherInit(out var ckey, 1, 2, npHdr.HeaderKey, vkey, 0); + AMCTRL.sceDrmBBCipherUpdate(ref ckey, psarBuff[0x40..], 0x60); + AMCTRL.sceDrmBBCipherFinal(ref ckey); + npHdr = Utils.AsRef(psarBuff); + + var lbasize = npHdr.Body.LbaEnd - npHdr.Body.LbaStart + 1; + if (npHdr.BlockBasis == 0) + { + return false; + } + + var totalBlocks = (int)(lbasize / npHdr.BlockBasis + (lbasize % npHdr.BlockBasis != 0 ? 1 : 0)); + var tablesize = totalBlocks * 0x20; + + if ((npHdr.Body.Unk40 & (1 << (Idps[5] & 0x1F))) == 0) + { + return false; + } + //Span newVkey = npHdr.NpFlags switch + //{ + // 1 => Keys[CId].Fixed, + // 2 => _keyGen.GetVersionKey(), + // 3 => Keys[CId].Type3, + // _ => throw new NotSupportedException("unknown np flag") + //}; + + var paramSize = pbpHdr.Icon0Off - pbpHdr.ParamOff; + var dataPspSize = pbpHdr.DataPsarOff - pbpHdr.DataPspOff; + var buffSize = paramSize + 0x30; + if (buffSize > 0x800) + { + return false; + } + + if (pbpHdr.DataPspOff + dataPspSize >= input.Length) + { + return false; + } + + Span paramSpan = stackalloc byte[paramSize]; + input.Seek(pbpHdr.ParamOff, SeekOrigin.Begin); + input.Read(paramSpan); + output.Seek(pbpHdr.ParamOff, SeekOrigin.Begin); + output.Write(paramSpan); + paramSpan.CopyTo(buff); + npHdr.ContentId.CopyTo(buff[paramSize..]); + input.Seek(pbpHdr.DataPspOff, SeekOrigin.Begin); + len = input.Read(buff.Slice(paramSize + 0x30, 0x28)); + if (len < 0x28) + { + return false; + } + + SceDdrdb.sceDdrdbHash(buff, paramSize + 0x30, digest); + // sig.r = buff.Skip(paramSize + 0x30).Take(0x14).ToArray(); + // sig.s = buff.Skip(paramSize + 0x30 + 0x14).Take(0x14).ToArray(); + + ret = SceDdrdb.sceDdrdbSigvry(point, digest, buff.Slice(paramSize + 0x30, 0x28)); + if (ret != 0) + { + return false; + } + + var offset = input.Seek(pbpHdr.DataPspOff + 0x560, SeekOrigin.Begin); + if (offset != pbpHdr.DataPspOff + 0x560) + { + return false; + } + len = input.Read(buff.Slice(0, 0x34)); + if (len != 0x34) + { + return false; + } + + if (Encoding.ASCII.GetString(npHdr.ContentId).TrimEnd('\0') != + Encoding.ASCII.GetString(buff[..0x30]).TrimEnd('\0')) + { + return false; + } + + var bufidx = 0x33; + var off = Marshal.OffsetOf(nameof(npHdr.NpFlags)).ToInt32(); + for (int i = 0; i < 4; i++) + { + if (buff[bufidx - i] != psarBuff[off + i]) + { + return false; + } + } + + Span newCid = stackalloc byte[0x30]; + Encoding.ASCII.GetBytes(CId).AsSpan().CopyTo(newCid); + + // Copy PSP.DATA + var pspDataSize = pbpHdr.DataPsarOff - pbpHdr.DataPspOff; + Memory pspData = new byte[pspDataSize]; + input.Seek(pbpHdr.DataPspOff, SeekOrigin.Begin); + input.Read(pspData.Span); + + paramSpan.CopyTo(buff); + newCid.CopyTo(buff[paramSize..]); + + ECDsaHelper.SignParamSfo(buff[..(paramSize + 0x30)], pspData.Span); + newCid.CopyTo(pspData.Span[0x560..]); + var opnssmpOff = MemoryMarshal.Read(pspData.Span[0x30..]); + var opnssmpSize = MemoryMarshal.Read(pspData.Span[0x34..]); + if (opnssmpOff != 0 && opnssmpSize != 0) + { + var opnssmp = pspData.Slice(opnssmpOff, opnssmpSize); + using var ms = opnssmp.AsStream(); + using var dnas = new DNASStream(ms, 0); + Span opnssmpData = new byte[dnas.Length]; + dnas.Read(opnssmpData); + DNASHelper.Encrypt(pspData.Span[opnssmpOff..], opnssmpData, NewVersionKey.Span, opnssmpData.Length, dnas.KeyIndex, 1); + } + + output.Seek(pbpHdr.DataPspOff, SeekOrigin.Begin); + output.Write(pspData.Span); + + + AMCTRL.sceDrmBBMacInit(mkey, 3); + var entityOff = pbpHdr.DataPsarOff + npHdr.Body.BlockEntryOffset; + offset = input.Seek(entityOff, SeekOrigin.Begin); + if (offset == entityOff) + { + if (tablesize > 0) + { + buff = new byte[0x8000]; + for (int i = 0; i < tablesize; i += 0x8000) + { + var tmpsize = tablesize - i; + if (tmpsize > 0x8000) + { + tmpsize = 0x8000; + } + + len = input.Read(buff[..0x8000]); + if (len < 0x8000) + { + return false; + } + + // sceAmctrl_driver_9227EA79 + AMCTRL.sceDrmBBMacUpdate(mkey, buff, tmpsize); + + } + + ret = AMCTRL.sceDrmBBMacFinal2(mkey, npHdr.DataKey, vkey); + if (ret != 0) + { + return false; + } + } + else + { + return false; + } + } + else + { + return false; + } + input.Seek(entityOff, SeekOrigin.Begin); + Span table = new byte[tablesize]; + var tp = MemoryMarshal.Cast(table); + input.Read(table); + // Decrypt Table + for (int i = 0; i < totalBlocks; i++) + { + XorTable(tp[(i * 8)..]); + } + + var blocks = MemoryMarshal.Cast(table); + for (int i = 0; i < blocks.Length; i++) + { + Span blockData = new byte[blocks[i].Size]; + input.Seek(pbpHdr.DataPsarOff + blocks[i].Offset, SeekOrigin.Begin); + input.Read(blockData); + + // Verify MAC + AMCTRL.sceDrmBBMacInit(mkey, 3); + AMCTRL.sceDrmBBMacUpdate(mkey, blockData, blocks[i].Size); + ret = AMCTRL.sceDrmBBMacFinal2(mkey, blocks[i].Mac, vkey); + if (ret != 0) + { + return false; + } + + // Decrypt block + AMCTRL.sceDrmBBCipherInit(out ckey, 1, 2, npHdr.HeaderKey, vkey, blocks[i].Offset >> 4); + AMCTRL.sceDrmBBCipherUpdate(ref ckey, blockData, blocks[i].Size); + AMCTRL.sceDrmBBCipherFinal(ref ckey); + + // TODO LzrDecompress + + // Encrypt block + AMCTRL.sceDrmBBCipherInit(out ckey, 1, 2, npHdr.HeaderKey, NewVersionKey.Span, blocks[i].Offset >> 4); + AMCTRL.sceDrmBBCipherUpdate(ref ckey, blockData, blocks[i].Size); + AMCTRL.sceDrmBBCipherFinal(ref ckey); + + // Build Mac + AMCTRL.sceDrmBBMacInit(mkey, 3); + AMCTRL.sceDrmBBMacUpdate(mkey, blockData, blocks[i].Size); + AMCTRL.sceDrmBBMacFinal(mkey, blocks[i].Mac, NewVersionKey.Span); + Utils.BuildDrmBBMacFinal2(blocks[i].Mac); + output.Seek(pbpHdr.DataPsarOff + blocks[i].Offset, SeekOrigin.Begin); + output.Write(blockData); + } + + // Encrypt Table + for (int i = 0; i < totalBlocks; i++) + { + XorTable(tp[(i * 8)..]); + } + + output.Seek(entityOff, SeekOrigin.Begin); + output.Write(table); + + AMCTRL.sceDrmBBMacInit(mkey, 3); + AMCTRL.sceDrmBBMacUpdate(mkey, table, tablesize); + AMCTRL.sceDrmBBMacFinal(mkey, npHdr.DataKey, NewVersionKey.Span); + Utils.BuildDrmBBMacFinal2(npHdr.DataKey); + newCid.CopyTo(npHdr.ContentId); + + // Encrypt NPUMDIMG body. + AMCTRL.sceDrmBBCipherInit(out ckey, 1, 2, npHdr.HeaderKey, NewVersionKey.Span, 0); + AMCTRL.sceDrmBBCipherUpdate(ref ckey, psarBuff[0x40..], 0x60); + AMCTRL.sceDrmBBCipherFinal(ref ckey); + + // Generate header hash. + AMCTRL.sceDrmBBMacInit(mkey, 3); + AMCTRL.sceDrmBBMacUpdate(mkey, psarBuff, 0xC0); + AMCTRL.sceDrmBBMacFinal(mkey, npHdr.HeaderHash, NewVersionKey.Span); + Utils.BuildDrmBBMacFinal2(npHdr.HeaderHash); + + ECDsaHelper.SignNpImageHeader(psarBuff); + output.Seek(pbpHdr.DataPsarOff, SeekOrigin.Begin); + output.Write(psarBuff); + + return true; + } + + static int CopyStartData(Stream input, Stream output, int psarOffset) + { + int startdat_offset; + using var br = new BinaryReader(input, new UTF8Encoding(), true); + startdat_offset = br.ReadInt32(); + if (startdat_offset > 0) + { + // Read the STARTDAT header + br.BaseStream.Seek(psarOffset + startdat_offset, SeekOrigin.Begin); + var startdat_header = ReadStruct(br); + br.BaseStream.Seek(psarOffset + startdat_offset, SeekOrigin.Begin); + + // Read the STARTDAT data. + int startdat_size = startdat_header.header_size + startdat_header.data_size; + byte[] startdat_data = br.ReadBytes(startdat_size); + output.Seek(psarOffset + startdat_offset, SeekOrigin.Begin); + output.Write(startdat_data, 0, startdat_size); + } + return startdat_offset; + } + + static bool DecryptIsoHeader(Stream input, Span header, int header_offset, out int block_size) + { + // Seek to the ISO header. + // input.Seek(header_offset, SeekOrigin.Current); + + // Read the ISO header. + using var dnas = new DNASStream(input, input.Position + header_offset); + block_size = dnas.BlockSize; + dnas.VersionKey.CopyTo(VersionKey.Span); + if (header.Length == dnas.Length) + { + dnas.Read(header); + return true; + } + return false; + } + + static bool CopySimpleData(Stream input, Stream output, PbpHeader pbpHdr, int simple_data_offset) + { + if (simple_data_offset > 0) + { + using var dnas = new DNASStream(input, pbpHdr.DataPsarOff + simple_data_offset); + Span simpleData = new byte[dnas.Length]; + if (dnas.Read(simpleData) == dnas.Length) + { + var simpleDataEnc = new byte[input.Length - pbpHdr.DataPsarOff - simple_data_offset]; + DNASHelper.Encrypt(simpleDataEnc, simpleData, NewVersionKey.Span, simpleData.Length, dnas.KeyIndex, 1, blockSize: dnas.BlockSize); + output.Seek(pbpHdr.DataPsarOff + simple_data_offset, SeekOrigin.Begin); + output.Write(simpleDataEnc); + return true; + } + } + return false; + } + + static bool CopyUnknownData(Stream input, Stream output, PbpHeader pbpHdr, int unknown_data_offset, int startdat_offset) + { + if (unknown_data_offset > 0) + { + input.Seek(pbpHdr.DataPsarOff, SeekOrigin.Begin); + using var dnas = new DNASStream(input, input.Position + unknown_data_offset, VersionKey.Span); + Span unknownData = new byte[dnas.Length]; + if (dnas.Read(unknownData) == dnas.Length) + { + var unknownDataEnc = new byte[startdat_offset - unknown_data_offset]; + DNASHelper.Encrypt(unknownDataEnc, unknownData, NewVersionKey.Span, unknownData.Length, dnas.KeyIndex, 1, blockSize: dnas.BlockSize); + output.Seek(pbpHdr.DataPsarOff + unknown_data_offset, SeekOrigin.Begin); + output.Write(unknownDataEnc); + return true; + } + } + return false; + } + + static bool CopyAudio(Stream input, Stream output, ReadOnlySpan iso_table, PbpHeader pbpHdr, int base_offset = 0) + { + + // Set CDDA entry. + CDDA_ENTRY audio_entry; + + var iso_offset = MemoryMarshal.Read(iso_table[0x7fc..]); + + // Start of the ISO data. + var iso_base_offset = iso_offset + base_offset; + + // Start the audio track number counter at 2 (data track is always the first one). + int audio_track_count = 2; + + + + // Read the audio track table (starts at 0x800 and ends at offset 0xE20). + for (var audio_offset = 0x800; audio_offset < 0xE20; audio_offset += Unsafe.SizeOf()) + { + // Read the CDDA entry. + audio_entry = MemoryMarshal.Read(iso_table[audio_offset..]); + + // Reached the last entry. + if (audio_entry.offset == 0) + { + break; + } + + // Locate the block offset in the DATA.PSAR. + input.Seek(pbpHdr.DataPsarOff + iso_base_offset + audio_entry.offset, SeekOrigin.Begin); + output.Seek(pbpHdr.DataPsarOff + iso_base_offset + audio_entry.offset, SeekOrigin.Begin); + + // Read the data. + Span track_data = new byte[audio_entry.size]; + input.Read(track_data); + output.Write(track_data); + + // Increment the track counter. + audio_track_count++; + } + return true; + } + + static bool CopyIso(Stream input, Stream output, ReadOnlySpan iso_table, Span header, int base_offset, PbpHeader pbpHdr, int hdrBlockSize) + { + var header_offset = base_offset + 0x400; + var header_size = 0xB6600; + + // Setup buffers. + int iso_block_size = 0x9300; + Span iso_block_comp = new byte[iso_block_size]; // Compressed block. + byte[] iso_block_decomp = new byte[iso_block_size]; // Decompressed block. + + // Locate the block table. + int table_offset = 0x3C00; // Fixed offset. + + int iso_base_offset = 0x100000 + base_offset; // Start of compressed ISO data. + + // Read the first entry. + var entry = MemoryMarshal.Read(iso_table[table_offset..]); + var entries = MemoryMarshal.Cast(header[0x3C00..]); + var i = 0; + + Span mkey = stackalloc byte[Marshal.SizeOf()]; + // Keep reading entries until we reach the end of the table. + while (entry.size > 0) + { + // Locate the block offset in the DATA.PSAR. + var block = iso_block_comp[..entry.size]; + input.Seek(pbpHdr.DataPsarOff + iso_base_offset + entry.offset, SeekOrigin.Begin); + input.Read(block); + output.Seek(pbpHdr.DataPsarOff + iso_base_offset + entry.offset, SeekOrigin.Begin); + output.Write(block); + + PspCrypto.AMCTRL.sceDrmBBMacInit(mkey, 3); + PspCrypto.AMCTRL.sceDrmBBMacUpdate(mkey, iso_block_comp, entry.size); + int ret = PspCrypto.AMCTRL.sceDrmBBMacFinal2(mkey, entries[i].checksum, VersionKey.Span); + if (ret != 0) + { + Console.WriteLine("ERROR: BLOCK CROP"); + return false; + } + PspCrypto.AMCTRL.sceDrmBBMacInit(mkey, 3); + PspCrypto.AMCTRL.sceDrmBBMacUpdate(mkey, iso_block_comp, entry.size); + Span checksum = new byte[20 + 0x10]; + PspCrypto.AMCTRL.sceDrmBBMacFinal(mkey, checksum[20..], NewVersionKey.Span); + + ref var aesHdr = ref MemoryMarshal.AsRef(checksum); + aesHdr.mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC; + aesHdr.keyseed = 0x63; + aesHdr.data_size = 0x10; + PspCrypto.KIRKEngine.sceUtilsBufferCopyWithRange(checksum, 0x10, checksum, 0x10, + KIRKEngine.KIRK_CMD_ENCRYPT_IV_0); + checksum.Slice(20, 0x10).CopyTo(entries[i].checksum); + // Go to next entry. + table_offset += Marshal.SizeOf(); + entry = MemoryMarshal.Read(iso_table[table_offset..]); + i++; + } + Span headerEnc = new byte[header_size]; + DNASHelper.Encrypt(headerEnc, header, NewVersionKey.Span, header.Length, 1, 1, blockSize: hdrBlockSize); + output.Seek(pbpHdr.DataPsarOff + header_offset, SeekOrigin.Begin); + output.Write(headerEnc); + output.Flush(); + + return true; + } + + static bool CopyPsxLoader(Stream input, Stream output, PbpHeader pbpHdr) + { + var pspDataSize = pbpHdr.DataPsarOff - pbpHdr.DataPspOff; + Memory loader = new byte[pspDataSize]; + input.Seek(pbpHdr.DataPspOff, SeekOrigin.Begin); + input.Read(loader.Span); + + Span config = new byte[0x410]; + loader.Span.Slice(0x150, 0x410).CopyTo(config); + var hdr = MemoryMarshal.AsRef(loader.Span); + var ret = SceMesgLed.sceMesgLed_driver_EBB4613D(loader.Span, hdr.pspSize, out var newSize, VersionKey.Span); + if (ret != 0) + { + return false; + } + Span decMod; + if (loader.Span[0] == 0x1f && loader.Span[1] == 0x8b) + { + using var ms = loader.AsStream(); + using var gz = new GZipStream(ms, CompressionMode.Decompress); + using var decMs = new MemoryStream(); + gz.CopyTo(decMs); + decMod = decMs.ToArray(); + } + else + { + decMod = loader.Span[..newSize]; + } + + + Span loaderEnc = new byte[pspDataSize]; + var key = Convert.ToHexString(NewVersionKey.Span); + SceMesgLed.Encrypt(loaderEnc, decMod, hdr.tag, SceExecFileDecryptMode.DECRYPT_MODE_POPS_EXEC, NewVersionKey.Span, CId, config); + output.Seek(pbpHdr.DataPspOff, SeekOrigin.Begin); + output.Write(loaderEnc); + return true; + } + + static bool CopyPsIsoImg(Stream input, Stream output, PbpHeader pbpHdr) + { + input.Seek(pbpHdr.DataPsarOff + 12, SeekOrigin.Begin); + int startdat_offset = CopyStartData(input, output, pbpHdr.DataPsarOff); + + // Decrypt the ISO header and get the block table. + // NOTE: In a single disc, the ISO header is located at offset 0x400 and has a length of 0xB6600. + Span header = new byte[0x400 + 0xb3880]; + input.Seek(pbpHdr.DataPsarOff, SeekOrigin.Begin); + input.Read(header[..0x400]); + + output.Seek(pbpHdr.DataPsarOff, SeekOrigin.Begin); + output.Write(header[..0x400]); + Span iso_table = header[0x400..]; + + input.Seek(pbpHdr.DataPsarOff, SeekOrigin.Begin); + if (!DecryptIsoHeader(input, iso_table, 0x400, out var hdrBlockSize)) + { + return false; + } + + // Save the ISO disc name and title (UTF-8). + ReadOnlySpan iso_disc_name = iso_table[..0x10]; + ReadOnlySpan iso_title = iso_table.Slice(0xE2C, 0x80); + string iso_disc_name_utf8 = Encoding.UTF8.GetString(iso_disc_name).TrimEnd('\0'); + string iso_title_utf8 = Encoding.UTF8.GetString(iso_title).TrimEnd('\0'); + Console.WriteLine($"ISO disc: {iso_disc_name_utf8}"); + Console.WriteLine($"ISO title: {iso_title_utf8}\n"); + + // Seek inside the ISO table to find the SIMPLE data offset. + int simple_data_offset = MemoryMarshal.Read(iso_table[0xE20..]); + + if (!CopySimpleData(input, output, pbpHdr, simple_data_offset)) + { + Console.WriteLine("CopySimpleData failed!"); + return false; + } + + // Seek inside the ISO table to find the unknown data offset. + int unknown_data_offset = MemoryMarshal.Read(iso_table[0xEDE..]); + + if (unknown_data_offset > 0) + { + if (!CopyUnknownData(input, output, pbpHdr, unknown_data_offset, startdat_offset)) + { + Console.WriteLine("CopyUnknownData failed"); + return false; + } + } + + // Extract the CDDA tracks. + if (!CopyAudio(input, output, iso_table, pbpHdr)) + { + Console.WriteLine("CopyAudio failed"); + return false; + } + + if (!CopyIso(input, output, iso_table, header[0x400..], 0, pbpHdr, hdrBlockSize)) + { + Console.WriteLine("CopyIso failed"); + return false; + } + + if (!CopyPsxLoader(input, output, pbpHdr)) + { + Console.WriteLine("CopyPsxLoader failed"); + return false; + } + + return true; + } + + + static bool ReadIsoMap(Stream input, PbpHeader pbpHdr, Span isoMap) + { + var mapOffset = 0x200; + using var dnas = new DNASStream(input, pbpHdr.DataPsarOff + mapOffset, VersionKey.Span); + Span buffer = new byte[dnas.Length]; + if (dnas.Read(buffer) == dnas.Length) + { + buffer.CopyTo(isoMap); + return true; + } + return false; + } + + static void WriteIsoMap(Stream output, PbpHeader pbpHdr, ReadOnlySpan isoMap) + { + var mapOffset = 0x200; + var mapSize = 0x2A0; + var isoMapEnc = new byte[mapSize]; + DNASHelper.Encrypt(isoMapEnc, isoMap, NewVersionKey.Span, isoMap.Length, 1, 1); + output.Seek(pbpHdr.DataPsarOff + mapOffset, SeekOrigin.Begin); + output.Write(isoMapEnc); + } + + static bool CopyPsTitleImg(Stream input, Stream output, PbpHeader pbpHdr) + { + input.Seek(pbpHdr.DataPsarOff, SeekOrigin.Begin); + output.Seek(pbpHdr.DataPsarOff, SeekOrigin.Begin); + Span psTitle = new byte[200]; + input.Read(psTitle); + output.Write(psTitle); + output.Flush(); + + input.Seek(pbpHdr.DataPsarOff + 16, SeekOrigin.Begin); + int startdat_offset = CopyStartData(input, output, pbpHdr.DataPsarOff); + Span isoMap = new byte[0x200]; + if (!ReadIsoMap(input, pbpHdr, isoMap)) + { + Console.WriteLine("ReadIsoMap failed"); + return false; + } + + Span discOffsets = isoMap[..20].Cast(); + Span macKeys = isoMap.Slice(20, 16 * 5); + + ReadOnlySpan iso_disc_name = isoMap.Slice(0x64, 0x20); + ReadOnlySpan iso_title = isoMap.Slice(0x10C, 0x80); + string iso_disc_name_utf8; + string iso_title_utf8; + unsafe + { + fixed (byte* ptr = iso_disc_name) + { + ReadOnlySpan disc = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr); + iso_disc_name_utf8 = Encoding.UTF8.GetString(disc); + } + fixed (byte* ptr = iso_title) + { + ReadOnlySpan title = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr); + iso_title_utf8 = Encoding.UTF8.GetString(title); + } + } + Console.WriteLine($"ISO disc: {iso_disc_name_utf8}"); + Console.WriteLine($"ISO title: {iso_title_utf8}\n"); + + int simple_data_offset = MemoryMarshal.Read(isoMap[0x84..]); + if (!CopySimpleData(input, output, pbpHdr, simple_data_offset)) + { + Console.WriteLine("CopySimpleData failed"); + return false; + } + + + // Build each valid ISO image. + Span mkey = stackalloc byte[Marshal.SizeOf()]; + for (int i = 0; i < discOffsets.Length; i++) + { + var diskOffset = discOffsets[i]; + if (diskOffset > 0) + { + Span header = new byte[0x400 + 0xb3880]; + input.Seek(pbpHdr.DataPsarOff + diskOffset, SeekOrigin.Begin); + input.Read(header[..0x400]); + output.Seek(pbpHdr.DataPsarOff + diskOffset, SeekOrigin.Begin); + output.Write(header[..0x400]); + Span iso_table = header[0x400..]; + + input.Seek(pbpHdr.DataPsarOff + diskOffset, SeekOrigin.Begin); + if (!DecryptIsoHeader(input, iso_table, 0x400, out var hdrBlockSize)) + { + return false; + } + int ret = PspCrypto.AMCTRL.sceDrmBBMacInit(mkey, 3); + ret = PspCrypto.AMCTRL.sceDrmBBMacUpdate(mkey, header, 0xb3c80); + ret = PspCrypto.AMCTRL.sceDrmBBMacFinal2(mkey, macKeys[(i * 0x10)..], VersionKey.Span); + if (ret != 0) + { + Console.WriteLine("ERROR: Header miss match"); + return false; + } + + var unknown = MemoryMarshal.Read(iso_table[0xEDE..]); + Console.WriteLine($"unknown {unknown}"); + + // Extract the CDDA tracks. + if (!CopyAudio(input, output, iso_table, pbpHdr, diskOffset)) + { + Console.WriteLine("CopyAudio failed"); + return false; + } + + if (!CopyIso(input, output, iso_table, header[0x400..], diskOffset, pbpHdr, hdrBlockSize)) + { + Console.WriteLine($"CopyIso disc{i} failed"); + return false; + } + ret = PspCrypto.AMCTRL.sceDrmBBMacInit(mkey, 3); + ret = PspCrypto.AMCTRL.sceDrmBBMacUpdate(mkey, header, 0xb3c80); + Span newKey = new byte[20 + 0x10]; + ret = PspCrypto.AMCTRL.sceDrmBBMacFinal(mkey, newKey[20..], NewVersionKey.Span); + ref var aesHdr = ref MemoryMarshal.AsRef(newKey); + aesHdr.mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC; + aesHdr.keyseed = 0x63; + aesHdr.data_size = 0x10; + PspCrypto.KIRKEngine.sceUtilsBufferCopyWithRange(newKey, 0x10, newKey, 0x10, + KIRKEngine.KIRK_CMD_ENCRYPT_IV_0); + newKey.Slice(20, 0x10).CopyTo(macKeys[(i * 0x10)..]); + } + } + WriteIsoMap(output, pbpHdr, isoMap); + + if (!CopyPsxLoader(input, output, pbpHdr)) + { + Console.WriteLine("CopyPsxLoader failed"); + return false; + } + + return true; + } + + static int GetKeyType(Stream input, PbpHeader pbpHdr) + { + input.Seek(pbpHdr.DataPspOff + 0x560, SeekOrigin.Begin); + Span contentInfo = new byte[0x34]; + input.Read(contentInfo); + var keyType = BinaryPrimitives.ReadInt32BigEndian(contentInfo[0x30..]); + return keyType; + } + + static bool CopyData(Stream input, Stream output, PbpHeader pbpHdr, out int type) + { + type = -1; + input.Seek(pbpHdr.DataPsarOff, SeekOrigin.Begin); + Span psarBuff = stackalloc byte[0x100]; + var len = input.Read(psarBuff); + if (len != 0x100) + { + return false; + } + + ref var npHdr = ref Utils.AsRef(psarBuff); + + if (npHdr.Magic0 == 0x4d55504e && npHdr.Magic1 == 0x474d4944) + { + int ret = _keyGen.GetVersionKey(NewVersionKey.Span, npHdr.NpFlags); + if (ret != 0) + { + Console.WriteLine($"GetVersionKey {npHdr.NpFlags} failed"); + return false; + } + type = 0; + return CopyNpUmdImg(input, output, pbpHdr, psarBuff, npHdr); + } + + if (psarBuff[..12].SequenceEqual(iso_magic)) + { + var keyType = GetKeyType(input, pbpHdr); + int ret = _keyGen.GetVersionKey(NewVersionKey.Span, keyType); + if (ret != 0) + { + Console.WriteLine("GetVersionKey 1 failed"); + return false; + } + type = 1; + DiscInfo[] discs = new DiscInfo[2]; + discs[0] = new DiscInfo("ABEE\\D1.CUE", "Oddworld: Abe's Exoddus", "SLES01480"); + discs[1] = new DiscInfo("ABEE\\D2.CUE", "Oddworld: Abe's Exoddus", "SLES11480"); + PsTitleImg title = new PsTitleImg(NewVersionKey.ToArray(), CId, discs); + title.CreatePsar(); + PbpBuilder.CreatePbp(File.ReadAllBytes("TEST\\PARAM.SFO"), File.ReadAllBytes("TEST\\ICON0.PNG"), null, + File.ReadAllBytes("TEST\\PIC0.PNG"), File.ReadAllBytes("TEST\\PIC1.PNG"), null, + title.GenerateDataPsp(), title, "ABE-EBOOT.PBP"); + //PsIsoImg i = new PsIsoImg("ROLLCAGE\\ROLLCAGE.CUE", "SLUS00800", "ROLLCAGE", CId, NewVersionKey.ToArray(), + // File.ReadAllBytes("TEST\\PARAM.SFO"), File.ReadAllBytes("TEST\\ICON0.PNG"), null, + // File.ReadAllBytes("TEST\\PIC0.PNG"), File.ReadAllBytes("TEST\\PIC1.PNG"), null); + //File.WriteAllBytes("TEST.BIN", i.GetIsoHeader()); + //File.WriteAllBytes("TEST.ISOc", i.GetIso()); + + + return CopyPsIsoImg(input, output, pbpHdr); + } + + if (psarBuff[..16].SequenceEqual(multi_iso_magic)) + { + var keyType = GetKeyType(input, pbpHdr); + int ret = _keyGen.GetVersionKey(NewVersionKey.Span, keyType); + if (ret != 0) + { + Console.WriteLine("GetVersionKey 1 failed"); + return false; + } + type = 2; + return CopyPsTitleImg(input, output, pbpHdr); + } + + return false; + } + + class KeyGen + { + private readonly byte[] _actData; + private readonly byte[] _rifData; + + public KeyGen(string rifName) + { + _actData = File.ReadAllBytes("act.dat"); + _rifData = File.ReadAllBytes(rifName); + } + + public int GetVersionKey(Span versionKey, int type) => SceNpDrm.sceNpDrmGetVersionKey(versionKey, _actData, _rifData, type); + } + + static bool CopyDocument(string src, string dist) + { + using var fs = File.OpenRead(src); + + return true; + } + + + + static void Main(string[] args) + { + if (args.Length != 3) + { + Console.Write("Usage PbpResign.exe accountId contentId input"); + return; + } + + if (!File.Exists("act.dat")) + { + Console.WriteLine("act.dat not exist"); + return; + } + if (!File.Exists("psid")) + { + Console.WriteLine("psid not exist"); + return; + } + + var aid = args[0]; + if (aid.Length < 16) + { + aid = aid.PadLeft(16, '0'); + } + var aidData = Convert.FromHexString(aid); + SceNpDrm.Aid = BitConverter.ToUInt64(aidData); + var cmaKey = CmaKeys.GenerateKey(aid); + Console.WriteLine(Convert.ToHexString(cmaKey)); + + var srcPbp = args[2]; + if (!File.Exists(srcPbp)) + { + Console.WriteLine($"{srcPbp} not exist"); + return; + } + + Idps = File.ReadAllBytes("psid"); + SceNpDrm.SetPSID(Idps); + CId = args[1]; + var rifName = $"{CId}.rif"; + if (!File.Exists(rifName)) + { + Console.WriteLine($"{rifName} not exist"); + return; + } + + _keyGen = new KeyGen(rifName); + + + var srcPath = Path.GetDirectoryName(srcPbp); + + // try + // { + using var input = File.OpenRead(srcPbp); + var hdr = new byte[Marshal.SizeOf()]; + var len = input.Read(hdr); + if (len != hdr.Length) + { + Console.WriteLine("Wrong input"); + return; + } + + var pbpHdr = Utils.AsRef(hdr); + if (pbpHdr.Sig != 0x50425000) + { + Console.WriteLine("Wrong pbp sig"); + return; + } + + input.Seek(pbpHdr.ParamOff, SeekOrigin.Begin); + var paramSize = pbpHdr.Icon0Off - pbpHdr.ParamOff; + Span param = new byte[paramSize]; + input.Read(param); + var sfoDic = Sfo.ReadSfo(param); + string distPath; + if (sfoDic["DISC_ID"] is string diskId) + { + distPath = Path.Combine(srcPath, $"signed\\game\\ux0_pspemu_temp_game_PSP_GAME_{diskId}"); + } + else + { + Console.WriteLine("Can't get diskid"); + return; + } + if (!Directory.Exists(distPath)) + { + Directory.CreateDirectory(distPath); + } + + var psxDoc = Path.Combine(srcPath, "DOCUMENT.DAT"); + var psxDistDoc = Path.Combine(distPath, "DOCUMENT.DAT"); + if (File.Exists(psxDoc)) + { + File.Copy(psxDoc, psxDistDoc, true); + } + + var vitaPath = Path.Combine(distPath, "VITA_PATH.TXT"); + var vitaPathData = $"ux0:pspemu/temp/game/PSP/GAME/{diskId}\0"; + File.WriteAllText(vitaPath, vitaPathData); + var licensePath = Path.Combine(srcPath, $"signed\\license\\ux0_pspemu_temp_game_PSP_LICENSE"); + if (!Directory.Exists(licensePath)) + { + Directory.CreateDirectory(licensePath); + } + var distLicense = Path.Combine(licensePath, rifName); + File.Copy(rifName, distLicense, true); + vitaPath = Path.Combine(licensePath, "VITA_PATH.TXT"); + vitaPathData = "ux0:pspemu/temp/game/PSP/LICENSE\0"; + File.WriteAllText(vitaPath, vitaPathData); + + var sceSysPath = Path.Combine(srcPath, "signed\\sce_sys"); + if (!Directory.Exists(sceSysPath)) + { + Directory.CreateDirectory(sceSysPath); + } + + var paramPath = Path.Combine(sceSysPath, "param.sfo"); + using (var fs = File.Create(paramPath)) + { + fs.Write(param); + fs.Flush(); + } + + + var distPbp = Path.Combine(distPath, "EBOOT.PBP"); + + using var output = File.OpenWrite(distPbp); + output.SetLength(input.Length); + output.Write(hdr); + + if (!CopyNormalData(input, output, pbpHdr.ParamOff, paramSize)) + { + Console.WriteLine("Wrong PARAM.SFO data"); + return; + } + + + var icon0Size = pbpHdr.Icon1Off - pbpHdr.Icon0Off; + if (!CopyNormalData(input, output, pbpHdr.Icon0Off, icon0Size)) + { + Console.WriteLine("Wrong ICON0.PNG data"); + return; + } + Span icon0 = new byte[icon0Size]; + input.Seek(pbpHdr.Icon0Off, SeekOrigin.Begin); + input.Read(icon0); + var icon0Path = Path.Combine(sceSysPath, "icon0.png"); + using (var fs = File.Create(icon0Path)) + { + fs.Write(icon0); + fs.Flush(); + } + + + var icon1Size = pbpHdr.Pic0Off - pbpHdr.Icon1Off; + if (!CopyNormalData(input, output, pbpHdr.Icon1Off, icon1Size)) + { + Console.WriteLine("Wrong ICON1.PMF/ICON1.PNG data"); + return; + } + + var pic0Size = pbpHdr.Pic1Off - pbpHdr.Pic0Off; + if (!CopyNormalData(input, output, pbpHdr.Pic0Off, pic0Size)) + { + Console.WriteLine("Wrong PIC0.PNG/UNKNOWN.PNG data"); + return; + } + + var pic1Size = pbpHdr.Snd0Off - pbpHdr.Pic1Off; + if (!CopyNormalData(input, output, pbpHdr.Pic1Off, pic1Size)) + { + Console.WriteLine("Wrong PIC1.PNG/PICT1.PNG data"); + return; + } + + var snd0Size = pbpHdr.DataPspOff - pbpHdr.Snd0Off; + if (!CopyNormalData(input, output, pbpHdr.Snd0Off, snd0Size)) + { + Console.WriteLine("Wrong SND0.AT3 data"); + return; + } + + if (!CopyData(input, output, pbpHdr, out var type)) + { + Console.WriteLine("Wrong DATA.PSP/DATA.PSAR data"); + } + else + { + Console.WriteLine($"Resign {srcPbp} to {distPbp} Successful"); + var sigPath = Path.Combine(distPath, "__sce_ebootpbp"); + input.Close(); + output.Close(); + Span ebootsig = stackalloc byte[0x200]; + switch (type) + { + case 0: + SceNpDrm.KsceNpDrmEbootSigGenPsp(distPbp, ebootsig, 0x3600000); + File.WriteAllBytes(sigPath, ebootsig.ToArray()); + break; + case 1: + SceNpDrm.KsceNpDrmEbootSigGenPs1(distPbp, ebootsig, 0x3600000); + File.WriteAllBytes(sigPath, ebootsig.ToArray()); + break; + case 2: + SceNpDrm.KsceNpDrmEbootSigGenPs1(distPbp, ebootsig, 0x3600000); + File.WriteAllBytes(sigPath, ebootsig.ToArray()); + break; + } + //var srcDir = Path.GetDirectoryName(srcPbp); + //var documentPath = Path.Combine(srcDir, "DOCUMENT.DAT"); + //var newDocumentPath = Path.Combine(distDir, "DOCUMENT_MOD.DAT"); + //if (File.Exists(documentPath)) + //{ + // CopyDocument(documentPath, newDocumentPath); + //} + } + + //} + //catch (Exception e) + //{ + // Console.WriteLine(e); + //} + + } + } +} diff --git a/PbpResign/Sfo.cs b/PbpResign/Sfo.cs new file mode 100644 index 0000000..f952763 --- /dev/null +++ b/PbpResign/Sfo.cs @@ -0,0 +1,78 @@ +using CommunityToolkit.HighPerformance; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace PbpResign +{ + internal class Sfo + { + private struct SfoHeader + { + public uint Magic; + public uint Version; + public int KeyTableStart; + public int DataTableStart; + public int TablesEntries; + } + + private struct SfoIndexTableEntry + { + public ushort KeyOffset; + public ushort DataFormat; + public int DataLen; + public int DataMaxLen; + public int DataOffset; + } + + private const ushort PSF_TYPE_BIN = 0x0004; + private const ushort PSF_TYPE_STR = 0x0204; + private const ushort PSF_TYPE_VAL = 0x0404; + + public static Dictionary ReadSfo(ReadOnlySpan sfo) + { + var dic = new Dictionary(); + var hdr = MemoryMarshal.Read(sfo); + if (hdr.Magic == 0x46535000) + { + dic = new Dictionary(hdr.TablesEntries); + var entries = MemoryMarshal.Cast(sfo.Slice(20, hdr.TablesEntries * 16)); + unsafe + { + foreach (var entry in entries) + { + var keyOffset = hdr.KeyTableStart + entry.KeyOffset; + string keyName; + fixed (byte* ptr = sfo[keyOffset..]) + { + var strData = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr); + keyName = Encoding.UTF8.GetString(strData); + } + + var dataOffset = hdr.DataTableStart + entry.DataOffset; + var dataLen = entry.DataLen; + var maxLen = entry.DataMaxLen; + var data = sfo.Slice(dataOffset, dataLen); + + switch (entry.DataFormat) + { + case PSF_TYPE_BIN: + dic[keyName] = data.ToArray(); + break; + case PSF_TYPE_STR: + dic[keyName] = Encoding.UTF8.GetString(data).TrimEnd('\0'); + break; + case PSF_TYPE_VAL: + dic[keyName] = MemoryMarshal.Read(data); + break; + } + } + } + } + return dic; + } + } +} diff --git a/PopsBuilder/Atrac3/Atrac3ToolEncoder.cs b/PopsBuilder/Atrac3/Atrac3ToolEncoder.cs new file mode 100644 index 0000000..ee8d10d --- /dev/null +++ b/PopsBuilder/Atrac3/Atrac3ToolEncoder.cs @@ -0,0 +1,155 @@ +using Org.BouncyCastle.Crypto.IO; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Channels; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace PopsBuilder.Atrac3 +{ + public class Atrac3ToolEncoder : IAtracEncoderBase + { + private static Random rng = new Random(); + private static string TOOLS_DIRECTORY = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tools"); + + private static string AT3TOOL_WIN = Path.Combine(TOOLS_DIRECTORY, "at3tool.exe"); + private static string AT3TOOL_LINUX = Path.Combine(TOOLS_DIRECTORY, "at3tool.elf"); + + private static string TEMP_DIRECTORY = Path.Combine(Path.GetTempPath(), "at3tool_tmp"); + + // random name so that can generate multiple at once if wanted .. + private string TEMP_WAV; + private string TEMP_AT3; + public Atrac3ToolEncoder() + { + string rdmPart = rng.Next().ToString("X"); + + TEMP_WAV = Path.Combine(TEMP_DIRECTORY, rdmPart + "_tmp.wav"); + TEMP_AT3 = Path.Combine(TEMP_DIRECTORY, rdmPart + "_tmp.at3"); + + } + + private static string AT3TOOL_LOCATION + { + get + { + if (OperatingSystem.IsWindows()) + return AT3TOOL_WIN; + else if (OperatingSystem.IsLinux()) + return AT3TOOL_LINUX; + else + throw new PlatformNotSupportedException("No at3tool binary for your platform"); + } + } + + private void runAtrac3Tool() + { + using(Process proc = new Process()) + { + proc.StartInfo.FileName = AT3TOOL_LOCATION; + proc.StartInfo.Arguments = "-br 132 -e \"" + TEMP_WAV + "\" \"" + TEMP_AT3 + "\""; + + proc.StartInfo.UseShellExecute = false; + proc.StartInfo.CreateNoWindow = true; + proc.StartInfo.RedirectStandardOutput = true; + proc.StartInfo.RedirectStandardInput = true; + + proc.Start(); + proc.WaitForExit(); + + string stdout = proc.StandardOutput.ReadToEnd(); + if (!stdout.Contains("Total Encoded Bytes")) + throw new Exception(stdout); + } + } + + private byte[] stripAtracHeader() + { + using(FileStream at3Stream = File.OpenRead(TEMP_AT3)) + { + StreamUtil at3Util = new StreamUtil(at3Stream); + at3Stream.Seek(0x4C, SeekOrigin.Begin); + int at3Len = at3Util.ReadInt32(); + return at3Util.ReadBytes(at3Len); + } + } + + private void makeWav(byte[] pcmData) + { + using (FileStream wavStream = File.Open(TEMP_WAV, FileMode.Create)) + { + // CD-AUDIO standard settings + int fileSize = pcmData.Length; + int samplerate = 44100; + short channels = 2; // channels + short format = 16; // signed, 16 bit PCM + + StreamUtil wavUtil = new StreamUtil(wavStream); + + wavUtil.WriteStr("RIFF"); + wavUtil.WriteInt32((fileSize + (0x2C - 8))); + + wavUtil.WriteStr("WAVE"); + wavUtil.WriteStr("fmt "); + wavUtil.WriteInt32(format); + + wavUtil.WriteInt16(1); + wavUtil.WriteInt16(channels); + wavUtil.WriteInt32(samplerate); + wavUtil.WriteInt32((samplerate * format * channels) / 8); + wavUtil.WriteInt16(Convert.ToInt16(format * channels)); + wavUtil.WriteInt16(format); + + wavUtil.WriteStr("data"); + wavUtil.WriteInt32(fileSize); + wavUtil.WriteBytes(pcmData); + } + } + + private void ensureFilesAvailable() + { + if (!Directory.Exists(TEMP_DIRECTORY)) + Directory.CreateDirectory(TEMP_DIRECTORY); + + if (!Directory.Exists(TOOLS_DIRECTORY)) + Directory.CreateDirectory(TOOLS_DIRECTORY); + + if (OperatingSystem.IsWindows()) + { + if (!File.Exists(AT3TOOL_WIN)) + { + throw new FileNotFoundException("Cannot find at3tool at " + AT3TOOL_WIN); + } + } + else if(OperatingSystem.IsLinux()) + { + if (!File.Exists(AT3TOOL_LINUX)) + { + throw new FileNotFoundException("Cannot find at3tool at " + AT3TOOL_LINUX); + } + } + } + + private void cleanup() + { + if (File.Exists(TEMP_WAV)) File.Delete(TEMP_WAV); + if (File.Exists(TEMP_AT3)) File.Delete(TEMP_AT3); + } + + public byte[] EncodeToAtrac(byte[] pcmData) + { + ensureFilesAvailable(); + + makeWav(pcmData); + runAtrac3Tool(); + byte[] rawAtracData = stripAtracHeader(); + + cleanup(); + + return rawAtracData; + } + } +} diff --git a/PopsBuilder/Atrac3/IAtracEncoderBase.cs b/PopsBuilder/Atrac3/IAtracEncoderBase.cs new file mode 100644 index 0000000..a3fe97d --- /dev/null +++ b/PopsBuilder/Atrac3/IAtracEncoderBase.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Atrac3 +{ + public interface IAtracEncoderBase + { + public byte[] EncodeToAtrac(byte[] pcmData); + } +} diff --git a/PopsBuilder/Cue/CueIndex.cs b/PopsBuilder/Cue/CueIndex.cs new file mode 100644 index 0000000..d8be24b --- /dev/null +++ b/PopsBuilder/Cue/CueIndex.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Cue +{ + public class CueIndex + { + public byte IndexNumber; + public short Mrel; + public short Srel; + public short Frel; + + public short Mdelta; + public short Sdelta; + public short Fdelta; + + internal int mTtl + { + get + { + return (Mrel + Mdelta); + } + } + + internal int sTtl + { + get + { + return (Srel + Sdelta); + } + } + + internal int fTtl + { + get + { + return (Frel + Fdelta); + } + } + + public byte m + { + get + { + int carryF = Convert.ToInt32(Math.Floor(Convert.ToDouble(fTtl) / 75.0)); + int carryS = Convert.ToInt32(Math.Floor(Convert.ToDouble(sTtl + carryF) / 60.0)); + + return Convert.ToByte(mTtl + carryS); + } + } + + public byte s + { + get + { + int carryF = Convert.ToInt32(Math.Floor(Convert.ToDouble(fTtl) / 75.0)); + + return Convert.ToByte(((Srel + Sdelta) + carryF) % 60); + } + } + + public byte f + { + get + { + return Convert.ToByte((fTtl) % 75); + } + } + public byte M + { + get + { + return CueReader.BinaryDecimalConv(m); + } + } + + public byte S + { + get + { + return CueReader.BinaryDecimalConv(s); + } + } + + public byte F + { + get + { + return CueReader.BinaryDecimalConv(f); + } + } + + internal CueIndex(byte indexNumber) + { + IndexNumber = indexNumber; + Mrel = 0; + Srel = 0; + Frel = 0; + Mdelta = 0; + Sdelta = 0; + Fdelta = 0; + } + } +} diff --git a/PopsBuilder/Cue/CueReader.cs b/PopsBuilder/Cue/CueReader.cs new file mode 100644 index 0000000..9402bd0 --- /dev/null +++ b/PopsBuilder/Cue/CueReader.cs @@ -0,0 +1,382 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Cue +{ + public class CueReader : IDisposable + { + public int FirstDataTrackNo + { + get + { + return getFirstDataTrackNo(); + } + } + + + private CueTrack[] tracks = new CueTrack[99]; + private Dictionary openTracks; + + public int GetTotalTracks() + { + int totalTracks = 0; + for(int i = 0; i < tracks.Length; i++) + if (tracks[i] is not null) totalTracks++; + return totalTracks; + } + + public static byte BinaryDecimalConv(int i) + { + return Convert.ToByte(Convert.ToInt32((i % 10) + 16 * ((i / 10) % 10))); + } + public int IdxToSectorRel(CueIndex index) + { + int offset = (((index.Mrel * 60) + index.Srel) * 75 + index.Frel); + return offset; + } + public int IdxToSector(CueIndex index) + { + int offset = (((index.m * 60) + index.s) * 75 + index.f); + return offset; + } + + public CueIndex SectorToIdx(int sector) + { + CueIndex idx = new CueIndex(1); + + int x = sector; + int f = sector % 75; + x = x - f; + x = Convert.ToInt32(Math.Floor(Convert.ToDouble(x) / 75.0)); + int s = x % 60; + int m = Convert.ToInt32(Math.Floor(Convert.ToDouble(x) / 60.0)); + + //idx.Mrel = Convert.ToByte(Convert.ToInt32(((sector / 75) / 60))); + //idx.Srel = Convert.ToByte(Convert.ToInt32(((sector / 75) % 60))); + //idx.Frel = Convert.ToByte(Convert.ToInt32(((sector % 75)))); + //idx.Sdelta = 2; // why? + + idx.Mrel = Convert.ToInt16(m); + idx.Srel = Convert.ToInt16(s); + idx.Frel = Convert.ToInt16(f); + + return idx; + } + + private int getFirstDataTrackNo() + { + foreach (CueTrack track in tracks) + if (track is not null) + if (track.TrackType == TrackType.TRACK_MODE2_2352) return track.TrackNo; + + // no non-data tracks? + return 1; + } + private void setTrackNumber(int trackNo, ref CueTrack? track) + { + tracks[trackNo - 1] = track; + } + public CueTrack GetTrackNumber(int trackNo) + { + return tracks[trackNo - 1]; + } + private int findTrackSz(int trackNo) + { + CueTrack track = GetTrackNumber(trackNo); + // total iso (size / sector size) + int startSector = IdxToSector(track.TrackIndex[1]); + int fileSectorSz = Convert.ToInt32(track.binFileSz / track.SectorSz); + int endSector = Convert.ToInt32(startSector + fileSectorSz); + + // check if another track begins (thus ending this one) + for (int i = 0; i < tracks.Length; i++) + { + CueTrack? cTrack = tracks[i]; + if (cTrack is not null) + { + if (cTrack.TrackNo <= track.TrackNo) continue; + int sector = IdxToSector(cTrack.TrackIndex[0]); + + if (sector < endSector) endSector = sector; + } + } + + int sectorsLength = (endSector - startSector); + return sectorsLength; + } + + public CueStream OpenTrack(int trackNo) + { + if (!openTracks.ContainsKey(trackNo)) + { + CueTrack track = GetTrackNumber(trackNo); + int sectorStart = IdxToSectorRel(track.TrackIndex[1]); + int sectorLen = findTrackSz(trackNo); + + CueStream trackBin = new CueStream(File.OpenRead(track.binFileName), sectorStart * track.SectorSz, sectorLen * track.SectorSz); + openTracks[trackNo] = trackBin; + return trackBin; + } + else + { + CueStream openTrack = openTracks[trackNo]; + if (!openTrack.IsClosed) + { + openTracks.Remove(trackNo); + return OpenTrack(trackNo); + } + else + { + openTracks[trackNo].Seek(0x00, SeekOrigin.Begin); + return openTracks[trackNo]; + } + } + + } + + private int getLastTrackNo() + { + int trackNo = 0; + for (int i = 0; i < tracks.Length; i++) + { + if (tracks[i] is null) continue; + trackNo = tracks[i].TrackNo; + } + return trackNo; + } + private int getTotalSectorSz() + { + int sectors = 0; + HashSet countedBins = new HashSet(); + + for(int i = 0; i < tracks.Length; i++) + { + if (tracks[i] is null) continue; + if (!countedBins.Contains(tracks[i].binFileName)) + { + countedBins.Add(tracks[i].binFileName); + sectors += Convert.ToInt32(tracks[i].binFileSz / CueTrack.MODE2_SECTOR_SZ); + } + } + return sectors; + } + + private bool haveAudioTracks + { + get + { + for (int i = 0; i < tracks.Length; i++) + { + if (tracks[i] is null) continue; + if (tracks[i].TrackType == TrackType.TRACK_CDDA) return true; + } + return false; + } + } + + private byte[] createDummyTracks() + { + // every psn ps1 game have "A0" track that points to sector 6000 (MSF 01 20 00) + byte[] tocA0Entry = new byte[10] { 0x41, 0x00, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x01, 0x20, 0x00 }; + + // And an A1 track (determines how many tracks there are) + byte[] tocA1Entry = new byte[10] { 0x41, 0x00, 0xA1, 0x00, 0x00, 0x00, 0x00, BinaryDecimalConv(GetTotalTracks()), 0x00, 0x00 }; + + // the A2 track is a bit more complicated .. + int totalSectors = getTotalSectorSz(); + CueIndex idx = SectorToIdx(totalSectors); + idx.Sdelta = 2; + + byte[] tocA2Entry = new byte[10] { 0x41, 0x00, 0xA2, 0x00, 0x00, 0x00, 0x00, idx.M, idx.S, idx.F }; + + if (GetTrackNumber(getLastTrackNo()).TrackType == TrackType.TRACK_CDDA) + { + tocA2Entry[0x00] = 0x01; + tocA1Entry[0x00] = 0x01; + } + + byte[] tocDummy = new byte[10 * 3]; + Array.ConstrainedCopy(tocA0Entry, 0, tocDummy, 0, 10); + Array.ConstrainedCopy(tocA1Entry, 0, tocDummy, 10, 10); + Array.ConstrainedCopy(tocA2Entry, 0, tocDummy, 20, 10); + + return tocDummy; + } + + private int getTrackSectorOnDisc(int trackNo) + { + int absolutePosition = 0; + for(int i = 0; i < tracks.Length; i++) + { + if (tracks[i] is null) continue; + if (tracks[i].TrackNo == trackNo) break; + + absolutePosition += findTrackSz(tracks[i].TrackNo); + } + return absolutePosition; + } + + private void fixUpMsf() + { + + Dictionary positions = new Dictionary(); + int totalPosition = 0; + + for (int i = 0; i < tracks.Length; i++) + { + if (tracks[i] is null) continue; + + if (!positions.ContainsKey(tracks[i].binFileName)) + { + positions[tracks[i].binFileName] = totalPosition; + double sz = Convert.ToDouble(tracks[i].binFileSz) / Convert.ToDouble(tracks[i].SectorSz); + + totalPosition += Convert.ToInt32(sz); + } + + } + + for (int i = 0; i < tracks.Length; i++) + { + if (tracks[i] is null) continue; + int pos = positions[tracks[i].binFileName]; + + CueIndex idx = this.SectorToIdx(pos); + // pregap not included on first track + + if (tracks[i].TrackNo == 1) tracks[i].TrackIndex[0].Sdelta = 0; + + // add pregap + idx.Mdelta = Convert.ToInt16(tracks[i].TrackIndex[1].Mrel - tracks[i].TrackIndex[0].Mrel); + idx.Sdelta = Convert.ToInt16(tracks[i].TrackIndex[1].Srel - tracks[i].TrackIndex[0].Srel); + idx.Fdelta = Convert.ToInt16(tracks[i].TrackIndex[1].Frel - tracks[i].TrackIndex[0].Frel); + + tracks[i].TrackIndex[0].Mdelta = idx.m; + tracks[i].TrackIndex[0].Sdelta = idx.s; + tracks[i].TrackIndex[0].Fdelta = idx.f; + + // index is always ofset by 2 thou + idx.Sdelta = 2; + tracks[i].TrackIndex[1].Mdelta = idx.m; + tracks[i].TrackIndex[1].Sdelta = idx.s; + tracks[i].TrackIndex[1].Fdelta = idx.f; + } + } + + public byte[] CreateToc() + { + using (MemoryStream toc = new MemoryStream()) + { + StreamUtil tocUtil = new StreamUtil(toc); + tocUtil.WriteBytes(createDummyTracks()); + + for (int trackNo = 0; trackNo < tracks.Length; trackNo++) + { + if (tracks[trackNo] is not null) + { + tocUtil.WriteBytes(tracks[trackNo].ToTocEntry()); + } + } + + int remain = Convert.ToInt32(0x3F0 - toc.Length); + tocUtil.WritePadding(0x00, remain); + + toc.Seek(0x00, SeekOrigin.Begin); + return toc.ToArray(); + } + } + + public void Dispose() + { + foreach(CueStream openTrack in openTracks.Values) + { + if(openTrack.IsClosed) + openTrack.Close(); + } + openTracks.Clear(); + } + + public CueReader(string cueFile) + { + openTracks = new Dictionary(); + for (int trackNo = 0; trackNo < tracks.Length; trackNo++) tracks[trackNo] = null; + + using (TextReader cueReader = File.OpenText(cueFile)) + { + CueTrack? curTrack = null; + + for (string? cueData = cueReader.ReadLine(); + cueData != null; + cueData = cueReader.ReadLine()) + { + string[] cueLn = cueData.Trim().Replace("\r", "").Replace("\n", "").Split(' '); + + if (cueData.StartsWith(" ")) // index of track + { + if (cueLn[0] == "INDEX") + { + if (curTrack is null) throw new Exception("tried to create new index, when track was null"); + + int indexNumber = Convert.ToByte(int.Parse(cueLn[1])); + string[] msf = cueLn[2].Split(':'); + + curTrack.TrackIndex[indexNumber].Mrel = Convert.ToByte(Int32.Parse(msf[0])); + curTrack.TrackIndex[indexNumber].Srel = Convert.ToByte(Int32.Parse(msf[1])); + curTrack.TrackIndex[indexNumber].Frel = Convert.ToByte(Int32.Parse(msf[2])); + + setTrackNumber(curTrack.TrackNo, ref curTrack); + } + } + else if (cueData.StartsWith(" ")) // start of new track + { + if (cueLn[0] == "TRACK") + { + if (curTrack is null) throw new Exception("tried to create new track, when track was null"); + + if (curTrack.TrackNo != 0xFF) + { + setTrackNumber(curTrack.TrackNo, ref curTrack); + curTrack = new CueTrack(curTrack.binFileName); + } + + curTrack.TrackNo = Convert.ToByte(int.Parse(cueLn[1])); + if (cueLn[2] == "MODE2/2352") + curTrack.TrackType = TrackType.TRACK_MODE2_2352; + else if (cueLn[2] == "AUDIO") + curTrack.TrackType = TrackType.TRACK_CDDA; + setTrackNumber(curTrack.TrackNo, ref curTrack); + } + } + else // new file + { + if (cueLn[0] == "FILE") + { + if (curTrack != null) setTrackNumber(curTrack.TrackNo, ref curTrack); + + // parse out filename.. + string[] cueFnameParts = new string[cueLn.Length - 2]; + Array.ConstrainedCopy(cueLn, 1, cueFnameParts, 0, cueFnameParts.Length); + string cueFname = String.Join(' ', cueFnameParts); + + // open file .. + string binFileName = cueFname.Substring(1, cueFname.Length - 2); + string? folderContainingCue = Path.GetDirectoryName(cueFile); + + if (folderContainingCue != null) + binFileName = Path.Combine(folderContainingCue, binFileName); + + curTrack = new CueTrack(binFileName); + } + } + + } + } + + fixUpMsf(); + } + } +} diff --git a/PopsBuilder/Cue/CueStream.cs b/PopsBuilder/Cue/CueStream.cs new file mode 100644 index 0000000..407696c --- /dev/null +++ b/PopsBuilder/Cue/CueStream.cs @@ -0,0 +1,152 @@ +using Org.BouncyCastle.Tls.Crypto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Cue +{ + public class CueStream : Stream + { + internal long position; + internal long start; + internal long end; + internal long length; + public bool IsClosed; + + private Stream baseStream; + public CueStream(Stream s, long start, long length) + { + this.IsClosed = false; + this.baseStream = s; + this.start = start; + this.length = length; + this.end = start + length; + + s.Seek(start, SeekOrigin.Begin); + } + private long remainLength + { + get + { + return this.Length - this.Position; + } + } + + public override bool CanRead + { + get + { + return this.baseStream.CanRead; + } + } + + public override bool CanSeek + { + + get + { + return this.baseStream.CanSeek; + } + } + + public override bool CanWrite + { + get + { + return this.baseStream.CanWrite; + } + } + + public override long Length + { + get + { + return this.length; + } + } + + public override long Position + { + get + { + return this.position; + } + set + { + this.position = value; + if (this.position > this.end) this.position = this.end; + if (this.position < 0) this.position = 0; + + this.baseStream.Position = (start + this.position); + } + } + private void seekToPos() + { + if (this.baseStream.Position != this.position) + this.baseStream.Seek(start + this.position, SeekOrigin.Begin); + } + public override void Close() + { + IsClosed = true; + this.baseStream.Dispose(); + base.Close(); + } + public override void Flush() + { + this.baseStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + seekToPos(); + + int nCount = count; + if (nCount > remainLength) nCount = Convert.ToInt32(remainLength); + if (nCount < 0) nCount = 0; + + int read = this.baseStream.Read(buffer, offset, count); + this.position += read; + return read; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + default: + case SeekOrigin.Begin: + this.Position = offset; + break; + + case SeekOrigin.Current: + this.Position += offset; + break; + + case SeekOrigin.End: + this.Position = this.Length - offset; + break; + } + + return position; + } + + public override void SetLength(long value) + { + throw new NotImplementedException("Cannot set length of CueStream."); + } + + public override void Write(byte[] buffer, int offset, int count) + { + seekToPos(); + + int nCount = count; + if (nCount > remainLength) nCount = Convert.ToInt32(remainLength); + if (nCount < 0) nCount = 0; + + this.baseStream.Write(buffer, offset, count); + this.position += nCount; + } + } +} diff --git a/PopsBuilder/Cue/CueTrack.cs b/PopsBuilder/Cue/CueTrack.cs new file mode 100644 index 0000000..df966e7 --- /dev/null +++ b/PopsBuilder/Cue/CueTrack.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Cue +{ + public class CueTrack + { + + public const int MODE2_SECTOR_SZ = 2352; + public const int CDDA_SECTOR_SZ = 2352; + + public TrackType TrackType; + public byte TrackNo; + public CueIndex[] TrackIndex; + + public int TrackLength; + public int SectorSz + { + get + { + if (TrackType == TrackType.TRACK_CDDA) return CDDA_SECTOR_SZ; + else return MODE2_SECTOR_SZ; + } + } + internal long binFileSz; + internal string binFileName; + + internal CueTrack(string binFile) + { + TrackIndex = new CueIndex[2]; + for (int i = 0; i < TrackIndex.Length; i++) + TrackIndex[i] = new CueIndex(Convert.ToByte(i)); + + binFileName = binFile; + binFileSz = new FileInfo(binFileName).Length; + + TrackType = TrackType.TRACK_MODE2_2352; + TrackNo = 0xFF; + } + + public byte[] ToTocEntry() + { + byte[] tocEntry = new byte[10]; + + tocEntry[0] = Convert.ToByte(this.TrackType); + tocEntry[1] = 0; + tocEntry[2] = CueReader.BinaryDecimalConv(this.TrackNo); + + + tocEntry[3] = this.TrackIndex[0].M; + tocEntry[4] = this.TrackIndex[0].S; + tocEntry[5] = this.TrackIndex[0].F; + tocEntry[6] = 0; + tocEntry[7] = this.TrackIndex[1].M; + tocEntry[8] = this.TrackIndex[1].S; + tocEntry[9] = this.TrackIndex[1].F; + + return tocEntry; + } + + } +} diff --git a/PopsBuilder/Cue/TrackType.cs b/PopsBuilder/Cue/TrackType.cs new file mode 100644 index 0000000..582478c --- /dev/null +++ b/PopsBuilder/Cue/TrackType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Cue +{ + public enum TrackType + { + TRACK_MODE2_2352 = 0x41, + TRACK_CDDA = 0x01 + } +} diff --git a/PopsBuilder/Pops/DiscCompressor.cs b/PopsBuilder/Pops/DiscCompressor.cs new file mode 100644 index 0000000..57fdfc9 --- /dev/null +++ b/PopsBuilder/Pops/DiscCompressor.cs @@ -0,0 +1,234 @@ +using PopsBuilder.Atrac3; +using PopsBuilder.Cue; +using PopsBuilder.Psp; +using PspCrypto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Pops +{ + public class DiscCompressor + { + const int COMPRESS_BLOCK_SZ = 0x9300; + const int DEFAULT_ISO_OFFSET = 0x100000; + public int IsoOffset; + + internal DiscCompressor(NpDrmPsar srcImg, DiscInfo disc, IAtracEncoderBase encoder, int offset = DEFAULT_ISO_OFFSET) + { + this.srcImg = srcImg; + this.disc = disc; + this.cue = new CueReader(disc.CueFile); + + this.IsoHeader = new MemoryStream(); + this.CompressedIso = new MemoryStream(); + + this.isoHeaderUtil = new StreamUtil(IsoHeader); + this.atrac3Encoder = encoder; + this.IsoOffset = offset; + } + + + private void writeCompressedIsoBlock(Stream s) + { + byte[] isoBlock = new byte[COMPRESS_BLOCK_SZ]; + int read = s.Read(isoBlock, 0, isoBlock.Length); + + byte[] compressed = Lz.compress(isoBlock); + + ushort sz = Convert.ToUInt16(compressed.Length); + int ptr = Convert.ToInt32(CompressedIso.Position); + writeIsoTblEntry(ptr, sz, compressed); + + CompressedIso.Write(compressed, 0, compressed.Length); + + } + + + private void writeIsoTblEntry(int ptr, ushort sz, byte[] data) + { + isoHeaderUtil.WriteInt32(ptr); + isoHeaderUtil.WriteUInt16(sz); + + isoHeaderUtil.WriteInt16(1); // mark that this is part of the image. + + isoHeaderUtil.WriteBytes(calculatePs1CompressedIsoSegmentChecksum(data)); + + isoHeaderUtil.WritePadding(0x00, 0x8); + } + + + private void writeHeader() + { + isoHeaderUtil.WriteStrWithPadding(disc.DiscIdHdr, 0x00, 0x400); + } + + + public byte[] GenerateIsoPgd() + { + IsoHeader.Seek(0x0, SeekOrigin.Begin); + byte[] isoHdr = IsoHeader.ToArray(); + + int headerSize = DNASHelper.CalculateSize(isoHdr.Length, 0x400); + byte[] headerEnc = new byte[headerSize]; + + int sz = DNASHelper.Encrypt(headerEnc, isoHdr, srcImg.VersionKey, isoHdr.Length, 1, 1, blockSize: 0x400); + byte[] isoHdrPgd = headerEnc.ToArray(); + Array.Resize(ref isoHdrPgd, sz); + + return isoHdrPgd; + } + + private void writeIsoLocation() + { + isoHeaderUtil.WriteInt32(0); + isoHeaderUtil.WriteInt32(0); + isoHeaderUtil.WriteInt32(0); + + isoHeaderUtil.WriteInt32(IsoOffset); // always 0x100000 on single disc game + + isoHeaderUtil.WritePadding(0x00, 0x620); + } + + private void writeCompressedIso() + { + using (CueStream cueStr = cue.OpenTrack(cue.FirstDataTrackNo)) + { + using (EccRemoverStream eccRem = new EccRemoverStream(cueStr)) + { + while (eccRem.Position < eccRem.Length) + { + Console.Write(Math.Floor(Convert.ToDouble(eccRem.Position) / Convert.ToDouble(eccRem.Length) * 100.0) + "%\r"); + writeCompressedIsoBlock(eccRem); + } + } + } + } + public void GenerateIsoHeaderAndCompress() + { + writeHeader(); + writeTOC(); + writeIsoLocation(); + writeName(); + + writeCompressedIso(); + + isoHeaderUtil.PadUntil(0x0, 0xb3880); + + // now write CD-Audio data. + writeCompressedCDATracks(); + } + + public void WriteSimpleDatLocation(Int64 location) + { + IsoHeader.Seek(0xE20, SeekOrigin.Begin); + isoHeaderUtil.WriteInt64(location); + } + private void writeName() + { + // copied from crash bandicoot warped + + isoHeaderUtil.WriteInt64(0x00); // SIMPLE.DAT location + + isoHeaderUtil.WriteInt32(2047); // unk + isoHeaderUtil.WriteStrWithPadding(disc.DiscName, 0x00, 0x80); + isoHeaderUtil.WriteInt32(3); // unk + + isoHeaderUtil.WriteInt32(0x72d0ee59); // appears to be constant? + isoHeaderUtil.WriteInt32(0); + isoHeaderUtil.WriteInt32(0); + isoHeaderUtil.WriteInt32(0); + + isoHeaderUtil.WritePadding(0, 0x2D40); + } + + private void writeCDAEntry(int position, int length, uint key) + { + isoHeaderUtil.WriteInt32(position); + isoHeaderUtil.WriteInt32(length); + isoHeaderUtil.WriteInt32(0); + isoHeaderUtil.WriteUInt32(key); + } + + private void writeCompressedCDATracks() + { + Random rng = new Random(); + + IsoHeader.Seek(0x800, SeekOrigin.Begin); // CDA Entries + + int totalTracks = cue.GetTotalTracks(); + for (int i = 1; i <= totalTracks; i++) + { + if (cue.GetTrackNumber(i).TrackType != TrackType.TRACK_CDDA) continue; + + Console.WriteLine("Encoding track " + i + " to ATRAC3."); + + using (CueStream audioStream = cue.OpenTrack(i)) + { + uint key = Convert.ToUInt32(rng.NextInt64(0, uint.MaxValue)); + + Atrac3ToolEncoder enc = new Atrac3ToolEncoder(); + + byte[] pcmData = new byte[audioStream.Length]; + audioStream.Read(pcmData, 0x00, pcmData.Length); + + byte[] atracData = enc.EncodeToAtrac(pcmData); + + writeCDAEntry(Convert.ToInt32(CompressedIso.Position), atracData.Length, key); + + using (MemoryStream atracStream = new MemoryStream(atracData)) + { + using (MemoryStream encryptedAtracStream = new MemoryStream()) + { + AtracCrypto.ScrambleAtracData(atracStream, encryptedAtracStream, key); + encryptedAtracStream.Seek(0x00, SeekOrigin.Begin); + encryptedAtracStream.CopyTo(CompressedIso); + } + } + + } + } + } + + private byte[] calculatePs1CompressedIsoSegmentChecksum(byte[] data) + { + byte[] outChecksum = new byte[0x10]; + + Span mkey = stackalloc byte[Marshal.SizeOf()]; + + AMCTRL.sceDrmBBMacInit(mkey, 3); + AMCTRL.sceDrmBBMacUpdate(mkey, data, data.Length); + Span checksum = new byte[20 + 0x10]; + AMCTRL.sceDrmBBMacFinal(mkey, checksum[20..], srcImg.VersionKey); + + ref var aesHdr = ref MemoryMarshal.AsRef(checksum); + aesHdr.mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC; + aesHdr.keyseed = 0x63; + aesHdr.data_size = 0x10; + KIRKEngine.sceUtilsBufferCopyWithRange(checksum, 0x10, checksum, 0x10, KIRKEngine.KIRK_CMD_ENCRYPT_IV_0); + + checksum.Slice(20, 0x10).CopyTo(outChecksum); + + return outChecksum; + } + + private void writeTOC() + { + isoHeaderUtil.WriteBytes(cue.CreateToc()); + } + + + private DiscInfo disc; + private CueReader cue; + private NpDrmPsar srcImg; + + public MemoryStream IsoHeader; + public MemoryStream CompressedIso; + + private StreamUtil isoHeaderUtil; + private IAtracEncoderBase atrac3Encoder; + } +} diff --git a/PopsBuilder/Pops/DiscInfo.cs b/PopsBuilder/Pops/DiscInfo.cs new file mode 100644 index 0000000..13a0baf --- /dev/null +++ b/PopsBuilder/Pops/DiscInfo.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Pops +{ + public class DiscInfo + { + private string cueFile; + private string discName; + private string discId; + + public string CueFile + { get + { + return cueFile; + } + } + + public string DiscIdHdr + { + get + { + return "_" + DiscId.Substring(0, 4) + "_" + DiscId.Substring(4, 5); + } + } + public string DiscName + { + get + { + return discName; + } + } + + public string DiscId + { + get + { + return discId.Replace("-", "").Replace("_", "").ToUpperInvariant(); + } + } + + public DiscInfo(string cueFile, string discName, string discId) + { + this.cueFile = cueFile; + this.discName = discName; + this.discId = discId; + } + } +} diff --git a/PopsBuilder/Pops/EccRemoverStream.cs b/PopsBuilder/Pops/EccRemoverStream.cs new file mode 100644 index 0000000..f1a6d6f --- /dev/null +++ b/PopsBuilder/Pops/EccRemoverStream.cs @@ -0,0 +1,226 @@ +using PopsBuilder.Cue; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Pops +{ + public class EccRemoverStream : Stream + { + private byte[] currentSector; + + private long position; + private Stream baseStream; + public EccRemoverStream(Stream s) + { + baseStream = s; + currentSector = new byte[CueTrack.MODE2_SECTOR_SZ]; + + invalidateSectorCache(); + } + private int positionInSector + { + get + { + return Convert.ToInt32(position % CueTrack.MODE2_SECTOR_SZ); + } + } + private int remainInSector + { + get + { + return CueTrack.MODE2_SECTOR_SZ - positionInSector; + } + } + private int positionSector + { + get + { + return findSector(position); + } + } + public Stream BaseStream + { + get + { + return baseStream; + } + } + public override bool CanRead + { + get + { + return baseStream.CanRead; + } + } + + public override bool CanSeek + { + get + { + return baseStream.CanSeek; + } + } + + public override bool CanWrite + { + get + { + return false; + } + } + + public override long Length + { + get + { + return baseStream.Length; + } + } + + public override long Position + { + get + { + return position; + } + set + { + long newPos = value; + + if (newPos < 0) newPos = 0; + if (newPos > Length) newPos = Length; + + + int oldSector = positionSector; + position = newPos; + + if (positionSector != oldSector) + invalidateSectorCache(); + } + } + + + private void removeEcc() + { + // clear current sync + Array.Fill(currentSector, (byte)0x00, 0x1, 0x0A); + + // remove MSF .. + currentSector[0x0C] = 0x00; // M + currentSector[0x0D] = 0x00; // S + currentSector[0x0E] = 0x00; // F + + // remove ecc + + // (only if this is not form2mode2 sector!) + if (!(currentSector[0xF] == 0x2 && (currentSector[0x12] & 0x20) == 0x20)) + Array.Fill(currentSector, (byte)0x00, 0x818, 0x118); + else if (position > 0x9300) // only clear if its past the system section .. + Array.Fill(currentSector, (byte)0x00, 0x92C, 0x4); + } + + private int findSector(long position) + { + long len = position; + len -= len % CueTrack.MODE2_SECTOR_SZ; + int sector = Convert.ToInt32(len / CueTrack.MODE2_SECTOR_SZ); + return sector; + } + + private long sectorToPos(int sector) + { + return sector * CueTrack.MODE2_SECTOR_SZ; + } + + private void seekToSector(int sector) + { + baseStream.Seek(sectorToPos(sector), SeekOrigin.Begin); + } + private void invalidateSectorCache() + { + + int sector = findSector(position); + seekToSector(sector); + baseStream.Read(currentSector, 0x00, currentSector.Length); + removeEcc(); + + } + public override void Close() + { + baseStream.Close(); + base.Close(); + } + public override void Flush() + { + baseStream.Flush(); + } + public override int Read(byte[] buffer, int offset, int count) + { + int effectiveCount = count; + + if (Position > Length) return 0; + + if (Position + effectiveCount > Length) effectiveCount = Convert.ToInt32(Length - Position); + + if (effectiveCount <= remainInSector) // read the data from the cached sector + { + Array.ConstrainedCopy(currentSector, positionInSector, buffer, offset, effectiveCount); + } + else if (effectiveCount > remainInSector) // read 1 sector at a time until count reached + { + int remain = effectiveCount; + int total = 0; + + while (remain > 0) + { + int toRead = Math.Min(remain, remainInSector); + int totalRead = Read(buffer, total + offset, toRead); + + if (totalRead < toRead) break; + + remain -= totalRead; + total += totalRead; + } + + return total; + } + + Position += effectiveCount; + return effectiveCount; + } + + public override long Seek(long offset, SeekOrigin origin) + { + switch (origin) + { + default: + case SeekOrigin.Begin: + Position = offset; + break; + + case SeekOrigin.Current: + Position += offset; + break; + + case SeekOrigin.End: + Position = Length - offset; + break; + } + + return position; + } + + public override void SetLength(long value) + { + throw new NotImplementedException("EccRemoverStream is read only."); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException("EccRemoverStream is read only."); + } + } +} diff --git a/PopsBuilder/Pops/PopsImg.cs b/PopsBuilder/Pops/PopsImg.cs new file mode 100644 index 0000000..1a46480 --- /dev/null +++ b/PopsBuilder/Pops/PopsImg.cs @@ -0,0 +1,105 @@ +using Org.BouncyCastle.Crypto.Paddings; +using PopsBuilder.Psp; +using PspCrypto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Pops +{ + public class PopsImg : NpDrmPsar + { + public PopsImg(byte[] versionKey, string contentId) : base(versionKey, contentId) + { + startDat = new MemoryStream(); + startDatUtil = new StreamUtil(startDat); + + simple = new MemoryStream(); + simpleUtil = new StreamUtil(simple); + + createStartDat(); + createSimpleDat(); + + SimplePgd = generateSimplePgd(); + + } + internal MemoryStream startDat; + internal StreamUtil startDatUtil; + + private MemoryStream simple; + private StreamUtil simpleUtil; + public byte[] SimplePgd; + internal Random rng = new Random(); + private void createSimpleDat() + { + simpleUtil.WriteStr("SIMPLE "); + simpleUtil.WriteInt32(100); + simpleUtil.WriteInt32(16); + simpleUtil.WriteInt32(Resources.SIMPLE.Length); + simpleUtil.WriteInt32(0); + simpleUtil.WriteInt32(0); + + simpleUtil.WriteBytes(Resources.SIMPLE); + } + + private void createStartDat() + { + startDatUtil.WriteStr("STARTDAT"); + startDatUtil.WriteInt32(0x1); + startDatUtil.WriteInt32(0x1); + startDatUtil.WriteInt32(0x50); + startDatUtil.WriteInt32(Resources.STARTDAT.Length); + startDatUtil.WriteInt32(0x0); + startDatUtil.WriteInt32(0x0); + + startDatUtil.WritePadding(0, 0x30); + + startDatUtil.WriteBytes(Resources.STARTDAT); + } + + private byte[] generateSimplePgd() + { + simple.Seek(0x0, SeekOrigin.Begin); + byte[] simpleData = simple.ToArray(); + + int simpleSz = DNASHelper.CalculateSize(simpleData.Length, 0x400); + byte[] simpleEnc = new byte[simpleSz]; + + // get pgd + int sz = DNASHelper.Encrypt(simpleEnc, simpleData, VersionKey, simpleData.Length, 1, 1, blockSize: 0x400); + byte[] pgd = simpleEnc.ToArray(); + Array.Resize(ref pgd, sz); + + return pgd; + } + + + public byte[] GenerateDataPsp() + { + Span loaderEnc = new byte[0x9B13]; + + byte[] dataPspElf = Resources.DATAPSPSD; + + // calculate size low and high part .. + uint szLow = Convert.ToUInt32(Psar.Length) >> 16; + uint szHigh = Convert.ToUInt32(Psar.Length) & 0xFFFF; + + // convert to big endain bytes + byte[] lowBits = BitConverter.GetBytes(Convert.ToUInt16(szLow)).ToArray(); + byte[] highBits = BitConverter.GetBytes(Convert.ToUInt16(szHigh)).ToArray(); + + // overwrite data.psar size check .. + Array.ConstrainedCopy(lowBits, 0, dataPspElf, 0x68C, 0x2); + Array.ConstrainedCopy(highBits, 0, dataPspElf, 0x694, 0x2); + + SceMesgLed.Encrypt(loaderEnc, dataPspElf, 0x0DAA06F0, SceExecFileDecryptMode.DECRYPT_MODE_POPS_EXEC, VersionKey, ContentId, Resources.DATAPSPSDCFG); + return loaderEnc.ToArray(); + } + + + + } +} diff --git a/PopsBuilder/Pops/PsIsoImg.cs b/PopsBuilder/Pops/PsIsoImg.cs new file mode 100644 index 0000000..0a96f2e --- /dev/null +++ b/PopsBuilder/Pops/PsIsoImg.cs @@ -0,0 +1,65 @@ +using Org.BouncyCastle.Crypto.Paddings; +using PopsBuilder.Atrac3; +using PopsBuilder.Cue; +using PspCrypto; +using System; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; + +namespace PopsBuilder.Pops +{ + public class PsIsoImg : PopsImg + { + internal PsIsoImg(byte[] versionkey, string contentId, DiscCompressor discCompressor) : base(versionkey, contentId) + { + this.compressor = discCompressor; + } + + public PsIsoImg(byte[] versionkey, string contentId, DiscInfo disc, IAtracEncoderBase encoder) : base(versionkey, contentId) + { + this.compressor = new DiscCompressor(this, disc, encoder); + } + + public PsIsoImg(byte[] versionkey, string contentId, DiscInfo disc) : base(versionkey, contentId) + { + this.compressor = new DiscCompressor(this, disc, new Atrac3ToolEncoder()); + } + public void CreatePsar(bool isPartOfMultiDisc=false) + { + compressor.GenerateIsoHeaderAndCompress(); + if (!isPartOfMultiDisc) compressor.WriteSimpleDatLocation((compressor.IsoOffset + compressor.CompressedIso.Length) + startDat.Length); + + psarUtil.WriteStr("PSISOIMG0000"); + psarUtil.WriteInt64(0x00); // location of STARTDAT + + psarUtil.WritePadding(0x00, 0x3ec); // Skip forwards + + byte[] isoHdrPgd = compressor.GenerateIsoPgd(); + psarUtil.WriteBytes(isoHdrPgd); + psarUtil.PadUntil(0x00, compressor.IsoOffset); + + compressor.CompressedIso.Seek(0x00, SeekOrigin.Begin); + compressor.CompressedIso.CopyTo(Psar); + + Psar.Seek(0x00, SeekOrigin.Begin); + if (isPartOfMultiDisc) return; + + // write STARTDAT + Int64 startDatLocation = Psar.Position; + startDat.Seek(0x00, SeekOrigin.Begin); + startDat.CopyTo(Psar); + + // write pgd + psarUtil.WriteBytes(this.SimplePgd); + + // set STARTDAT location + Psar.Seek(0xC, SeekOrigin.Begin); + psarUtil.WriteInt64(startDatLocation); + + Psar.Seek(0x00, SeekOrigin.Begin); + } + private DiscCompressor compressor; + + } +} \ No newline at end of file diff --git a/PopsBuilder/Pops/PsTitleImg.cs b/PopsBuilder/Pops/PsTitleImg.cs new file mode 100644 index 0000000..0ed7b6e --- /dev/null +++ b/PopsBuilder/Pops/PsTitleImg.cs @@ -0,0 +1,153 @@ +using PopsBuilder.Atrac3; +using PspCrypto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Pops +{ + public class PsTitleImg : PopsImg + { + const int MAX_DISCS = 5; + const int PSISO_ALIGN = 0x8000; + public PsTitleImg(byte[] versionKey, string contentId, DiscInfo[] discs) : base(versionKey, contentId) + { + if (discs.Length > MAX_DISCS) throw new Exception("Sorry, multi disc games only support up to 5 discs... (i dont make the rules)"); + this.compressors = new DiscCompressor[MAX_DISCS]; + this.discs = discs; + + for (int i = 0; i < compressors.Length; i++) + { + if (i > (discs.Length - 1)) compressors[i] = null; + else compressors[i] = new DiscCompressor(this, discs[i], new Atrac3ToolEncoder()); + } + + + isoMap = new MemoryStream(); + isoMapUtil = new StreamUtil(isoMap); + + isoPart = new MemoryStream(); + isoPartUtil = new StreamUtil(isoPart); + + + } + + public void CreatePsar() + { + createIsoMap(); + + psarUtil.WriteStr("PSTITLEIMG000000"); + psarUtil.WriteInt64(PSISO_ALIGN+isoPart.Length); // location of STARTDAT + + psarUtil.WriteRandom(0x10); // dunno what this is + psarUtil.WritePadding(0x00, 0x1D8); + + byte[] isoMap = generateIsoMapPgd(); + psarUtil.WriteBytes(isoMap); + psarUtil.PadUntil(0x00, PSISO_ALIGN); + + isoPart.Seek(0x00, SeekOrigin.Begin); + isoPart.CopyTo(Psar); + + startDat.Seek(0x00, SeekOrigin.Begin); + startDat.CopyTo(Psar); + + psarUtil.WriteBytes(SimplePgd); + } + + private byte[] calculateChecksumForIsoImgTitle(byte[] header) + { + byte[] checksum = new byte[0x10]; + Span mkey = stackalloc byte[Marshal.SizeOf()]; + + PspCrypto.AMCTRL.sceDrmBBMacInit(mkey, 3); + PspCrypto.AMCTRL.sceDrmBBMacUpdate(mkey, header, header.Length /*0xb3c80*/); + Span newKey = new byte[20 + 0x10]; + PspCrypto.AMCTRL.sceDrmBBMacFinal(mkey, newKey[20..], VersionKey); + ref var aesHdr = ref MemoryMarshal.AsRef(newKey); + aesHdr.mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC; + aesHdr.keyseed = 0x63; + aesHdr.data_size = 0x10; + PspCrypto.KIRKEngine.sceUtilsBufferCopyWithRange(newKey, 0x10, newKey, 0x10, KIRKEngine.KIRK_CMD_ENCRYPT_IV_0); + + newKey.Slice(20, 0x10).CopyTo(checksum); + + return checksum; + } + + private byte[] generateIsoMapPgd() + { + isoMap.Seek(0, SeekOrigin.Begin); + byte[] isoMapBuf = isoMap.ToArray(); + + int encryptedSz = DNASHelper.CalculateSize(isoMapBuf.Length, 1024); + var isoMapEnc = new byte[encryptedSz]; + + DNASHelper.Encrypt(isoMapEnc, isoMapBuf, VersionKey, isoMapBuf.Length, 1, 1); + + return isoMapEnc; + } + + private void createIsoMap() + { + byte[] checksums = new byte[0x10 * MAX_DISCS]; + for(int i = 0; i < MAX_DISCS; i++) + { + if (compressors[i] is null) { isoMapUtil.WriteInt32(0); continue; }; + + int padLen = Convert.ToInt32(PSISO_ALIGN - (isoPart.Position % PSISO_ALIGN)); + isoPartUtil.WritePadding(0x00, padLen); + + using (PsIsoImg psIsoImg = new PsIsoImg(this.VersionKey, this.ContentId, compressors[i])) + { + isoMapUtil.WriteUInt32(Convert.ToUInt32(PSISO_ALIGN + isoPart.Position)); + + psIsoImg.CreatePsar(true); + + + psIsoImg.Psar.Seek(0x0, SeekOrigin.Begin); + compressors[i].IsoHeader.Seek(0x00, SeekOrigin.Begin); + + byte[] isoHdr = new byte[compressors[i].IsoHeader.Length + 0x400]; + psIsoImg.Psar.Read(isoHdr, 0x00, 0x400); + compressors[i].IsoHeader.Read(isoHdr, 0x400, Convert.ToInt32(compressors[i].IsoHeader.Length)); + + byte[] checksum = calculateChecksumForIsoImgTitle(isoHdr); + Array.ConstrainedCopy(checksum, 0, checksums, i * 0x10, 0x10); + + psIsoImg.Psar.Seek(0x00, SeekOrigin.Begin); + psIsoImg.Psar.CopyTo(isoPart); + + } + + } + isoMapUtil.WriteBytes(checksums); + isoMapUtil.WriteStrWithPadding(discs.First().DiscIdHdr, 0x00, 0x20); + + isoMapUtil.WriteInt64(Convert.ToInt64(PSISO_ALIGN + isoPart.Length + startDat.Length)); + isoMapUtil.WriteRandom(0x80); + isoMapUtil.WriteStrWithPadding(discs.First().DiscName, 0x00, 0x80); + isoMapUtil.WriteInt32(MAX_DISCS); + isoMapUtil.WritePadding(0x00, 0x70); + } + + public override void Dispose() + { + isoPart.Dispose(); + isoMap.Dispose(); + base.Dispose(); + } + + private DiscInfo[] discs; + private DiscCompressor[] compressors; + + private MemoryStream isoPart; + private StreamUtil isoPartUtil; + + private MemoryStream isoMap; + private StreamUtil isoMapUtil; + } +} diff --git a/PopsBuilder/PopsBuilder.csproj b/PopsBuilder/PopsBuilder.csproj new file mode 100644 index 0000000..2788edf --- /dev/null +++ b/PopsBuilder/PopsBuilder.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + enable + + + + + + + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/PopsBuilder/Psp/NpDrmPsar.cs b/PopsBuilder/Psp/NpDrmPsar.cs new file mode 100644 index 0000000..4960cac --- /dev/null +++ b/PopsBuilder/Psp/NpDrmPsar.cs @@ -0,0 +1,34 @@ +using PspCrypto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Psp +{ + public class NpDrmPsar : IDisposable + { + public NpDrmPsar(byte[] versionKey, string contentId) + { + VersionKey = versionKey; + ContentId = contentId; + + Psar = new MemoryStream(); + psarUtil = new StreamUtil(Psar); + } + + public byte[] VersionKey; + public string ContentId; + + public MemoryStream Psar; + internal StreamUtil psarUtil; + + public virtual void Dispose() + { + Psar.Dispose(); + } + } +} diff --git a/PopsBuilder/Psp/PbpBuilder.cs b/PopsBuilder/Psp/PbpBuilder.cs new file mode 100644 index 0000000..c356018 --- /dev/null +++ b/PopsBuilder/Psp/PbpBuilder.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Psp +{ + public static class PbpBuilder + { + public static void CreatePbp(byte[]? paramSfo, byte[]? icon0Png, byte[]? icon1Png, + byte[]? pic0Png, byte[]? pic1Png, byte[]? snd0At3, + byte[] dataPsp, NpDrmPsar dataPsar, string outputFile) + { + + using (FileStream pbpStream = File.Open(outputFile, FileMode.Create)) + { + StreamUtil pbpUtil = new StreamUtil(pbpStream); + pbpUtil.WriteByte(0x00); + pbpUtil.WriteStrWithPadding("PBP", 0x00, 0x5); + pbpUtil.WriteInt16(1); + + // param location + uint loc = 0x28; + if (paramSfo is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(paramSfo.Length); } + + // icon0 location + if (icon0Png is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(icon0Png.Length); } + + // icon1 location + if (icon1Png is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(icon1Png.Length); } + + // pic0 location + if (pic0Png is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(pic0Png.Length); } + + // pic1 location + if (pic1Png is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(pic1Png.Length); } + + // snd0 location + if (snd0At3 is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(snd0At3.Length); } + + // datapsp location + pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(dataPsp.Length); + + // psar location + pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(dataPsar.Psar.Length); + + // write pbp metadata + if (paramSfo is not null) pbpUtil.WriteBytes(paramSfo); + if (icon0Png is not null) pbpUtil.WriteBytes(icon0Png); + if (icon1Png is not null) pbpUtil.WriteBytes(icon1Png); + if (pic0Png is not null) pbpUtil.WriteBytes(pic0Png); + if (pic1Png is not null) pbpUtil.WriteBytes(pic1Png); + if (snd0At3 is not null) pbpUtil.WriteBytes(snd0At3); + + // write DATA.PSP + pbpUtil.WriteBytes(dataPsp); + + // write DATA.PSAR + dataPsar.Psar.Seek(0x00, SeekOrigin.Begin); + dataPsar.Psar.CopyTo(pbpStream); + } + + } + + + } +} diff --git a/PopsBuilder/Resources.Designer.cs b/PopsBuilder/Resources.Designer.cs new file mode 100644 index 0000000..2a9c5a0 --- /dev/null +++ b/PopsBuilder/Resources.Designer.cs @@ -0,0 +1,103 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PopsBuilder { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PopsBuilder.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] DATAPSPSD { + get { + object obj = ResourceManager.GetObject("DATAPSPSD", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] DATAPSPSDCFG { + get { + object obj = ResourceManager.GetObject("DATAPSPSDCFG", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] SIMPLE { + get { + object obj = ResourceManager.GetObject("SIMPLE", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] STARTDAT { + get { + object obj = ResourceManager.GetObject("STARTDAT", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/PopsBuilder/Resources.resx b/PopsBuilder/Resources.resx new file mode 100644 index 0000000..285a13a --- /dev/null +++ b/PopsBuilder/Resources.resx @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Resources\DATAPSPSD.ELF;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\DATAPSPSDCFG.BIN;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\SIMPLE.PNG;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\STARTDAT.PNG;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/PopsBuilder/Resources/DATAPSPSD.ELF b/PopsBuilder/Resources/DATAPSPSD.ELF new file mode 100644 index 0000000000000000000000000000000000000000..19d8a642907c6c440cbe4ca4dddeec2ba80804d3 GIT binary patch literal 14658 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 K2C1?QXiip3k_(Rj literal 0 HcmV?d00001 diff --git a/PopsBuilder/Resources/SIMPLE.PNG b/PopsBuilder/Resources/SIMPLE.PNG new file mode 100644 index 0000000000000000000000000000000000000000..c3850e7f70019f9da61a81bb69d99d3099d0290b GIT binary patch literal 22964 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 Hu0mjfz&KGW literal 0 HcmV?d00001 diff --git a/PopsBuilder/Resources/STARTDAT.PNG b/PopsBuilder/Resources/STARTDAT.PNG new file mode 100644 index 0000000000000000000000000000000000000000..cd366d589f2aa6f2cef0835602e31dde073d9d6b GIT binary patch literal 9283 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 literal 0 HcmV?d00001 diff --git a/PopsBuilder/StreamUtil.cs b/PopsBuilder/StreamUtil.cs new file mode 100644 index 0000000..2aac518 --- /dev/null +++ b/PopsBuilder/StreamUtil.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder +{ + public class StreamUtil + { + private Stream s; + private Random rng; + public StreamUtil(Stream s) + { + this.s = s; + rng = new Random(); + } + + public void WriteStrWithPadding(string str, byte b, int len) + { + byte[] sdata = Encoding.UTF8.GetBytes(str); + if (len < sdata.Length) + { + s.Write(sdata, 0, len); + return; + } + else + { + WriteBytes(sdata); + WritePadding(b, (len - sdata.Length)); + } + } + + public void WriteRandom(int len) + { + byte[] randomBytes = new byte[len]; + rng.NextBytes(randomBytes); + this.WriteBytes(randomBytes); + } + public byte[] ReadBytes(int len) + { + byte[] data = new byte[len]; + s.Read(data, 0x00, len); + return data; + } + public UInt16 ReadUInt16() + { + byte[] vbytes = ReadBytes(0x2); + return BitConverter.ToUInt16(vbytes); + } + public Int16 ReadInt16() + { + byte[] vbytes = ReadBytes(0x2); + return BitConverter.ToInt16(vbytes); + } + public UInt32 ReadUInt32() + { + byte[] vbytes = ReadBytes(0x4); + return BitConverter.ToUInt32(vbytes); + } + public Int32 ReadInt32() + { + byte[] vbytes = ReadBytes(0x4); + return BitConverter.ToInt32(vbytes); + } + public void WriteInt64(Int64 v) + { + WriteBytes(BitConverter.GetBytes(v)); + } + public void WriteUInt16(UInt16 v) + { + WriteBytes(BitConverter.GetBytes(v)); + } + public void WriteInt16(Int16 v) + { + WriteBytes(BitConverter.GetBytes(v)); + } + + public void WriteUInt32(UInt32 v) + { + WriteBytes(BitConverter.GetBytes(v)); + } + + public void WriteInt32(Int32 v) + { + WriteBytes(BitConverter.GetBytes(v)); + } + public void WriteStr(string str) + { + WriteBytes(Encoding.UTF8.GetBytes(str)); + } + + public void WritePadding(byte b, int len) + { + for(int i = 0; i < len; i++) + { + WriteByte(b); + } + } + + public void PadUntil(byte b, int len) + { + int remain = Convert.ToInt32(len - s.Length); + WritePadding(b, remain); + } + public void WriteBytes(byte[] bytes) + { + s.Write(bytes, 0, bytes.Length); + } + public void WriteByte(byte b) + { + s.WriteByte(b); + } + + } +} diff --git a/PopsBuilder/xXEccD3str0yerXx.cs b/PopsBuilder/xXEccD3str0yerXx.cs new file mode 100644 index 0000000..db65b96 --- /dev/null +++ b/PopsBuilder/xXEccD3str0yerXx.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder +{ + public class xXEccD3str0yerXx + { + const int SECTOR_SZ = 2352; + const int COMPRESS_BLOCK_SZ = 0x9300; + + public static MemoryStream RemoveEcc(string iso) + { + using(FileStream eccIso = File.OpenRead(iso)) + { + MemoryStream noEccIso = new MemoryStream(); + while (eccIso.Position < eccIso.Length) + { + byte[] sector = new byte[SECTOR_SZ]; + + eccIso.Read(sector, 0, sector.Length); + + // clear current sync + Array.Fill(sector, (byte)0x00, 0x1, 0x0A); + + // remove MSF .. + sector[0x0C] = 0x00; // M + sector[0x0D] = 0x00; // S + sector[0x0E] = 0x00; // F + + // remove ecc + + // (only if this is not form2mode2 sector!) + if (!(sector[0xF] == 0x2 && (sector[0x12] & 0x20) == 0x20)) + Array.Fill(sector, (byte)0x00, 0x818, 0x118); + else if(eccIso.Position > 0x9300) // only clear if its past the system section .. + Array.Fill(sector, (byte)0x00, 0x92C, 0x4); + + noEccIso.Write(sector, 0, sector.Length); + } + + // extend ISO image to compress block sz + int padLen = COMPRESS_BLOCK_SZ - (Convert.ToInt32(noEccIso.Length) % COMPRESS_BLOCK_SZ); + byte[] padding = new byte[padLen]; + noEccIso.Write(padding, 0x00, padding.Length); + + noEccIso.Seek(0, SeekOrigin.Begin); + + return noEccIso; + } + } + } +} diff --git a/PspCrypto/AMCTRL.cs b/PspCrypto/AMCTRL.cs new file mode 100644 index 0000000..515fc7d --- /dev/null +++ b/PspCrypto/AMCTRL.cs @@ -0,0 +1,710 @@ +using System; +using System.Runtime.InteropServices; + +namespace PspCrypto +{ + public class AMCTRL + { + static readonly Memory kirk_buf = new byte[0x0814]; + static readonly Memory kirk_buf2 = new byte[0x8014]; + + // AMCTRL keys. + static readonly byte[] amctrl_key1 = { 0xE3, 0x50, 0xED, 0x1D, 0x91, 0x0A, 0x1F, 0xD0, 0x29, 0xBB, 0x1C, 0x3E, 0xF3, 0x40, 0x77, 0xFB }; + static readonly byte[] amctrl_key2 = { 0x13, 0x5F, 0xA4, 0x7C, 0xAB, 0x39, 0x5B, 0xA4, 0x76, 0xB8, 0xCC, 0xA9, 0x8F, 0x3A, 0x04, 0x45 }; + static readonly byte[] amctrl_key3 = { 0x67, 0x8D, 0x7F, 0xA3, 0x2A, 0x9C, 0xA0, 0xD1, 0x50, 0x8A, 0xD8, 0x38, 0x5E, 0x4B, 0x01, 0x7E }; + + public unsafe struct MAC_KEY + { + public int type; + private fixed byte _key[16]; + + public Span key + { + get + { + fixed (byte* ptr = _key) + { + return new Span(ptr, 16); + } + } + } + private fixed byte _pad[16]; + + public Span pad + { + get + { + fixed (byte* ptr = _pad) + { + return new Span(ptr, 16); + } + } + } + public int pad_size; + } + + public unsafe struct CIPHER_KEY + { + public int type; + public int seed; + fixed byte _key[16]; + + public Span key + { + get + { + fixed (byte* ptr = _key) + { + return new Span(ptr, 16); + } + } + } + } + + /* + * KIRK wrapper functions. + */ + static int Kirk4(Span buf, int size, int type) + { + int retv; + ref var hdr = ref Utils.AsRef(buf); + hdr.mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC; + hdr.keyseed = type; + hdr.data_size = size; + + retv = KIRKEngine.sceUtilsBufferCopyWithRange(buf, size + 0x14, buf, size, KIRKEngine.KIRK_CMD_ENCRYPT_IV_0); + + if (retv != 0) + return -2142174447; // 0x80510311; + + return 0; + } + + static int Kirk5(Span buf, int size) + { + int retv; + ref var hdr = ref Utils.AsRef(buf); + hdr.mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC; + hdr.keyseed = 0x0100; + hdr.data_size = size; + + retv = KIRKEngine.sceUtilsBufferCopyWithRange(buf, size + 0x14, buf, size, KIRKEngine.KIRK_CMD_ENCRYPT_IV_FUSE); + + if (retv != 0) + return -2142174446; // 0x80510312; + + return 0; + } + + static int Kirk7(Span buf, int size, int type) + { + int retv; + ref var hdr = ref Utils.AsRef(buf); + hdr.mode = KIRKEngine.KIRK_MODE_DECRYPT_CBC; + hdr.keyseed = type; + hdr.data_size = size; + + retv = KIRKEngine.sceUtilsBufferCopyWithRange(buf, size + 0x14, buf, size, KIRKEngine.KIRK_CMD_DECRYPT_IV_0); + + if (retv != 0) + return -2142174447; // 0x80510311; + + return 0; + } + + static int Kirk8(Span buf, int size) + { + int retv; + ref var hdr = ref Utils.AsRef(buf); + hdr.mode = KIRKEngine.KIRK_MODE_DECRYPT_CBC; + hdr.keyseed = 0x0100; + hdr.data_size = size; + + retv = KIRKEngine.sceUtilsBufferCopyWithRange(buf, size + 0x14, buf, size, KIRKEngine.KIRK_CMD_DECRYPT_IV_FUSE); + + if (retv != 0) + return -2142174446; // 0x80510312; + + return 0; + } + + static int Kirk14(Span buf) + { + int retv; + + retv = KIRKEngine.sceUtilsBufferCopyWithRange(buf, 0x14, null, 0, KIRKEngine.KIRK_CMD_PRNG); + + if (retv != 0) + return -2142174443; // 0x80510315; + + return 0; + } + + static int encrypt_buf(Span buf, int size, Span key, int key_type) + { + int i, retv; + + for (i = 0; i < 16; i++) + { + buf[0x14 + i] ^= key[i]; + } + + unsafe + { + fixed (byte* ptr = buf) + { + + } + } + + retv = Kirk4(buf, size, key_type); + + if (retv != 0) + return retv; + + buf.Slice(size + 4, 16).CopyTo(key); + + return 0; + } + + static int decrypt_buf(Span buf, int size, Span key, int key_type) + { + int i, retv; + Span tmp = stackalloc byte[16]; + + buf.Slice(size + 0x14 - 16, 16).CopyTo(tmp); + + retv = Kirk7(buf, size, key_type); + + if (retv != 0) + return retv; + + for (i = 0; i < 16; i++) + { + buf[i] ^= key[i]; + } + + tmp.CopyTo(key); + + return 0; + } + + static int cipher_buf(Span kbuf, Span dbuf, int size, ref CIPHER_KEY ckey) + { + int i, retv; + Span tmp1 = stackalloc byte[16], tmp2 = stackalloc byte[16]; + + ckey.key.CopyTo(kbuf[0x14..]); + + for (i = 0; i < 16; i++) + { + kbuf[0x14 + i] ^= amctrl_key3[i]; + } + + if (ckey.type == 2) + retv = Kirk8(kbuf, 16); + else + retv = Kirk7(kbuf, 16, 0x39); + + if (retv != 0) + return retv; + + for (i = 0; i < 16; i++) + { + kbuf[i] ^= amctrl_key2[i]; + } + + kbuf.Slice(0, 0x10).CopyTo(tmp2); + + if (ckey.seed == 1) + { + tmp1.Fill(0); + } + else + { + tmp2.CopyTo(tmp1); + var tmp = ckey.seed - 1; + MemoryMarshal.Write(tmp1[0x0c..], ref tmp); + } + + for (i = 0; i < size; i += 16) + { + tmp2[..12].CopyTo(kbuf[(0x14 + i)..]); + MemoryMarshal.Write(kbuf[(0x14 + i + 12)..], ref ckey.seed); + ckey.seed += 1; + } + + retv = decrypt_buf(kbuf, size, tmp1, 0x63); + + if (retv != 0) + return retv; + + for (i = 0; i < size; i++) + { + dbuf[i] ^= kbuf[i]; + } + + return 0; + } + + public static int sceDrmBBMacInit(Span mkey, int type) + { + ref MAC_KEY macKey = ref MemoryMarshal.AsRef(mkey); + macKey.type = type; + macKey.key.Clear(); + macKey.pad.Clear(); + return 0; + } + + public static int sceDrmBBMacUpdate(Span mkey, ReadOnlySpan buf, int size) + { + ref MAC_KEY macKey = ref MemoryMarshal.AsRef(mkey); + + int retv = 0, ksize, p, type; + int kbuf; + + if (macKey.pad_size > 16) + { + retv = -2142174462; // 0x80510302 + return retv; + } + + if (macKey.pad_size + size <= 16) + { + buf[..size].CopyTo(macKey.pad[macKey.pad_size..]); + macKey.pad_size += size; + retv = 0; + } + else + { + kbuf = 0x14; + macKey.pad[..macKey.pad_size].CopyTo(kirk_buf[0x14..].Span); + + p = macKey.pad_size; + + macKey.pad_size += size; + macKey.pad_size &= 0x0f; + if (macKey.pad_size == 0) + macKey.pad_size = 16; + + size -= macKey.pad_size; + buf.Slice(size, macKey.pad_size).CopyTo(macKey.pad); + + type = (macKey.type == 2) ? 0x3A : 0x38; + + int offset = 0; + + while (size > 0) + { + ksize = (size + p >= 0x0800) ? 0x0800 : size + p; + buf.Slice(offset, ksize - p).CopyTo(kirk_buf[(kbuf + p)..].Span); + retv = encrypt_buf(kirk_buf.Span, ksize, macKey.key, type); + + if (retv != 0) + return retv; + + size -= (ksize - p); + offset += ksize - p; + p = 0; + } + } + + return retv; + } + + public static int sceDrmBBMacUpdate2(Span mkey, Span buf, int size) + { + ref MAC_KEY macKey = ref MemoryMarshal.AsRef(mkey); + + int retv = 0, ksize, p, type; + int kbuf; + + if (macKey.pad_size > 16) + { + retv = -2142174462; // 0x80510302 + return retv; + } + + if (macKey.pad_size + size <= 16) + { + buf.Slice(0, size).CopyTo(macKey.pad.Slice(macKey.pad_size)); + macKey.pad_size += size; + retv = 0; + } + else + { + kbuf = 0x14; + macKey.pad.Slice(0, macKey.pad_size).CopyTo(kirk_buf.Slice(0x14).Span); + + p = macKey.pad_size; + + macKey.pad_size += (size & 0x0f); + //mkey.pad_size &= 0x0f; + if (macKey.pad_size == 0) + macKey.pad_size = 16; + + size -= macKey.pad_size; + buf.Slice(size, macKey.pad_size).CopyTo(macKey.pad); + + type = (macKey.type == 2) ? 0x3A : 0x38; + + int idx = 0; + + ksize = size + p; + int offset = 0; + if (size + p >= 0x8001) + { + ; + buf.Slice(offset, 0x8000 - p).CopyTo(kirk_buf2.Slice(kbuf + p).Span); + retv = encrypt_buf(kirk_buf2.Span, 0x8000, macKey.key, type); + idx = 0x8000 - p; + var fix = -p; + while (retv == 0) + { + if (size <= 0x10000 - fix) + { + p = 0; + ksize = size; + break; + } + buf.Slice(offset + idx, 0x8000).CopyTo(kirk_buf2.Slice(kbuf).Span); + retv = encrypt_buf(kirk_buf2.Span, 0x8000, macKey.key, type); + fix = idx; + idx += 0x8000; + } + + if (retv != 0) + { + return retv; + } + } + buf.Slice(offset + idx, size - idx).CopyTo(kirk_buf2.Slice(kbuf + p).Span); + retv = encrypt_buf(kirk_buf2.Span, ksize - idx, macKey.key, type); + } + + return retv; + } + + public static int sceDrmBBMacFinal(Span mkey, Span buf, ReadOnlySpan vkey) + { + ref MAC_KEY macKey = ref MemoryMarshal.AsRef(mkey); + int i, retv, code; + Span tmp = stackalloc byte[16], tmp1 = stackalloc byte[16]; + int kbuf; + uint t0, v0, v1; + + if (macKey.pad_size > 16) + return -2142174462; //0x80510302; + + code = (macKey.type == 2) ? 0x3A : 0x38; + kbuf = 0x14; + + kirk_buf.Slice(kbuf, 16).Span.Fill(0); + retv = Kirk4(kirk_buf.Span, 16, code); + if (retv != 0) + { + return retv; + } + + kirk_buf.Slice(kbuf, 16).Span.CopyTo(tmp); + + t0 = ((tmp[0] & 0x80) > 0) ? 0x87u : 0; + for (i = 0; i < 15; i++) + { + v1 = tmp[i + 0]; + v0 = tmp[i + 1]; + v1 <<= 1; + v0 >>= 7; + v0 |= v1; + tmp[i + 0] = (byte)v0; + } + v0 = tmp[15]; + v0 <<= 1; + v0 ^= t0; + tmp[15] = (byte)v0; + + if (macKey.pad_size < 16) + { + t0 = ((tmp[0] & 0x80) > 0) ? 0x87u : 0; + for (i = 0; i < 15; i++) + { + v1 = tmp[i + 0]; + v0 = tmp[i + 1]; + v1 <<= 1; + v0 >>= 7; + v0 |= v1; + tmp[i + 0] = (byte)v0; + } + v0 = tmp[15]; + v0 <<= 1; + v0 ^= t0; + tmp[15] = (byte)v0; + + macKey.pad[macKey.pad_size] = 0x80; + if (macKey.pad_size + 1 < 16) + { + macKey.pad.Slice(macKey.pad_size + 1, 16 - macKey.pad_size - 1).Fill(0); + } + } + + for (i = 0; i < 16; i++) + { + macKey.pad[i] ^= tmp[i]; + } + + macKey.pad.CopyTo(kirk_buf.Slice(kbuf).Span); + macKey.key.CopyTo(tmp1); + + retv = encrypt_buf(kirk_buf.Span, 0x10, tmp1, code); + + if (retv != 0) + return retv; + + for (i = 0; i < 0x10; i++) + { + tmp1[i] ^= amctrl_key1[i]; + } + + if (macKey.type == 2) + { + tmp1.CopyTo(kirk_buf.Slice(kbuf).Span); + + retv = Kirk5(kirk_buf.Span, 0x10); + + if (retv != 0) + return retv; + + retv = Kirk4(kirk_buf.Span, 0x10, code); + + if (retv != 0) + return retv; + + kirk_buf.Slice(kbuf, 16).Span.CopyTo(tmp1); + } + + if (vkey != null) + { + for (i = 0; i < 0x10; i++) + { + tmp1[i] ^= vkey[i]; + } + tmp1.CopyTo(kirk_buf.Slice(kbuf).Span); + + retv = Kirk4(kirk_buf.Span, 0x10, code); + + if (retv != 0) + return retv; + + kirk_buf.Slice(kbuf, 16).Span.CopyTo(tmp1); + } + + tmp1.CopyTo(buf); + + macKey.key.Fill(0); + macKey.pad.Fill(0); + + macKey.pad_size = 0; + macKey.type = 0; + retv = 0; + + return retv; + } + + + + public static int bbmac_getkey(Span mkey, ReadOnlySpan bbmac, Span vkey) + { + int i, retv, type, code; + Span tmp = stackalloc byte[16], tmp1 = stackalloc byte[16]; + int kbuf; + ref MAC_KEY macKey = ref MemoryMarshal.AsRef(mkey); + + type = macKey.type; + retv = sceDrmBBMacFinal(mkey, tmp, null); + + if (retv != 0) + return retv; + + kbuf = 0x14; + + if (type == 3) + { + bbmac[..0x10].CopyTo(kirk_buf[kbuf..].Span); + Kirk7(kirk_buf.Span, 0x10, 0x63); + } + else + { + bbmac[..0x10].CopyTo(kirk_buf.Span); + } + + kirk_buf[..16].Span.CopyTo(tmp1); + tmp1.CopyTo(kirk_buf[kbuf..].Span); + + code = (type == 2) ? 0x3A : 0x38; + Kirk7(kirk_buf.Span, 0x10, code); + + for (i = 0; i < 0x10; i++) + { + vkey[i] = (byte)(tmp[i] ^ kirk_buf.Span[i]); + } + + return 0; + } + + public static int sceDrmBBMacFinal2(Span mkey, ReadOnlySpan hash, ReadOnlySpan vkey) + { + int i, retv, type; + byte[] tmp = new byte[16]; + int kbuf; + ref MAC_KEY macKey = ref MemoryMarshal.AsRef(mkey); + + type = macKey.type; + retv = sceDrmBBMacFinal(mkey, tmp, vkey); + if (retv != 0) + return retv; + + kbuf = 0x14; + + if (type == 3) + { + hash[..0x10].CopyTo(kirk_buf[kbuf..].Span); + Kirk7(kirk_buf.Span, 0x10, 0x63); + } + else + { + hash[..0x10].CopyTo(kirk_buf.Span); + } + + retv = 0; + if (!kirk_buf.Span[..0x10].SequenceEqual(tmp)) + { + retv = -2142174464; //0x80510300; + } + //for (i = 0; i < 0x10; i++) + //{ + // if (kirk_buf.Span[i] != tmp[i]) + // { + // retv = -2142174464; //0x80510300; + // break; + // } + //} + + return retv; + } + + + /* + BBCipher functions. + */ + public static int sceDrmBBCipherInit(out CIPHER_KEY ckey, int type, int mode, ReadOnlySpan header_key, ReadOnlySpan version_key, int seed) + { + int i, retv; + int kbuf; + + kbuf = 0x14; + ckey = new CIPHER_KEY { type = type }; + if (mode == 2) + { + ckey.seed = seed + 1; + for (i = 0; i < 16; i++) + { + ckey.key[i] = header_key[i]; + } + if (version_key != null) + { + for (i = 0; i < 16; i++) + { + ckey.key[i] ^= version_key[i]; + } + } + retv = 0; + } + else if (mode == 1) + { + ckey.seed = 1; + retv = Kirk14(kirk_buf.Span); + + if (retv != 0) + return retv; + + kirk_buf.Slice(0, 0x10).CopyTo(kirk_buf.Slice(kbuf)); + kirk_buf.Slice(kbuf + 0xC, 4).Span.Fill(0); + + if (ckey.type == 2) + { + for (i = 0; i < 16; i++) + { + kirk_buf.Span[i + kbuf] ^= amctrl_key2[i]; + } + retv = Kirk5(kirk_buf.Span, 0x10); + for (i = 0; i < 16; i++) + { + kirk_buf.Span[i + kbuf] ^= amctrl_key3[i]; + } + } + else + { + for (i = 0; i < 16; i++) + { + kirk_buf.Span[i + kbuf] ^= amctrl_key2[i]; + } + retv = Kirk4(kirk_buf.Span, 0x10, 0x39); + for (i = 0; i < 16; i++) + { + kirk_buf.Span[i + kbuf] ^= amctrl_key3[i]; + } + } + + if (retv != 0) + return retv; + + kirk_buf.Slice(kbuf, 0x10).Span.CopyTo(ckey.key); + // kirk_buf.Slice(kbuf, 0x10).Span.CopyTo(header_key); + + if (version_key != null) + { + for (i = 0; i < 16; i++) + { + ckey.key[i] ^= version_key[i]; + } + } + } + else + { + retv = 0; + } + + return retv; + } + + public static int sceDrmBBCipherUpdate(ref CIPHER_KEY ckey, Span data, int size) + { + int p, retv, dsize; + + retv = 0; + p = 0; + + while (size > 0) + { + dsize = (size >= 0x0800) ? 0x0800 : size; + retv = cipher_buf(kirk_buf.Span, data.Slice(p), dsize, ref ckey); + + if (retv != 0) + break; + + size -= dsize; + p += dsize; + } + + return retv; + } + + public static int sceDrmBBCipherFinal(ref CIPHER_KEY ckey) + { + ckey.key.Fill(0); + ckey.type = 0; + ckey.seed = 0; + return 0; + } + } +} diff --git a/PspCrypto/AesHelper.cs b/PspCrypto/AesHelper.cs new file mode 100644 index 0000000..9ba14fd --- /dev/null +++ b/PspCrypto/AesHelper.cs @@ -0,0 +1,250 @@ +using System; +using System.Linq; +using System.Security.Cryptography; + +namespace PspCrypto +{ + public static class AesHelper + { + private static readonly byte[] padding = { 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11 }; + + static readonly byte[] IV0 = new byte[16]; + static readonly byte[] Z = new byte[16]; + + public static Aes CreateAes() + { + var aes = Aes.Create(); + aes.KeySize = 128; + if (aes == null) + { + throw new Exception("Create Aes Failed"); + } + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.None; + aes.IV = IV0; + return aes; + } + + public static Aes CreateKirkAes() + { + var aes = CreateAes(); + aes.Key = KeyVault.kirk1_key; + return aes; + } + +#if false + public static byte[] Cmac(Aes aes, byte[] orgdata, int offset = 0, int len = -1) + { + if (len == -1) + { + len = orgdata.Length; + } + + byte[] data; + if (offset == 0 && len == orgdata.Length) + { + data = orgdata; + } + else + { + data = new byte[len]; + Buffer.BlockCopy(orgdata, offset, data, 0, len); + } + // SubKey generation + // step 1, AES-128 with key K is applied to an all-zero input block. + byte[] L = AesEncrypt(aes, Z); + + // step 2, K1 is derived through the following operation: + byte[] FirstSubkey = Rol(L); //If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit. + if ((L[0] & 0x80) == 0x80) + FirstSubkey[15] ^= 0x87; // Otherwise, K1 is the exclusive-OR of c + + // step 3, K2 is derived through the following operation: + byte[] SecondSubkey = Rol(FirstSubkey); // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit. + if ((FirstSubkey[0] & 0x80) == 0x80) + SecondSubkey[15] ^= 0x87; // Otherwise, K2 is the exclusive-OR of const_Rb and the left-shift of K1 by 1 bit. + + // MAC computing + if (((data.Length != 0) && (data.Length % 16 == 0))) + { + // If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits), + // the last block shall be exclusive-OR'ed with K1 before processing + for (int j = 0; j < FirstSubkey.Length; j++) + data[data.Length - 16 + j] ^= FirstSubkey[j]; + } + else + { + // Otherwise, the last block shall be padded with 10^i + byte[] padding = new byte[16 - data.Length % 16]; + padding[0] = 0x80; + + data = data.Concat(padding).ToArray(); + + // and exclusive-OR'ed with K2 + for (int j = 0; j < SecondSubkey.Length; j++) + data[data.Length - 16 + j] ^= SecondSubkey[j]; + } + + // The result of the previous process will be the input of the last encryption. + byte[] encResult = AesEncrypt(aes, data); + + byte[] HashValue = new byte[16]; + //Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length); + Buffer.BlockCopy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length); + + return HashValue; + } + +#endif + public static void Cmac(Aes aes, Span dst, ReadOnlySpan src) + { + byte[] data = src.ToArray(); + // SubKey generation + // step 1, AES-128 with key K is applied to an all-zero input block. + byte[] L = AesEncrypt(aes, Z); + + // step 2, K1 is derived through the following operation: + byte[] FirstSubkey = Rol(L); //If the most significant bit of L is equal to 0, K1 is the left-shift of L by 1 bit. + if ((L[0] & 0x80) == 0x80) + FirstSubkey[15] ^= 0x87; // Otherwise, K1 is the exclusive-OR of c + + // step 3, K2 is derived through the following operation: + byte[] SecondSubkey = Rol(FirstSubkey); // If the most significant bit of K1 is equal to 0, K2 is the left-shift of K1 by 1 bit. + if ((FirstSubkey[0] & 0x80) == 0x80) + SecondSubkey[15] ^= 0x87; // Otherwise, K2 is the exclusive-OR of const_Rb and the left-shift of K1 by 1 bit. + + // MAC computing + if (((data.Length != 0) && (data.Length % 16 == 0))) + { + // If the size of the input message block is equal to a positive multiple of the block size (namely, 128 bits), + // the last block shall be exclusive-OR'ed with K1 before processing + for (int j = 0; j < FirstSubkey.Length; j++) + data[data.Length - 16 + j] ^= FirstSubkey[j]; + } + else + { + // Otherwise, the last block shall be padded with 10^i + byte[] padding = new byte[16 - data.Length % 16]; + padding[0] = 0x80; + + data = data.Concat(padding).ToArray(); + + // and exclusive-OR'ed with K2 + for (int j = 0; j < SecondSubkey.Length; j++) + data[data.Length - 16 + j] ^= SecondSubkey[j]; + } + + // The result of the previous process will be the input of the last encryption. + byte[] encResult = AesEncrypt(aes, data); + + encResult.AsSpan(encResult.Length - 16,16).CopyTo(dst); + + //byte[] HashValue = new byte[16]; + //Array.Copy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length); + //Buffer.BlockCopy(encResult, encResult.Length - HashValue.Length, HashValue, 0, HashValue.Length); + + //return HashValue; + } + + private static byte[] Rol(byte[] b) + { + byte[] r = new byte[b.Length]; + byte carry = 0; + + for (int i = b.Length - 1; i >= 0; i--) + { + ushort u = (ushort)(b[i] << 1); + r[i] = (byte)((u & 0xff) + carry); + carry = (byte)((u & 0xff00) >> 8); + } + + return r; + } + + private static byte[] AesEncrypt(Aes aes, byte[] data) + { + using var encryptor = aes.CreateEncryptor(); + return encryptor.TransformFinalBlock(data, 0, data.Length); + } +#if false + + public static byte[] AesEncrypt(Aes aes, byte[] data, int offset, int length) + { + using var encryptor = aes.CreateEncryptor(); + return encryptor.TransformFinalBlock(data, offset, length); + } +#endif + + public static void AesEncrypt(Aes aes, Span oubput, ReadOnlySpan input, int size = 0) + { + byte[] buffer; + if (size == 0) + { + size = input.Length; + } + if (size % 16 != 0) + { + buffer = new byte[(size + 15) / 16 * 16]; + var bufferSpan = new Span(buffer); + input.CopyTo(bufferSpan); + padding.AsSpan(0, buffer.Length - size).CopyTo(bufferSpan[size..]); + } + else + { + buffer = input[..size].ToArray(); + } + using var encryptor = aes.CreateEncryptor(); + var encData = encryptor.TransformFinalBlock(buffer, 0, buffer.Length); + encData.AsSpan().CopyTo(oubput); + } +#if false + public static byte[] AesDecrypt(Aes aes, byte[] data, int offset, int length) + { + aes.Padding = PaddingMode.None; + using var encryptor = aes.CreateDecryptor(); + int fixLength = length; + if (length % 16 != 0) + { + fixLength = (length / 16 + 1) * 16; + } + + byte[] ret = encryptor.TransformFinalBlock(data, offset, fixLength); + return ret.Take(length).ToArray(); + } + + public static void AesDecrypt(Aes aes, byte[] src, byte[] dst, int size) + { + var tmp = AesDecrypt(aes, src, 0, size); + Buffer.BlockCopy(tmp, 0, dst, 0, size); + } +#endif + + public static void AesDecrypt(Aes aes, Span dst, ReadOnlySpan src, int length) + { + + int fixLength = length; + if (length % 16 != 0) + { + fixLength = (length + 15) / 16 * 16; + } + var buffer = src[..fixLength].ToArray(); + using var encryptor = aes.CreateDecryptor(); + var decData = encryptor.TransformFinalBlock(buffer, 0, buffer.Length); + decData.AsSpan(0, length).CopyTo(dst); + } + + public static int AesDecrypt(ReadOnlySpan src, Span dst, ReadOnlySpan key) + { + Aes aes = Aes.Create(); + aes.Key = key[..16].ToArray(); + return aes.DecryptEcb(src[..16], dst, PaddingMode.None); + } + + public static int AesEncrypt(ReadOnlySpan src, Span dst, ReadOnlySpan key) + { + Aes aes = Aes.Create(); + aes.Key = key[..16].ToArray(); + return aes.EncryptEcb(src[..16], dst, PaddingMode.None); + } + } +} diff --git a/PspCrypto/AtracCrypto.cs b/PspCrypto/AtracCrypto.cs new file mode 100644 index 0000000..898d2cd --- /dev/null +++ b/PspCrypto/AtracCrypto.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace PspCrypto +{ + public class AtracCrypto + { + + const int NBYTES = 0x180; + private static uint ROTR32(uint v, int n) + { + n &= 32 - 1; + return (v >> n) | (v << (32 - n)); + } + + public static int UnscrambleAtracData(byte[] data, uint key) + { + int blocks = (data.Length / NBYTES) / 0x10; + int chunks_rest = (data.Length / NBYTES) % 0x10; + Span ptr = MemoryMarshal.Cast(data); + uint tmp2 = key; + uint tmp; + uint value; + // for each block + while (blocks > 0) + { + // for each chunk of block + for (int i = 0; i < 0x10; i++) + { + tmp = tmp2; + + // for each value of chunk + for (int k = 0; k < (NBYTES / 4); k++) + { + value = ptr[k]; + ptr[k] = tmp ^ value; + tmp = tmp2 + (value * 123456789); + } + + tmp2 = ROTR32(tmp2, 1); + ptr = ptr[(NBYTES / 4)..]; // pointer on next chunk + } + + blocks--; + } + + // do rest chunks + for (int i = 0; i < chunks_rest; i++) + { + tmp = tmp2; + + // for each value of chunk + for (int k = 0; k < (NBYTES / 4); k++) + { + value = ptr[k]; + ptr[k] = tmp ^ value; + tmp = tmp2 + (value * 123456789); + } + + tmp2 = ROTR32(tmp2, 1); + ptr = ptr[(NBYTES / 4)..]; // next chunk + } + + return 0; + } + + public static void ScrambleAtracData(Stream input, Stream output, uint key) + { + int blocks = (Convert.ToInt32(input.Length) / NBYTES) / 0x10; + int chunks_rest = (Convert.ToInt32(input.Length) / NBYTES) % 0x10; + Span block = stackalloc byte[NBYTES]; + uint tmp2 = key; + uint tmp; + // for each block + while (blocks > 0) + { + // for each chunk of block + for (int i = 0; i < 0x10; i++) + { + tmp = tmp2; + + input.Read(block); + Span ptr = MemoryMarshal.Cast(block); + + // for each value of chunk + for (int k = 0; k < (NBYTES / 4); k++) + { + ptr[k] ^= tmp; + tmp = tmp2 + (ptr[k] * 123456789); + } + output.Write(block); + + tmp2 = ROTR32(tmp2, 1); + ptr = ptr[(NBYTES / 4)..]; // pointer on next chunk + } + + blocks--; + } + + // do rest chunks + for (int i = 0; i < chunks_rest; i++) + { + tmp = tmp2; + + input.Read(block); + Span ptr = MemoryMarshal.Cast(block); + + // for each value of chunk + for (int k = 0; k < (NBYTES / 4); k++) + { + ptr[k] ^= tmp; + tmp = tmp2 + (ptr[k] * 123456789); + } + output.Write(block); + + tmp2 = ROTR32(tmp2, 1); + ptr = ptr[(NBYTES / 4)..]; // next chunk + } + } + } +} diff --git a/PspCrypto/DNASHelper.cs b/PspCrypto/DNASHelper.cs new file mode 100644 index 0000000..3baf241 --- /dev/null +++ b/PspCrypto/DNASHelper.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace PspCrypto +{ + using PgdHeader = DNASStream.PgdHeader; + using PgdDesc = DNASStream.PgdDesc; + + public static class DNASHelper + { + public static int CalculateSize(int dataSize, int blockSize) + { + int alignSize = (dataSize + 15) & ~15; + int tableSize = ((alignSize + blockSize - 1) & ~(blockSize - 1)) / (blockSize / 16); + int pgdSize = 0x90 + alignSize + tableSize; + return pgdSize; + } + + public static int Encrypt(Span pgdData, ReadOnlySpan data, ReadOnlySpan key, int dataSize, int keyIndex, int drmType, int flag = 2, int blockSize = 0x400) + { + // Additional size variables. + var dataOffset = 0x90; + var alignSize = (dataSize + 15) & ~15; + var tableOffset = dataOffset + alignSize; + var tableSize = ((alignSize + blockSize - 1) & ~(blockSize - 1)) / (blockSize / 16); + var pgdSize = 0x90 + alignSize + tableSize; + + if (pgdData.Length < pgdSize) + { + return -1; + } + + data[..dataSize].CopyTo(pgdData[dataOffset..]); + + ref var pgdHdr = ref Utils.AsRef(pgdData); + pgdHdr.Magic = 0x44475000; + pgdHdr.KeyIndex = keyIndex; + pgdHdr.DrmType = drmType; + + // Select the hashing, crypto and open modes. + int macType; + int cipherType; + var openFlag = flag; + if (drmType == 1) + { + macType = 1; + cipherType = 1; + openFlag |= 4; + if (keyIndex > 1) + { + macType = 3; + openFlag |= 0xc; + } + } + else + { + macType = 2; + cipherType = 2; + } + + // Select the fixed DNAS key. + + + byte[] dnasKey = null; + + if ((openFlag & 2) != 0) + { + dnasKey = DNASStream.DnasKey1; + } + else if ((openFlag & 1) != 0) + { + dnasKey = DNASStream.DnasKey2; + } + + if (dnasKey == null) + { + throw new Exception(); + } + + // Set the decryption parameters in the decrypted header. + ref var pgdDesc = ref Utils.AsRef(pgdHdr.PgdDesc); + pgdDesc.DataSize = dataSize; + pgdDesc.BlockSize = blockSize; + pgdDesc.DataOffset = dataOffset; + + // Generate random header and data keys. + RandomNumberGenerator.Fill(pgdData.Slice(0x10, 0x30)); + + // Encrypt the data. + DNASStream.DoBBCipher(pgdData[dataOffset..], alignSize, 0, key, pgdDesc.Key, cipherType); + + // Build data MAC hash. + var tableNum = tableSize / 16; + for (int i = 0; i < tableNum; i++) + { + int rsize = alignSize - i * blockSize; + if (rsize > blockSize) + rsize = blockSize; + if (keyIndex < 3) + { + BuildBBMac(pgdData[(dataOffset + i * blockSize)..], rsize, key, + pgdData[(tableOffset + i * 16)..], + macType); + } + else + { + BuildBBMac(pgdData[(dataOffset + i * blockSize)..], rsize, key, + pgdData[(tableOffset + i * 16)..], + macType); + } + } + + // Build table MAC hash. + BuildBBMac(pgdData.Slice(tableOffset), tableSize, key, pgdHdr.MacTableHash, macType); + + // Encrypt the PGD header block (0x30 bytes). + DNASStream.DoBBCipher(pgdHdr.PgdDesc, 0x30, 0, key, pgdHdr.DescKey, cipherType); + + // Build MAC hash at 0x70 (key hash). + BuildBBMac(pgdData, 0x70, key, pgdHdr.Hash70, macType); + + // Build MAC hash at 0x80 (DNAS hash). + BuildBBMac(pgdData, 0x80, dnasKey, pgdHdr.Hash80, macType); + + return pgdSize; + } + + static int BuildBBMac(ReadOnlySpan data, int size, ReadOnlySpan key, Span hash, int macType, int seed = 0) + { + Span mkey = stackalloc byte[Marshal.SizeOf()]; + Span tmpKey = stackalloc byte[0x10]; + int ret = unchecked((int)0x80510201); + if (hash != null) + { + ret = AMCTRL.sceDrmBBMacInit(mkey, macType); + if (ret != 0) + { + return ret; + } + + ret = AMCTRL.sceDrmBBMacUpdate(mkey, data, size); + if (ret != 0) + { + return ret; + } + key.CopyTo(tmpKey); + if (seed != 0) + { + var tmpXor = MemoryMarshal.Cast(tmpKey); + tmpXor[0] ^= seed; + } + + ret = AMCTRL.sceDrmBBMacFinal(mkey, hash, tmpKey); + if (ret != 0) + { + ret = unchecked((int)0x80510207); + } + + if (macType == 3) + { + Utils.BuildDrmBBMacFinal2(hash); + } + + } + + return ret; + } + } +} diff --git a/PspCrypto/DNASStream.cs b/PspCrypto/DNASStream.cs new file mode 100644 index 0000000..ba005d4 --- /dev/null +++ b/PspCrypto/DNASStream.cs @@ -0,0 +1,636 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace PspCrypto +{ + public class DNASStream : Stream + { + internal static readonly byte[] DnasKeyBase = + { + 0x2A, 0x05, 0x54, 0x40, 0x62, 0xD9, 0x1F, 0xE3, 0xF2, 0xD0, 0x2B, 0xC6, 0x21, 0xFF, 0x20, 0x0E, + 0xB1, 0x44, 0x28, 0xDF, 0x0A, 0xCD, 0x14, 0x5B, 0xC8, 0x19, 0x36, 0x90, 0xD1, 0x42, 0x99, 0x2F + }; + + internal static readonly byte[] DnasKey1 = + { + 0xED, 0xE2, 0x5D, 0x2D, 0xBB, 0xF8, 0x12, 0xE5, 0x3C, 0x5C, 0x59, 0x32, 0xFA, 0xE3, 0xE2, 0x43 + }; + + internal static readonly byte[] DnasKey2 = + { + 0x27, 0x74, 0xFB, 0xEB, 0xA4, 0xA0, 0x01, 0xD7, 0x02, 0x56, 0x9E, 0x33, 0x8C, 0x19, 0x57, 0x83 + }; + + private static Memory _gMemory = new byte[0x640]; + private static Memory _gCipherMemory = new byte[0x200]; + + private Stream _baseStream; + private byte[] _versionKey; + + private long _position; + private long _pgdOffset; + private int _keyIndex; + private int _openFlag; + private long _dataOffset; + private long _tableOffset; + + public int KeyIndex => _keyIndex; + + private PgdDesc _desc; + + internal unsafe struct PgdHeader + { + public uint Magic; + public int KeyIndex; + public int DrmType; + public int Unk12; + private fixed byte _descKey[0x10]; + public Span DescKey + { + get + { + fixed (byte* ptr = _descKey) + { + return new Span(ptr, 0x10); + } + } + } + private fixed byte _keyHash[0x10]; + public Span KeyHash + { + get + { + fixed (byte* ptr = _keyHash) + { + return new Span(ptr, 0x10); + } + } + } + private fixed byte _pgdDesc[0x30]; + public Span PgdDesc + { + get + { + fixed (byte* ptr = _pgdDesc) + { + return new Span(ptr, 0x30); + } + } + } + private fixed byte _macTableHash[0x10]; + public Span MacTableHash + { + get + { + fixed (byte* ptr = _macTableHash) + { + return new Span(ptr, 0x10); + } + } + } + private fixed byte _hash70[0x10]; + public Span Hash70 + { + get + { + fixed (byte* ptr = _hash70) + { + return new Span(ptr, 0x10); + } + } + } + private fixed byte _hash80[0x10]; + public Span Hash80 + { + get + { + fixed (byte* ptr = _hash80) + { + return new Span(ptr, 0x10); + } + } + } + } + + internal unsafe struct PgdDesc + { + private fixed byte _key[0x10]; + + public Span Key + { + get + { + fixed (byte* ptr = _key) + { + return new Span(ptr, 0x10); + } + } + } + public int Version; + public int DataSize; + public int BlockSize; + public int DataOffset; + private fixed byte _unk20[0x10]; + + public Span Unk20 + { + get + { + fixed (byte* ptr = _unk20) + { + return new Span(ptr, 0x10); + } + } + } + + } + + public int BlockSize => _desc.BlockSize; + + public DNASStream(Stream stream, long pgdOffset, ReadOnlySpan versionKey, int flag = 2) + : this(stream, pgdOffset, versionKey.ToArray(), flag) + { + + } + + public DNASStream(Stream stream, long pgdOffset, byte[] versionKey = null, int flag = 2) + { + _baseStream = stream; + _pgdOffset = pgdOffset; + var offset = stream.Seek(pgdOffset, SeekOrigin.Begin); + if (offset != _pgdOffset) + { + throw new ArgumentOutOfRangeException(); + } + Span hdr = _gMemory[..0x90].Span; + var size = stream.Read(hdr); + if (size != 0x90) + { + throw new ArgumentException("stream too small", nameof(stream)); + } + var header = Utils.AsRef(hdr); + _keyIndex = header.KeyIndex; + if (_keyIndex == 1) + { + _versionKey = versionKey ?? new byte[16]; + } + else + { + if (versionKey == null) + { + throw new ArgumentNullException(nameof(versionKey)); + } + Span mkey = stackalloc byte[Marshal.SizeOf()]; + AMCTRL.sceDrmBBMacInit(mkey, 1); + AMCTRL.sceDrmBBMacUpdate(mkey, DnasKeyBase, (_keyIndex - 1) * 0x10); + AMCTRL.sceDrmBBMacFinal(mkey, _versionKey, versionKey); + return; + } + + int macType; + int cipherType; + + if (header.DrmType == 1) + { + flag |= 4; + macType = 1; + cipherType = 1; + if (header.KeyIndex > 1) + { + flag |= 0xc; + macType = 3; + } + } + else if (header.DrmType == 0 && (flag & 4) == 0) + { + macType = 2; + cipherType = 2; + } + else + { + throw new IOException(); + } + + byte[] dnasKey = null; + + if ((flag & 2) != 0) + { + dnasKey = DnasKey1; + } + else if ((flag & 1) != 0) + { + dnasKey = DnasKey2; + } + + if (dnasKey == null) + { + throw new IOException(); + } + + var ret = CheckBBMac(hdr, 0x80, dnasKey, header.Hash80, macType); + if (ret != 0) + { + throw new IOException("Wrong MAC 0x80"); + } + + if (!Utils.isEmpty(_versionKey, 0x10)) + { + ret = CheckBBMac(hdr, 0x70, _versionKey, header.Hash70, macType); + } + else + { + ret = GetMacKey(hdr, 0x70, _versionKey, header.Hash70, macType); + } + + if (ret != 0) + { + throw new IOException("Wrong MAC 0x70"); + } + + ret = DoBBCipher(header.PgdDesc, 0x30, 0, _versionKey, header.DescKey, cipherType); + if (ret != 0) + { + throw new IOException($"Error 0x{ret:X8}"); + } + var desc = Utils.AsRef(header.PgdDesc); + if (desc.Version != 0) + { + throw new IOException($"Error 0x{8051020:X8}"); + } + + if (desc.BlockSize != 0x400) + { + throw new IOException($"Error 0x{80510204:X8}"); + } + + _openFlag = flag | 0x10; + _desc = desc; + _dataOffset = _desc.DataOffset + pgdOffset; + var blockSize = desc.BlockSize; + var alignSize = (desc.DataSize + 15) & ~15; + var tableSize = ((alignSize + blockSize - 1) & ~(blockSize - 1)) / (blockSize / 16); + _tableOffset = pgdOffset + 0x90 + alignSize; + if (header.KeyIndex < 3 && 0x7ffff < tableSize) + { + + } + { + Span mkey = stackalloc byte[Marshal.SizeOf()]; + ret = AMCTRL.sceDrmBBMacInit(mkey, macType); + stream.Seek(_tableOffset, SeekOrigin.Begin); + int read = 0; + if (tableSize != 0) + { + Span dataBuf = new byte[0x400]; + do + { + var tmpSize = tableSize - read; + if (tmpSize > 0x400) + { + tmpSize = 0x400; + } + + var data = dataBuf.Slice(0, tmpSize); + var readSize = stream.Read(data); + if (readSize != tmpSize) + { + throw new IOException(); + } + ret = AMCTRL.sceDrmBBMacUpdate(mkey, data, tmpSize); + if (ret != 0) + { + throw new Exception(); + } + read += 0x400; + } while (read < tableSize); + } + ret = AMCTRL.sceDrmBBMacFinal2(mkey, header.MacTableHash, _versionKey); + if (ret != 0) + { + throw new IOException($"Error 0x{80510204:X8}"); + } + } + + _position = 0; + + } + + public override void Flush() + { + _baseStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int ret; + Span bufferSpan = buffer; + if ((_openFlag & 0x10) == 0) + { + throw new IOException($"Error 0x{80510207:X8}"); + } + + var dataSize = _desc.DataSize; + var seekOffset = _position; + if (seekOffset < dataSize) + { + var macType = 2; + var cipherType = 2; + if ((_openFlag & 4) != 0) + { + macType = 1; + if ((_openFlag & 8) != 0) + { + macType = 3; + } + + cipherType = 1; + } + + var endOffset = dataSize; + if (seekOffset + count <= dataSize) + { + endOffset = (int)(seekOffset + count); + } + + var blockSize = _desc.BlockSize; + var alignOffset = (int)seekOffset; + var totalReadSize = 0; + while (true) + { + if (endOffset <= alignOffset) + { + return totalReadSize; + } + + if ((_openFlag & 0x10) == 0) + { + break; + } + + var align = alignOffset & blockSize - 1; + + alignOffset -= align; + var alignBlockSize = endOffset - alignOffset; + int readBytes; + int uVar6; + Span readBuffer; + if (align == 0 && blockSize <= alignBlockSize) + { + readBytes = alignBlockSize & ~(blockSize - 1); + readBuffer = bufferSpan; + uVar6 = readBytes; + } + else + { + readBytes = blockSize; + readBuffer = _gMemory.Span; + if (dataSize < alignOffset + blockSize) + { + readBytes = (dataSize - alignOffset + 15) & ~15; + } + + uVar6 = blockSize; + if (endOffset < alignOffset + blockSize) + { + uVar6 = alignBlockSize; + } + } + + _baseStream.Seek(_dataOffset + alignOffset, SeekOrigin.Begin); + var realReadBytes = _baseStream.Read(readBuffer.Slice(0, readBytes)); + if (realReadBytes < readBytes) + { + throw new IOException(); + } + + var tableOffset = (alignOffset / blockSize) * 0x10; + _baseStream.Seek(_tableOffset + tableOffset, SeekOrigin.Begin); + var blockNr = 0; + if (readBytes != 0) + { + var tableReadOffset = 0; + var cipherSpan = _gCipherMemory.Span; + do + { + alignBlockSize = readBytes - tableReadOffset; + if (blockSize < readBytes - tableReadOffset) + { + alignBlockSize = blockSize; + } + + if ((blockNr & 0x1f) == 0) + { + if (blockSize == 0) + { + throw new IOException(); + } + + var tableBlock = readBytes / blockSize - blockNr; + if (tableBlock == 0) + { + tableBlock = 1; + } + + if (0x20 < tableBlock) + { + tableBlock = 0x20; + } + cipherSpan.Fill(0); + realReadBytes = _baseStream.Read(cipherSpan[..(tableBlock * 16)]); + if (realReadBytes < tableBlock * 16) + { + throw new IOException(); + } + } + if (_keyIndex < 3) + { + ret = CheckBBMac(readBuffer[tableReadOffset..], alignBlockSize, _versionKey, + cipherSpan.Slice((blockNr & 0x1f) * 16), macType); + } + else + { + ret = CheckBBMac(readBuffer[tableReadOffset..], alignBlockSize, _versionKey, + cipherSpan[((blockNr & 0x1f) * 16)..], macType, alignOffset); + } + + if (ret != 0) + { + throw new IOException(); + } + + tableReadOffset += blockSize; + blockNr++; + } while (tableReadOffset < readBytes); + } + ret = DoBBCipher(readBuffer, readBytes, alignOffset + align >> 4, _versionKey, _desc.Key, cipherType); + + if (ret != 0) + { + throw new IOException(); + } + var iVar2 = uVar6 - align; + seekOffset += iVar2; + _position = seekOffset; + if (readBuffer != bufferSpan) + { + readBuffer.Slice(align, iVar2).CopyTo(bufferSpan); + } + bufferSpan = bufferSpan[iVar2..]; + totalReadSize += iVar2; + alignOffset += uVar6; + } + } + + return 0; + } + + public override long Seek(long offset, SeekOrigin origin) + { + if ((_openFlag & 0x10) == 0) + { + throw new IOException($"Error 0x{80510206:X8}"); + } + + var dataSize = _desc.DataSize; + + switch (origin) + { + case SeekOrigin.Begin: + break; + case SeekOrigin.Current: + offset += -_position; + break; + case SeekOrigin.End: + offset += dataSize; + break; + } + if (offset > 0xffffffff) + { + offset = 0xffffffff; + } + + if (offset > dataSize) + { + offset = dataSize; + } + + _position = offset; + + return _position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + internal static int CheckBBMac(ReadOnlySpan data, int size, ReadOnlySpan key, ReadOnlySpan hash, int macType, int seed = 0) + { + Span mkey = stackalloc byte[Marshal.SizeOf()]; + Span tmpKey = stackalloc byte[0x10]; + int ret = unchecked((int)0x80510201); + if (hash != null) + { + ret = AMCTRL.sceDrmBBMacInit(mkey, macType); + if (ret != 0) + { + return ret; + } + + ret = AMCTRL.sceDrmBBMacUpdate(mkey, data, size); + if (ret != 0) + { + return ret; + } + key.CopyTo(tmpKey); + if (seed != 0) + { + var tmpXor = MemoryMarshal.Cast(tmpKey); + tmpXor[0] ^= seed; + } + + ret = AMCTRL.sceDrmBBMacFinal2(mkey, hash, tmpKey); + if (ret != 0) + { + ret = unchecked((int)0x80510207); + } + + } + + return ret; + } + + internal static int GetMacKey(ReadOnlySpan data, int size, Span key, ReadOnlySpan hash, int macType, int seed = 0) + { + Span mkey = stackalloc byte[Marshal.SizeOf()]; + Span tmpKey = stackalloc byte[0x10]; + int ret = unchecked((int)0x80510201); + if (hash != null) + { + ret = AMCTRL.sceDrmBBMacInit(mkey, macType); + if (ret != 0) + { + return ret; + } + + ret = AMCTRL.sceDrmBBMacUpdate(mkey, data, size); + if (ret != 0) + { + return ret; + } + + ret = AMCTRL.bbmac_getkey(mkey, hash, tmpKey); + if (ret != 0) + { + ret = unchecked((int)0x80510207); + } + if (seed != 0) + { + var tmpXor = MemoryMarshal.Cast(tmpKey); + tmpXor[0] ^= seed; + } + tmpKey.CopyTo(key); + + } + + return ret; + } + + internal static int DoBBCipher(Span data, int size, int seed, ReadOnlySpan versionKey, + ReadOnlySpan headerKey, int cipherType) + { + int ret = AMCTRL.sceDrmBBCipherInit(out var ckey, cipherType, 2, headerKey, versionKey, seed); + if (ret != 0) + { + return ret; + } + + ret = AMCTRL.sceDrmBBCipherUpdate(ref ckey, data, size); + if (ret != 0) + { + return ret; + } + + ret = AMCTRL.sceDrmBBCipherFinal(ref ckey); + return ret; + } + + public byte[] VersionKey => _versionKey; + + public override bool CanRead => _baseStream.CanRead; + public override bool CanSeek => _baseStream.CanSeek; + public override bool CanWrite => false; + public override long Length => _desc.DataSize; + public override long Position + { + get => _position; + set => _position = value; + } + } +} diff --git a/PspCrypto/ECDsaHelper.cs b/PspCrypto/ECDsaHelper.cs new file mode 100644 index 0000000..ad98625 --- /dev/null +++ b/PspCrypto/ECDsaHelper.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using PspCrypto.Security.Cryptography; + +namespace PspCrypto +{ + public static class ECDsaHelper + { + public static ECCurve SetCurve(byte[] p, byte[] a, byte[] b, byte[] n, byte[] gx, byte[] gy) + { + return new ECCurve + { + A = a, + B = b, + Prime = p, + Order = n, + CurveType = ECCurve.ECCurveType.PrimeShortWeierstrass, + Cofactor = new byte[] { 0x01 }, + G = new ECPoint { X = gx, Y = gy } + }; + } + + public static (byte[], ECPoint) GenerateKey(byte[] p, byte[] a, byte[] b, byte[] n, byte[] gx, byte[] gy) + { + var curve = new ECCurve + { + A = a, + B = b, + Prime = p, + Order = n, + CurveType = ECCurve.ECCurveType.PrimeShortWeierstrass, + Cofactor = new byte[] { 0x01 }, + G = { X = gx, Y = gy } + }; + var ecdsa = new ECDsaManaged(); + ecdsa.GenerateKey(curve); + var parameter = ecdsa.ExportExplicitParameters(true); + return (parameter.D, parameter.Q); + } + + public static ECDsa Create(ECCurve curve, byte[] privateKey) + { + return Create(curve, privateKey, new byte[privateKey.Length], new byte[privateKey.Length]); + } + + + public static ECDsa Create(ECCurve curve, byte[] pubx, byte[] puby) + { + return Create(curve, null, pubx, puby); + } + + public static ECDsa Create(ECCurve curve, Span pubx, Span puby) + { + return Create(curve, null, pubx.ToArray(), puby.ToArray()); + } + + public static ECDsa Create(ECCurve curve, byte[] privateKey, byte[] pubx, byte[] puby, bool ebootPbp = false, int type = 1) + { + var par = new ECParameters + { + Curve = curve, + D = privateKey, + Q = { X = pubx, Y = puby } + }; + return new ECDsaManaged(par, ebootPbp, type); + } + + public static ECDsa CreateNet(ECCurve curve, byte[] privateKey, byte[] pubx, byte[] puby) + { + var par = new ECParameters + { + Curve = curve, + D = privateKey, + Q = { X = pubx, Y = puby } + }; + return ECDsa.Create(par); + } + + public static void SignNpImageHeader(Span npHdr) + { + var curve = SetCurve(KeyVault.ec_p, KeyVault.ec_a, KeyVault.ec_b2, KeyVault.ec_N2, KeyVault.Gx2, + KeyVault.Gy2); + using var ecdsa = Create(curve, KeyVault.ec_Priv2, KeyVault.Px2, KeyVault.Py2); + var hash = ecdsa.SignData(npHdr[..0xD8].ToArray(), HashAlgorithmName.SHA1); + hash.CopyTo(npHdr[0xD8..]); + } + + public static bool VerifyEdat(Span edat) + { + var curve = SetCurve(KeyVault.ec_p, KeyVault.ec_a, KeyVault.ec_b2, KeyVault.ec_N2, KeyVault.Gx2, + KeyVault.Gy2); + using var ecdsa = Create(curve, KeyVault.EdatPx, KeyVault.EdatPy); + return ecdsa.VerifyData(edat[..0x58], edat.Slice(0x58, 0x28), HashAlgorithmName.SHA1); + } + + public static void SignEdat(Span edat) + { + var curve = SetCurve(KeyVault.ec_p, KeyVault.ec_a, KeyVault.ec_b2, KeyVault.ec_N2, KeyVault.Gx2, + KeyVault.Gy2); + using var ecdsa = Create(curve, KeyVault.EdatPirv, KeyVault.EdatPx, KeyVault.EdatPy); + var sig = ecdsa.SignData(edat[..0x58].ToArray(), HashAlgorithmName.SHA1); + sig.CopyTo(edat[0x58..]); + } + + public static void SignParamSfo(ReadOnlySpan param, Span sig) + { + var curve = SetCurve(KeyVault.ec_p, KeyVault.ec_a, KeyVault.ec_b2, KeyVault.ec_N2, KeyVault.Gx2, + KeyVault.Gy2); + using var ecdsa = Create(curve, KeyVault.ec_Priv2, KeyVault.Px2, KeyVault.Py2); + var sigTmp = ecdsa.SignData(param.ToArray(), HashAlgorithmName.SHA1); + sigTmp.CopyTo(sig); + } + + public static bool VerifyEbootPbp(Span data, Span sig) + { + var sha224 = SHA224.Create(); + var hash = sha224.ComputeHash(data.ToArray()); + var curve = SetCurve(KeyVault.Eboot_p, KeyVault.Eboot_a, KeyVault.Eboot_b, KeyVault.Eboot_N, KeyVault.Eboot_Gx, + KeyVault.Eboot_Gy); + using var ecdsa = Create(curve, KeyVault.Eboot_priv2, KeyVault.Eboot_pub2x, KeyVault.Eboot_pub2y, true); + return ecdsa.VerifyHash(hash, sig); + } + } +} diff --git a/PspCrypto/Interop/Interop.Bignum.cs b/PspCrypto/Interop/Interop.Bignum.cs new file mode 100644 index 0000000..c9c45a7 --- /dev/null +++ b/PspCrypto/Interop/Interop.Bignum.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using ECDsaTest.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Crypto + { + + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_BigNumDestroy")] + internal static extern void BigNumDestroy(IntPtr a); + + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_BigNumToBinary")] + private static extern unsafe int BigNumToBinary(SafeBignumHandle a, byte* to); + + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_GetBigNumBytes")] + private static extern int GetBigNumBytes(SafeBignumHandle a); + + + internal static byte[] ExtractBignum(IntPtr bignum, int targetSize) + { + // Given that the only reference held to bignum is an IntPtr, create an unowned SafeHandle + // to ensure that we don't destroy the key after extraction. + using (SafeBignumHandle handle = new SafeBignumHandle(bignum, ownsHandle: false)) + { + return ExtractBignum(handle, targetSize); + } + } + + private static unsafe byte[] ExtractBignum(SafeBignumHandle bignum, int targetSize) + { + if (bignum == null || bignum.IsInvalid) + { + return null; + } + + int compactSize = GetBigNumBytes(bignum); + + if (targetSize < compactSize) + { + targetSize = compactSize; + } + + // OpenSSL BIGNUM values do not record leading zeroes. + // Windows Crypt32 does. + // + // Since RSACryptoServiceProvider already checks that RSAParameters.DP.Length is + // exactly half of RSAParameters.Modulus.Length, we need to left-pad (big-endian) + // the array with zeroes. + int offset = targetSize - compactSize; + + byte[] buf = new byte[targetSize]; + + fixed (byte* to = buf) + { + byte* start = to + offset; + BigNumToBinary(bignum, start); + } + + return buf; + } + } +} diff --git a/PspCrypto/Interop/Interop.ERR.cs b/PspCrypto/Interop/Interop.ERR.cs new file mode 100644 index 0000000..ee07020 --- /dev/null +++ b/PspCrypto/Interop/Interop.ERR.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +internal static partial class Interop +{ + internal static partial class Crypto + { + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_ErrClearError")] + internal static extern ulong ErrClearError(); + + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_ErrGetErrorAlloc")] + private static extern ulong ErrGetErrorAlloc([MarshalAs(UnmanagedType.Bool)] out bool isAllocFailure); + + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_ErrErrorStringN")] + private static extern unsafe void ErrErrorStringN(ulong e, byte* buf, int len); + + private static unsafe string ErrErrorStringN(ulong error) + { + var buffer = new byte[1024]; + fixed (byte* buf = &buffer[0]) + { + ErrErrorStringN(error, buf, buffer.Length); + return Marshal.PtrToStringAnsi((IntPtr)buf); + } + } + + + internal static Exception CreateOpenSslCryptographicException() + { + // The Windows cryptography library reports error codes through + // Marshal.GetLastWin32Error, which has a single value when the + // function exits, last writer wins. + // + // OpenSSL maintains an error queue. Calls to ERR_get_error read + // values out of the queue in the order that ERR_set_error wrote + // them. Nothing enforces that a single call into an OpenSSL + // function will guarantee at-most one error being set. + // + // In order to maintain parity in how error flows look between the + // Windows code and the OpenSSL-calling code, drain the queue + // whenever an Exception is desired, and report the exception + // related to the last value in the queue. + bool isAllocFailure; + ulong error = ErrGetErrorAlloc(out isAllocFailure); + ulong lastRead = error; + bool lastIsAllocFailure = isAllocFailure; + + // 0 (there's no named constant) is only returned when the calls + // to ERR_get_error exceed the calls to ERR_set_error. + while (lastRead != 0) + { + error = lastRead; + isAllocFailure = lastIsAllocFailure; + + lastRead = ErrGetErrorAlloc(out lastIsAllocFailure); + } + + // If we're in an error flow which results in an Exception, but + // no calls to ERR_set_error were made, throw the unadorned + // CryptographicException. + if (error == 0) + { + return new CryptographicException(); + } + + if (isAllocFailure) + { + return new OutOfMemoryException(); + } + + // Even though ErrGetError returns ulong (C++ unsigned long), we + // really only expect error codes in the UInt32 range + Debug.Assert(error <= uint.MaxValue, "ErrGetError should only return error codes in the UInt32 range."); + + // If there was an error code, and it wasn't something handled specially, + // use the OpenSSL error string as the message to a CryptographicException. + return new OpenSslCryptographicException(unchecked((int)error), ErrErrorStringN(error)); + } + + private sealed class OpenSslCryptographicException : CryptographicException + { + internal OpenSslCryptographicException(int errorCode, string message) + : base(message) + { + HResult = errorCode; + } + } + } +} diff --git a/PspCrypto/Interop/Interop.EcDsa.ImportExport.cs b/PspCrypto/Interop/Interop.EcDsa.ImportExport.cs new file mode 100644 index 0000000..3bdf33f --- /dev/null +++ b/PspCrypto/Interop/Interop.EcDsa.ImportExport.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using ECDsaTest.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Crypto + { + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EcKeyCreateByExplicitParameters")] + internal static extern SafeEcKeyHandle EcKeyCreateByExplicitParameters( + ECCurve.ECCurveType curveType, + byte[] qx, int qxLength, + byte[] qy, int qyLength, + byte[] d, int dLength, + byte[] p, int pLength, + byte[] a, int aLength, + byte[] b, int bLength, + byte[] gx, int gxLength, + byte[] gy, int gyLength, + byte[] order, int nLength, + byte[] cofactor, int cofactorLength, + byte[] seed, int seedLength); + + internal static SafeEcKeyHandle EcKeyCreateByExplicitCurve(ECCurve curve) + { + byte[] p; + if (curve.IsPrime) + { + p = curve.Prime; + } + else if (curve.IsCharacteristic2) + { + p = curve.Polynomial; + } + else + { + throw new PlatformNotSupportedException(string.Format("The specified curve '{0}' or its parameters are not valid for this platform.", curve.CurveType.ToString())); + } + + SafeEcKeyHandle key = Interop.Crypto.EcKeyCreateByExplicitParameters( + curve.CurveType, + null, 0, + null, 0, + null, 0, + p, p.Length, + curve.A, curve.A.Length, + curve.B, curve.B.Length, + curve.G.X, curve.G.X.Length, + curve.G.Y, curve.G.Y.Length, + curve.Order, curve.Order.Length, + curve.Cofactor, curve.Cofactor.Length, + curve.Seed, curve.Seed == null ? 0 : curve.Seed.Length); + + if (key == null || key.IsInvalid) + { + if (key != null) + key.Dispose(); + throw Interop.Crypto.CreateOpenSslCryptographicException(); + } + + // EcKeyCreateByExplicitParameters may have polluted the error queue, but key was good in the end. + // Clean up the error queue. + Interop.Crypto.ErrClearError(); + + return key; + } + } +} diff --git a/PspCrypto/Interop/Interop.EcDsa.cs b/PspCrypto/Interop/Interop.EcDsa.cs new file mode 100644 index 0000000..e3c909d --- /dev/null +++ b/PspCrypto/Interop/Interop.EcDsa.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using ECDsaTest; +using ECDsaTest.SafeHandles; + +internal static partial class Interop +{ + internal static partial class Crypto + { + internal static bool EcDsaSignEx(ReadOnlySpan dgst, Span sig, [In, Out] ref int siglen, + ReadOnlySpan kinv, ReadOnlySpan rp, SafeEcKeyHandle ecKey) => EcDsaSignEx( + ref MemoryMarshal.GetReference(dgst), dgst.Length, ref MemoryMarshal.GetReference(sig), ref siglen, + ref MemoryMarshal.GetReference(kinv), kinv.Length, ref MemoryMarshal.GetReference(rp), rp.Length, ecKey); + + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EcDsaSignEx")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool EcDsaSignEx(ref byte dgst, int dlen, ref byte sig, [In, Out] ref int siglen, + ref byte kinv, int kinvlen, ref byte rp, int rplen, SafeEcKeyHandle ecKey); + + // returns the maximum length of a DER encoded ECDSA signature created with this key. + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EcDsaSize")] + private static extern int CryptoNative_EcDsaSize(SafeEcKeyHandle ecKey); + + internal static int EcDsaSize(SafeEcKeyHandle ecKey) + { + int ret = CryptoNative_EcDsaSize(ecKey); + + if (ret == 0) + { + throw CreateOpenSslCryptographicException(); + } + + return ret; + } + + + internal static PspParameter EcPspParameter(SafeEcKeyHandle key, ReadOnlySpan sha256, int len) + { + SafeBignumHandle kinv_bn, rp_bn; + int kinvlen, rplen; + + bool refAdded = false; + try + { + key.DangerousAddRef(ref refAdded); + var ret = EcPspParameter(key, ref MemoryMarshal.GetReference(sha256), len, out kinv_bn, out kinvlen, + out rp_bn, out rplen); + if (!ret) + { + throw Interop.Crypto.CreateOpenSslCryptographicException(); + } + + using (kinv_bn) + using (rp_bn) + { + var par = new PspParameter + { + Kinv = Crypto.ExtractBignum(kinv_bn, kinvlen), + Rp = Crypto.ExtractBignum(rp_bn, rplen) + }; + return par; + } + } + finally + { + if (refAdded) + key.DangerousRelease(); + } + } + + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EcPspParameter")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool EcPspParameter(SafeEcKeyHandle ecKey, ref byte sha256, int len, out SafeBignumHandle kinv, + out int kinvlen, out SafeBignumHandle rp, out int rplen); + } +} diff --git a/PspCrypto/Interop/Interop.EcKey.cs b/PspCrypto/Interop/Interop.EcKey.cs new file mode 100644 index 0000000..62625d7 --- /dev/null +++ b/PspCrypto/Interop/Interop.EcKey.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +internal static partial class Interop +{ + internal static partial class Crypto + { + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EcKeyDestroy")] + internal static extern void EcKeyDestroy(IntPtr a); + + [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EcKeyUpRef")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool EcKeyUpRef(IntPtr r); + } +} diff --git a/PspCrypto/KIRKEngine.cs b/PspCrypto/KIRKEngine.cs new file mode 100644 index 0000000..33a2db7 --- /dev/null +++ b/PspCrypto/KIRKEngine.cs @@ -0,0 +1,1113 @@ +using System; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace PspCrypto +{ + public static class KIRKEngine + { + // KIRK return values + public const int KIRK_OPERATION_SUCCESS = 0; + public const int KIRK_NOT_ENABLED = 1; + public const int KIRK_INVALID_MODE = 2; + public const int KIRK_HEADER_HASH_INVALID = 3; + public const int KIRK_DATA_HASH_INVALID = 4; + public const int KIRK_SIG_CHECK_INVALID = 5; + public const int KIRK_UNK_1 = 6; + public const int KIRK_UNK_2 = 7; + public const int KIRK_UNK_3 = 8; + public const int KIRK_UNK_4 = 9; + public const int KIRK_UNK_5 = 0xA; + public const int KIRK_UNK_6 = 0xB; + public const int KIRK_NOT_INITIALIZED = 0xC; + public const int KIRK_INVALID_OPERATION = 0xD; + public const int KIRK_INVALID_SEED_CODE = 0xE; + public const int KIRK_INVALID_SIZE = 0xF; + public const int KIRK_DATA_SIZE_ZERO = 0x10; + + // sceUtilsBufferCopyWithRange modes + public const int KIRK_CMD_DECRYPT_PRIVATE = 1; + public const int KIRK_CMD_2 = 2; + public const int KIRK_CMD_3 = 3; + public const int KIRK_CMD_ENCRYPT_IV_0 = 4; + public const int KIRK_CMD_ENCRYPT_IV_FUSE = 5; + public const int KIRK_CMD_ENCRYPT_IV_USER = 6; + public const int KIRK_CMD_DECRYPT_IV_0 = 7; + public const int KIRK_CMD_DECRYPT_IV_FUSE = 8; + public const int KIRK_CMD_DECRYPT_IV_USER = 9; + public const int KIRK_CMD_PRIV_SIGN_CHECK = 10; + public const int KIRK_CMD_SHA1_HASH = 11; + public const int KIRK_CMD_ECDSA_GEN_KEYS = 12; + public const int KIRK_CMD_ECDSA_MULTIPLY_POINT = 13; + public const int KIRK_CMD_PRNG = 14; + public const int KIRK_CMD_15 = 15; + public const int KIRK_CMD_ECDSA_SIGN = 16; + public const int KIRK_CMD_ECDSA_VERIFY = 17; + + // KIRK header modes + public const int KIRK_MODE_CMD1 = 1; + public const int KIRK_MODE_CMD2 = 2; + public const int KIRK_MODE_CMD3 = 3; + public const int KIRK_MODE_ENCRYPT_CBC = 4; + public const int KIRK_MODE_DECRYPT_CBC = 5; + + // sceUtilsBufferCopyWithRange errors + public const int SUBCWR_NOT_16_ALGINED = 0x90A; + public const int SUBCWR_HEADER_HASH_INVALID = 0x920; + public const int SUBCWR_BUFFER_TOO_SMALL = 0x1000; + + [StructLayout(LayoutKind.Sequential)] + public struct KIRK_AES128CBC_HEADER + { + public int mode; + public int unk_4; + public int unk_8; + public int keyseed; + public int data_size; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct KIRK_CMD1_HEADER + { + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + private fixed byte _AES_key[16]; + public Span AES_Key + { + get + { + fixed (byte* ptr = _AES_key) + { + return new Span(ptr, 16); + } + } + } + public Span AESKeys + { + get + { + fixed (byte* ptr = _AES_key) + { + return new Span(ptr, 32); + } + } + } + + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + private fixed byte _CMAC_key[16]; // 0x10 + public Span CMAC_key + { + get + { + fixed (byte* ptr = _CMAC_key) + { + return new Span(ptr, 16); + } + } + } + + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + private fixed byte _CMAC_header_hash[16]; // 0x20 + public Span CMAC_header_hash + { + get + { + fixed (byte* ptr = _CMAC_header_hash) + { + return new Span(ptr, 16); + } + } + } + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + private fixed byte _CMAC_data_hash[16]; // 0x30 + public Span CMAC_data_hash + { + get + { + fixed (byte* ptr = _CMAC_data_hash) + { + return new Span(ptr, 16); + } + } + } + + public Span content + { + get + { + fixed (byte* ptr = unused) + { + return new Span(ptr, 0x40); + } + } + } + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + fixed byte unused[32]; // 0x40 + public uint mode; // 0x60 + public byte ecdsa_hash; // 0x64 + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)] + fixed byte unk3[11]; // 0x65 + public int data_size; // 0x70 + + public Span off70 + { + get + { + fixed (int* ptr = &data_size) + { + return new Span(ptr, 0x10); + } + } + } + public int data_offset; // 0x74 + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + fixed byte unk4[8]; // 0x78 + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + fixed byte unk5[16]; // 0x80 + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct KIRK_CMD1_ECDSA_HEADER + { + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + private fixed byte _AES_key[16]; + + public Span AES_Key + { + get + { + fixed (byte* ptr = _AES_key) + { + return new Span(ptr, 16); + } + } + } + + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + private fixed byte _header_sig_r[20]; // 0x10 + + public Span header_sig_r + { + get + { + fixed (byte* ptr = _header_sig_r) + { + return new Span(ptr, 20); + } + } + } + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + private fixed byte _header_sig_s[20]; // 0x24 + + public Span header_sig_s + { + get + { + fixed (byte* ptr = _header_sig_s) + { + return new Span(ptr, 20); + } + } + } + + public Span header_sig + { + get + { + fixed (byte* ptr = _header_sig_r) + { + return new Span(ptr, 40); + } + } + } + + + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + private fixed byte _data_sig_r[20]; // 0x38 + + public Span data_sig_r + { + get + { + fixed (byte* ptr = _data_sig_r) + { + return new Span(ptr, 20); + } + } + } + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + private fixed byte _data_sig_s[20]; // 0x4c + public Span data_sig_s + { + get + { + fixed (byte* ptr = _data_sig_s) + { + return new Span(ptr, 20); + } + } + } + + public Span data_sig + { + get + { + fixed (byte* ptr = _data_sig_r) + { + return new Span(ptr, 40); + } + } + } + + + public uint mode; //0x60 + public byte ecdsa_hash; + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 11)] + fixed byte unk3[11]; + uint data_size; + uint data_offset; + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + fixed byte unk4[8]; + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + fixed byte unk5[16]; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct ECDSA_SIG + { + private fixed byte _r[0x14]; + + public Span r + { + get + { + fixed (byte* ptr = _r) + { + return new Span(ptr, 0x14); + } + } + } + private fixed byte _s[0x14]; + + public Span s + { + get + { + fixed (byte* ptr = _s) + { + return new Span(ptr, 0x14); + } + } + } + + public Span sig + { + get + { + fixed (byte* ptr = _r) + { + return new Span(ptr, 0x28); + } + } + } + + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct ECDSA_POINT + { + private fixed byte _x[0x14]; + + public Span x + { + get + { + fixed (byte* ptr = _x) + { + return new Span(ptr, 0x14); + } + } + } + private fixed byte _y[0x14]; + + + public Span y + { + get + { + fixed (byte* ptr = _y) + { + return new Span(ptr, 0x14); + } + } + } + + public Span point + { + get + { + fixed (byte* ptr = _x) + { + return new Span(ptr, 0x28); + } + } + } + } + + [StructLayout(LayoutKind.Sequential)] + public struct KIRK_SHA1_HEADER + { + public int data_size; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct KIRK_CMD12_BUFFER + { + private fixed byte private_key_[0x14]; + + public Span private_key + { + get + { + fixed (byte* ptr = private_key_) + { + return new Span(ptr, 0x14); + } + } + } + public ECDSA_POINT public_key; + } + + [StructLayout(LayoutKind.Sequential)] + public struct KIRK_CMD13_BUFFER + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] + public byte[] multiplier; + public ECDSA_POINT public_key; + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct KIRK_CMD16_BUFFER + { + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x20)] + private fixed byte enc_private_[0x20]; + + public Span enc_private + { + get + { + fixed (byte* ptr = enc_private_) + { + return new Span(ptr, 0x20); + } + } + } + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 0x14)] + private fixed byte message_hash_[0x14]; + + public Span message_hash + { + get + { + fixed (byte* ptr = message_hash_) + { + return new Span(ptr, 0x14); + } + } + } + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct KIRK_CMD17_BUFFER + { + public ECDSA_POINT public_key; + private fixed byte _message_hash[0x14]; + + public Span message_hash + { + get + { + fixed (byte* ptr = _message_hash) + { + return new Span(ptr, 0x14); + } + } + } + public ECDSA_SIG signature; + } + // KIRK commands + /* + // Private Sig + Cipher + 0x01: Super-Duper decryption (no inverse) + 0x02: Encrypt Operation (inverse of 0x03) + 0x03: Decrypt Operation (inverse of 0x02) + + // Cipher + 0x04: Encrypt Operation (inverse of 0x07) (IV=0) + 0x05: Encrypt Operation (inverse of 0x08) (IV=FuseID) + 0x06: Encrypt Operation (inverse of 0x09) (IV=UserDefined) + 0x07: Decrypt Operation (inverse of 0x04) + 0x08: Decrypt Operation (inverse of 0x05) + 0x09: Decrypt Operation (inverse of 0x06) + + // Sig Gens + 0x0A: Private Signature Check (checks for private SCE sig) + 0x0B: SHA1 Hash + 0x0C: Mul1 + 0x0D: Mul2 + 0x0E: Random Number Gen + 0x0F: (absolutely no idea – could be KIRK initialization) + 0x10: Signature Gen + + // Sig Checks + 0x11: Signature Check (checks for generated sigs) + 0x12: Certificate Check (idstorage signatures) + */ + + // Internal variables + [StructLayout(LayoutKind.Sequential)] + private unsafe struct kirk16_data + { + private fixed byte fuseid_[8]; + + public Span fuseid + { + get + { + fixed (byte* ptr = fuseid_) + { + return new Span(ptr, 8); + } + } + } + private fixed byte mesh_[0x40]; + + public Span mesh + { + get + { + fixed (byte* ptr = mesh_) + { + return new Span(ptr, 0x40); + } + } + } + } + + //[StructLayout(LayoutKind.Sequential)] + //private unsafe struct header_keys + //{ + // // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + // public fixed byte AES[16]; + // // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + // public fixed byte CMAC[16]; + //} + + private static uint g_fuse90; + private static uint g_fuse94; + + private static Aes aes_kirk1; + private static byte[] PRNG_DATA = new byte[0x14]; + + private static bool is_kirk_initialized; + private static readonly byte[] IV0 = new byte[16]; + + // Internal functions + private static byte[] kirk_4_7_get_key(int key_type) + { + if ((key_type < 0) || (key_type >= 0x80)) throw new Exception("KIRK_INVALID_SIZE"); + return KeyVault.kirkKeys[key_type]; + } + + private static Aes CreateAes() + { + var aes_ctx = Aes.Create(); + aes_ctx.Mode = CipherMode.CBC; + aes_ctx.Padding = PaddingMode.None; + aes_ctx.IV = IV0; + return aes_ctx; + } + + public static void decrypt_kirk16_private(Span dA_out, Span dA_enc) + { + int i, k; + kirk16_data keydata; + byte[] subkey_1 = new byte[0x10], subkey_2 = new byte[0x10]; + using var aes_ctx = CreateAes(); + + keydata.fuseid[7] = (byte)(g_fuse90 & 0xFF); + keydata.fuseid[6] = (byte)((g_fuse90 >> 8) & 0xFF); + keydata.fuseid[5] = (byte)((g_fuse90 >> 16) & 0xFF); + keydata.fuseid[4] = (byte)((g_fuse90 >> 24) & 0xFF); + keydata.fuseid[3] = (byte)(g_fuse94 & 0xFF); + keydata.fuseid[2] = (byte)((g_fuse94 >> 8) & 0xFF); + keydata.fuseid[1] = (byte)((g_fuse94 >> 16) & 0xFF); + keydata.fuseid[0] = (byte)((g_fuse94 >> 24) & 0xFF); + + /* set encryption key */ + aes_ctx.Key = KeyVault.kirk16_key; + + /* set the subkeys */ + for (i = 0; i < 0x10; i++) + { + /* set to the fuseid */ + subkey_2[i] = subkey_1[i] = keydata.fuseid[i % 8]; + } + /* do aes crypto */ + using (var encryptor = aes_ctx.CreateEncryptor()) + { + using var decryptor = aes_ctx.CreateDecryptor(); + for (i = 0; i < 3; i++) + { + /* encrypt + decrypt */ + encryptor.TransformBlock(subkey_1, 0, subkey_1.Length, subkey_1, 0); + decryptor.TransformBlock(subkey_2, 0, subkey_2.Length, subkey_2, 0); + } + } + + /* set new key */ + aes_ctx.Key = subkey_1; + using (var encryptor = aes_ctx.CreateEncryptor()) + { + /* now lets make the key mesh */ + for (i = 0; i < 3; i++) + { + /* do encryption in group of 3 */ + for (k = 0; k < 3; k++) + { + /* crypto */ + encryptor.TransformBlock(subkey_2, 0, subkey_2.Length, subkey_2, 0); + } + + /* copy to out block */ + subkey_2.AsSpan().CopyTo(keydata.mesh[(i * 0x10)..]); + } + } + + /* set the key to the mesh */ + aes_ctx.Key = keydata.mesh.Slice(0x20, 0x10).ToArray(); + + using (var encryptor = aes_ctx.CreateEncryptor()) + { + /* do the encryption routines for the aes key */ + for (i = 0; i < 2; i++) + { + /* encrypt the data */ + var tmp = encryptor.TransformFinalBlock(keydata.mesh.ToArray(), 0x10, 0x30); + tmp.CopyTo(keydata.mesh[0x10..]); + } + } + + /* set the key to that mesh shit */ + using var aes = CreateAes(); + aes.Key = keydata.mesh.Slice(0x10, 0x10).ToArray(); + + + /* cbc decrypt the dA */ + AesHelper.AesDecrypt(aes, dA_out, dA_enc, 0x20); + } + + public static void encrypt_kirk16_private(Span dA_out, Span dA_dec) + { + int i, k; + kirk16_data keydata; + byte[] subkey_1 = new byte[0x10], subkey_2 = new byte[0x10]; + using var aes_ctx = CreateAes(); + + keydata.fuseid[7] = (byte)(g_fuse90 & 0xFF); + keydata.fuseid[6] = (byte)((g_fuse90 >> 8) & 0xFF); + keydata.fuseid[5] = (byte)((g_fuse90 >> 16) & 0xFF); + keydata.fuseid[4] = (byte)((g_fuse90 >> 24) & 0xFF); + keydata.fuseid[3] = (byte)(g_fuse94 & 0xFF); + keydata.fuseid[2] = (byte)((g_fuse94 >> 8) & 0xFF); + keydata.fuseid[1] = (byte)((g_fuse94 >> 16) & 0xFF); + keydata.fuseid[0] = (byte)((g_fuse94 >> 24) & 0xFF); + + /* set encryption key */ + aes_ctx.Key = KeyVault.kirk16_key; + + /* set the subkeys */ + for (i = 0; i < 0x10; i++) + { + /* set to the fuseid */ + subkey_2[i] = subkey_1[i] = keydata.fuseid[i % 8]; + } + /* do aes crypto */ + using (var encryptor = aes_ctx.CreateEncryptor()) + { + using var decryptor = aes_ctx.CreateDecryptor(); + for (i = 0; i < 3; i++) + { + /* encrypt + decrypt */ + subkey_1 = encryptor.TransformFinalBlock(subkey_1, 0, subkey_1.Length); + subkey_2 = decryptor.TransformFinalBlock(subkey_2, 0, subkey_2.Length); + } + } + + /* set new key */ + aes_ctx.Key = subkey_1; + using (var encryptor = aes_ctx.CreateEncryptor()) + { + /* now lets make the key mesh */ + for (i = 0; i < 3; i++) + { + /* do encryption in group of 3 */ + for (k = 0; k < 3; k++) + { + /* crypto */ + encryptor.TransformBlock(subkey_2, 0, subkey_2.Length, subkey_2, 0); + } + + /* copy to out block */ + subkey_2.AsSpan().CopyTo(keydata.mesh.Slice(i * 0x10)); + } + } + + /* set the key to the mesh */ + aes_ctx.Key = keydata.mesh.Slice(0x20, 0x10).ToArray(); + + using (var encryptor = aes_ctx.CreateEncryptor()) + { + /* do the encryption routines for the aes key */ + for (i = 0; i < 2; i++) + { + /* encrypt the data */ + var tmp = encryptor.TransformFinalBlock(keydata.mesh.ToArray(), 0x10, 0x30); + tmp.CopyTo(keydata.mesh.Slice(0x10)); + } + } + + /* set the key to that mesh shit */ + using var aes = CreateAes(); + aes.Key = keydata.mesh.Slice(0x10, 0x10).ToArray(); + + + /* cbc encrypt the dA */ + AesHelper.AesEncrypt(aes, dA_out, dA_dec, 0x20); + } + + static KIRKEngine() + { + kirk_init(); + } + + // KIRK commands + + public static int kirk_init() + { + //94 90 + //0x0000332 e6e050311 3000 + //0x0000772 ccda50f12 2000 + //0x1008010 1ef1e7101 vita + return kirk_init2(Encoding.ASCII.GetBytes("Lazy Dev should have initialized!"), 33, 0xBABEF00D, 0xDEADBEEF); + } + + public static int kirk_init2(byte[] rnd_seed, int seed_size, uint fuseid_90, uint fuseid_94) + { + Span temp = stackalloc byte[0x104]; + + // Another randomly selected data for a "key" to add to each randomization + ReadOnlySpan key = stackalloc byte[] { 0x07, 0xAB, 0xEF, 0xF8, 0x96, 0x8C, 0xF3, 0xD6, 0x14, 0xE0, 0xEB, 0xB2, 0x9D, 0x8B, 0x4E, 0x74 }; + uint curtime; + + is_kirk_initialized = true; + + //Set PRNG_DATA initially, otherwise use what ever uninitialized data is in the buffer + if (seed_size > 0) + { + Span seedbuf = stackalloc byte[seed_size + 4]; + RandomNumberGenerator.Fill(seedbuf); + var seedheader = new KIRK_SHA1_HEADER + { + data_size = seed_size + }; + MemoryMarshal.Write(seedbuf, ref seedheader); + kirk_CMD11(PRNG_DATA, seedbuf, seed_size + 4); + } + // Buffer.BlockCopy(PRNG_DATA, 0, header.data, 0, 0x14); + PRNG_DATA.CopyTo(temp.Slice(4)); + + // This uses the standard C time function for portability. + curtime = (uint)DateTimeOffset.Now.ToUnixTimeMilliseconds(); + temp[0x18] = (byte)(curtime & 0xFF); + temp[0x19] = (byte)((curtime >> 8) & 0xFF); + temp[0x1A] = (byte)((curtime >> 16) & 0xFF); + temp[0x1B] = (byte)((curtime >> 24) & 0xFF); + // Buffer.BlockCopy(key, 0, header.data, 0x18, 0x10); + key.CopyTo(temp.Slice(0x1c)); + + // This leaves the remainder of the 0x100 bytes in temp to whatever remains on the stack + // in an uninitialized state. This should add unpredicableness to the results as well + + var header = new KIRK_SHA1_HEADER + { + data_size = 0x100 + }; + MemoryMarshal.Write(temp, ref header); + kirk_CMD11(PRNG_DATA, temp, 0x104); + + + //Set Fuse ID + g_fuse90 = fuseid_90; + g_fuse94 = fuseid_94; + + // Set KIRK1 main key + aes_kirk1 = CreateAes(); + aes_kirk1.Key = KeyVault.kirk1_key; + return 0; + } + + static int kirk_CMD0(Span outbuff, ReadOnlySpan inbuff, int size, bool generate_trash) + { + KIRK_CMD1_HEADER header = Utils.AsRef(outbuff); + // header_keys keys = Utils.AsRef(outbuff); + int chk_size; + Aes k1; + Aes cmac_key; + + if (!is_kirk_initialized) return KIRK_NOT_INITIALIZED; + + inbuff[..size].CopyTo(outbuff); + + if (header.mode != KIRK_MODE_CMD1) return KIRK_INVALID_MODE; + + // FILL PREDATA WITH RANDOM DATA + if (generate_trash) kirk_CMD14(outbuff[Unsafe.SizeOf()..], header.data_offset); + + // Make sure data is 16 aligned + chk_size = header.data_size; + if (chk_size % 16 != 0) chk_size += 16 - (chk_size % 16); + + // ENCRYPT DATA + + // ENCRYPT DATA + using (k1 = CreateAes()) + { + k1.Key = header.AES_Key.ToArray(); + AesHelper.AesEncrypt(k1, outbuff[(Unsafe.SizeOf() + header.data_offset)..], + inbuff.Slice(Unsafe.SizeOf() + header.data_offset, header.data_size)); + //byte[] encData = AesHelper.AesEncrypt(k1, inbuff.ToArray(), + // Unsafe.SizeOf() + header.data_offset, + // chk_size); + //encData.CopyTo(outbuff.Slice(Unsafe.SizeOf() + header.data_offset)); + } + + + // CMAC HASHES + using (cmac_key = CreateAes()) + { + cmac_key.Key = header.CMAC_key.ToArray(); + //var cmac_header_hash = AesHelper.Cmac(cmac_key, outbuff.ToArray(), 0x60, 0x30); + //var cmac_data_hash = AesHelper.Cmac(cmac_key, outbuff.ToArray(), 0x60, 0x30 + chk_size); + Span cmac_header_hash = stackalloc byte[16]; + Span cmac_data_hash = stackalloc byte[16]; + AesHelper.Cmac(cmac_key, cmac_header_hash, outbuff.Slice(0x60, 0x30)); + AesHelper.Cmac(cmac_key, cmac_data_hash, outbuff.Slice(0x60, 0x30 + chk_size)); + cmac_header_hash.CopyTo(header.CMAC_header_hash); + cmac_data_hash.CopyTo(header.CMAC_data_hash); + } + + // ENCRYPT KEYS + // AesHelper.AesEncrypt(aes_kirk1) + return KIRK_OPERATION_SUCCESS; + } + + static int kirk_CMD1(Span outbuff, ReadOnlySpan inbuff, int size) + { + KIRK_CMD1_HEADER header = Utils.AsRef(inbuff); + // header_keys keys; //0-15 AES key, 16-31 CMAC key + Aes k1; + + if (size < 0x90) return KIRK_INVALID_SIZE; + if (!is_kirk_initialized) return KIRK_NOT_INITIALIZED; + if (header.mode != KIRK_MODE_CMD1) return KIRK_INVALID_MODE; + // byte[] keytmp = AesHelper.AesDecrypt(aes_kirk1, inbuff.ToArray(), 0, 32); + Span keytmp = stackalloc byte[32]; + AesHelper.AesDecrypt(aes_kirk1, keytmp, inbuff, 32); + + + if (header.ecdsa_hash == 1) + { + KIRK_CMD1_ECDSA_HEADER eheader = Utils.AsRef(inbuff); + var curve = ECDsaHelper.SetCurve(KeyVault.ec_p, KeyVault.ec_a, KeyVault.ec_b1, KeyVault.ec_N1, KeyVault.Gx1, + KeyVault.Gy1); + unsafe + { + var ecdsa = ECDsaHelper.Create(curve, KeyVault.ec_Priv1); + if (!ecdsa.VerifyData(inbuff.Slice(0x60, 0x30), eheader.header_sig.ToArray(), + HashAlgorithmName.SHA1)) + { + return KIRK_HEADER_HASH_INVALID; + } + ecdsa = ECDsaHelper.Create(curve, KeyVault.ec_Priv1); + if (!ecdsa.VerifyData(inbuff[0x60..size], eheader.data_sig.ToArray(), HashAlgorithmName.SHA1)) + { + return KIRK_DATA_HASH_INVALID; + } + } + } + else + { + int ret = kirk_CMD10(inbuff, size); + if (ret != KIRK_OPERATION_SUCCESS) return ret; + } + + k1 = CreateAes(); + k1.Key = keytmp.Slice(0, 16).ToArray(); + //var outtmp = AesHelper.AesDecrypt(k1, inbuff.ToArray(), Unsafe.SizeOf() + header.data_offset, header.data_size); + // Buffer.BlockCopy(outtmp, 0, outbuff, 0, (int)header.data_size); + + AesHelper.AesDecrypt(k1, outbuff, inbuff.Slice(Unsafe.SizeOf() + header.data_offset), header.data_size); + + return KIRK_OPERATION_SUCCESS; + } + + static int kirk_CMD4(Span outbuff, ReadOnlySpan inbuff, int size) + { + KIRK_AES128CBC_HEADER header = Utils.AsRef(inbuff); + byte[] key; + Aes aes; + + if (!is_kirk_initialized) return KIRK_NOT_INITIALIZED; + if (header.mode != KIRK_MODE_ENCRYPT_CBC) return KIRK_INVALID_MODE; + if (header.data_size == 0) return KIRK_DATA_SIZE_ZERO; + // size = size - 20; + using (aes = CreateAes()) + { + key = kirk_4_7_get_key(header.keyseed); + + // Set the key + aes.Key = key; + //var enc = AesHelper.AesEncrypt(aes, inbuff.ToArray(), Unsafe.SizeOf(), size); + // Buffer.BlockCopy(enc, 0, outbuff, Marshal.SizeOf() + outOffset, size); + //enc.AsSpan(0, size).CopyTo(outbuff.Slice(Unsafe.SizeOf(), size)); + AesHelper.AesEncrypt(aes, outbuff.Slice(Unsafe.SizeOf()), + inbuff.Slice(Unsafe.SizeOf(), header.data_size)); + } + return KIRK_OPERATION_SUCCESS; + } + + static int kirk_CMD7(Span outbuff, ReadOnlySpan inbuff, int size) + { + KIRK_AES128CBC_HEADER header = Utils.AsRef(inbuff); + byte[] key; + Aes aes; + + if (!is_kirk_initialized) return KIRK_NOT_INITIALIZED; + if (header.mode != KIRK_MODE_DECRYPT_CBC) return KIRK_INVALID_MODE; + if (header.data_size == 0) return KIRK_DATA_SIZE_ZERO; + using (aes = CreateAes()) + { + key = kirk_4_7_get_key(header.keyseed); + + // Set the key + aes.Key = key; + //var enc = AesHelper.AesDecrypt(aes, inbuff.ToArray(), Unsafe.SizeOf(), header.data_size); + //Buffer.BlockCopy(enc, 0, outbuff, outOffset, header.data_size); + //enc.AsSpan(0, header.data_size).CopyTo(outbuff); + AesHelper.AesDecrypt(aes, outbuff, inbuff.Slice(Unsafe.SizeOf()), header.data_size); + } + return KIRK_OPERATION_SUCCESS; + } + + static int kirk_CMD10(ReadOnlySpan inbuff, int insize) + { + KIRK_CMD1_HEADER header = Utils.AsRef(inbuff); + // header_keys keys; //0-15 AES key, 16-31 CMAC key + Span cmac_header_hash = stackalloc byte[16]; + Span cmac_data_hash = stackalloc byte[16]; + using Aes cmac_key = CreateAes(); + int chk_size; + + if (!is_kirk_initialized) return KIRK_NOT_INITIALIZED; + if (!(header.mode == KIRK_MODE_CMD1 || header.mode == KIRK_MODE_CMD2 || header.mode == KIRK_MODE_CMD3)) return KIRK_INVALID_MODE; + if (header.data_size == 0) return KIRK_DATA_SIZE_ZERO; + if (header.mode == KIRK_MODE_CMD1) + { + // var decdata = AesHelper.AesDecrypt(aes_kirk1, inbuff.ToArray(), 0, 32); + Span tmpKey = stackalloc byte[32]; + AesHelper.AesDecrypt(aes_kirk1, tmpKey, inbuff, 32); + cmac_key.Key = tmpKey[16..].ToArray(); + //cmac_header_hash = AesHelper.Cmac(cmac_key, inbuff.ToArray(), 0x60, 0x30); + AesHelper.Cmac(cmac_key, cmac_header_hash, inbuff.Slice(0x60, 0x30)); + + // Make sure data is 16 aligned + chk_size = header.data_size; + if (chk_size % 16 != 0) chk_size += 16 - (chk_size % 16); + //cmac_data_hash = AesHelper.Cmac(cmac_key, inbuff.ToArray(), 0x60, 0x30 + chk_size + header.data_offset); + AesHelper.Cmac(cmac_key, cmac_data_hash, inbuff.Slice(0x60, 0x30 + chk_size + header.data_offset)); + + if (!header.CMAC_header_hash.SequenceEqual(cmac_header_hash)) return KIRK_HEADER_HASH_INVALID; + if (!header.CMAC_data_hash.SequenceEqual(cmac_data_hash)) return KIRK_DATA_HASH_INVALID; + + return KIRK_OPERATION_SUCCESS; + } + + return KIRK_SIG_CHECK_INVALID; //Checks for cmd 2 & 3 not included right now + } + + static int kirk_CMD11(Span outbuff, ReadOnlySpan inbuff, int size) + { + KIRK_SHA1_HEADER header = Utils.AsRef(inbuff); + if (!is_kirk_initialized) return KIRK_NOT_INITIALIZED; + if (header.data_size == 0 || size == 0) return KIRK_DATA_SIZE_ZERO; + + //byte[] buff = new byte[header.data_size + 4]; + //using (var ms = new MemoryStream(buff)) + //{ + // using var bw = new BinaryWriter(ms); + // bw.Write(inbuff.data_size); + // bw.Write(inbuff.data); + //} + var sha = SHA1.Create(); + var hash = sha.ComputeHash(inbuff.Slice(4, header.data_size).ToArray()); + // Buffer.BlockCopy(hash, 0, outbuff, 0, hash.Length); + hash.CopyTo(outbuff); + return KIRK_OPERATION_SUCCESS; + } + + static int kirk_CMD12(Span outbuff, int outsize) + { + if (outsize != 0x3C) return KIRK_INVALID_SIZE; + + var (d, q) = ECDsaHelper.GenerateKey(KeyVault.ec_p, KeyVault.ec_a, KeyVault.ec_b2, KeyVault.ec_N2, KeyVault.Gx2, + KeyVault.Gy2); + d.AsSpan(0, 0x14).CopyTo(outbuff); + q.X.AsSpan(0, 0x14).CopyTo(outbuff.Slice(0x14)); + q.Y.AsSpan(0, 0x14).CopyTo(outbuff.Slice(0x28)); + //Buffer.BlockCopy(d, 0, outbuff, 0, 0x14); + //Buffer.BlockCopy(q.X, 0, outbuff, 0x14, 0x14); + //Buffer.BlockCopy(q.Y, 0, outbuff, 0x28, 0x14); + + return KIRK_OPERATION_SUCCESS; + } + + //static int kirk_CMD13() + + static int kirk_CMD14(Span outbuff, int outsize) + { + Span temp = stackalloc byte[0x104]; + + // Some randomly selected data for a "key" to add to each randomization + Span key = stackalloc byte[] { 0xA7, 0x2E, 0x4C, 0xB6, 0xC3, 0x34, 0xDF, 0x85, 0x70, 0x01, 0x49, 0xFC, 0xC0, 0x87, 0xC4, 0x77 }; + uint curtime; + + if (outsize <= 0) return KIRK_OPERATION_SUCCESS; + // Buffer.BlockCopy(PRNG_DATA, 0, header.data, 0, 0x14); + PRNG_DATA.CopyTo(temp.Slice(4)); + + // This uses the standard C time function for portability. + curtime = (uint)DateTimeOffset.Now.ToUnixTimeMilliseconds(); + temp[0x18] = (byte)(curtime & 0xFF); + temp[0x19] = (byte)((curtime >> 8) & 0xFF); + temp[0x1A] = (byte)((curtime >> 16) & 0xFF); + temp[0x1B] = (byte)((curtime >> 24) & 0xFF); + // Buffer.BlockCopy(key, 0, header.data, 0x18, 0x10); + key.CopyTo(temp[0x1C..]); + + // This leaves the remainder of the 0x100 bytes in temp to whatever remains on the stack + // in an uninitialized state. This should add unpredicableness to the results as well + // header.data_size = 0x100; + var header = new KIRK_SHA1_HEADER + { + data_size = 0x100 + }; + MemoryMarshal.Write(temp, ref header); + kirk_CMD11(PRNG_DATA, temp, 0x104); + + while (outsize > 0) + { + int blockrem = outsize % 0x14; + int block = outsize / 0x14; + + if (block > 0) + { + // Buffer.BlockCopy(PRNG_DATA, 0, outbuff, outoff, 0x14); + PRNG_DATA.CopyTo(outbuff); + outsize -= 0x14; + kirk_CMD14(outbuff[0x14..], outsize); + } + else if (blockrem > 0) + { + // Buffer.BlockCopy(PRNG_DATA, 0, outbuff, outoff, blockrem); + PRNG_DATA.AsSpan(0, blockrem).CopyTo(outbuff); + outsize -= blockrem; + } + } + + return KIRK_OPERATION_SUCCESS; + } + + static int kirk_CMD16(Span outbuff, int outsize, ReadOnlySpan inbuff, int insize) + { + byte[] dec_private = new byte[0x20]; + KIRK_CMD16_BUFFER signbuf = Utils.AsRef(inbuff); + //ECDSA_SIG sig = BufferToStruct(outbuff); + + if (insize != 0x34) return KIRK_INVALID_SIZE; + if (outsize != 0x28) return KIRK_INVALID_SIZE; + + + decrypt_kirk16_private(dec_private, signbuf.enc_private); + + // Clear out the padding for safety + //Array.Clear(dec_private, 0x14, 0xC); + + var curve = ECDsaHelper.SetCurve(KeyVault.ec_p, KeyVault.ec_a, KeyVault.ec_b2, KeyVault.ec_N2, KeyVault.Gx2, + KeyVault.Gy2); + var ecdsa = ECDsaHelper.Create(curve, dec_private.Take(0x14).ToArray()); + unsafe + { + var hashSig = ecdsa.SignHash(signbuf.message_hash.ToArray()); + // Buffer.BlockCopy(hashSig, 0, outbuff, outoffset, 0x28); + hashSig.CopyTo(outbuff); + } + + return KIRK_OPERATION_SUCCESS; + } + + static int kirk_CMD17(ReadOnlySpan inbuff, int insize) + { + KIRK_CMD17_BUFFER sig = Utils.AsRef(inbuff); + + if (insize != 0x64) return KIRK_INVALID_SIZE; + + var curve = ECDsaHelper.SetCurve(KeyVault.ec_p, KeyVault.ec_a, KeyVault.ec_b2, KeyVault.ec_N2, KeyVault.Gx2, + KeyVault.Gy2); + + unsafe + { + var ecdsa = ECDsaHelper.Create(curve, sig.public_key.x, sig.public_key.y); + + if (ecdsa.VerifyHash(sig.message_hash, sig.signature.sig)) + { + return KIRK_OPERATION_SUCCESS; + + } + } + return KIRK_SIG_CHECK_INVALID; + } + + // SCE functions + public static int sceUtilsBufferCopyWithRange(Span outbuff, int outsize, ReadOnlySpan inbuff, int insize, int cmd) + { + switch (cmd) + { + case KIRK_CMD_DECRYPT_PRIVATE: return kirk_CMD1(outbuff, inbuff, insize); + case KIRK_CMD_ENCRYPT_IV_0: return kirk_CMD4(outbuff, inbuff, insize); + case KIRK_CMD_DECRYPT_IV_0: return kirk_CMD7(outbuff, inbuff, insize); + case KIRK_CMD_PRIV_SIGN_CHECK: return kirk_CMD10(inbuff, insize); + case KIRK_CMD_SHA1_HASH: return kirk_CMD11(outbuff, inbuff, insize); + case KIRK_CMD_ECDSA_GEN_KEYS: return kirk_CMD12(outbuff, outsize); + //case KIRK_CMD_ECDSA_MULTIPLY_POINT: return kirk_CMD13(outbuff, outoffset, outsize, inbuff, insize); + case KIRK_CMD_PRNG: return kirk_CMD14(outbuff, outsize); + case KIRK_CMD_ECDSA_SIGN: return kirk_CMD16(outbuff, outsize, inbuff, insize); + case KIRK_CMD_ECDSA_VERIFY: return kirk_CMD17(inbuff, insize); + } + return -1; + } + } +} diff --git a/PspCrypto/KeyVault.cs b/PspCrypto/KeyVault.cs new file mode 100644 index 0000000..e0d5fcc --- /dev/null +++ b/PspCrypto/KeyVault.cs @@ -0,0 +1,231 @@ +using System; + +namespace PspCrypto +{ + + public static class KeyVault + { + // KIRK AES keys + public static readonly byte[][] kirkKeys = + { + new byte[] {0x2C, 0x92, 0xE5, 0x90, 0x2B, 0x86, 0xC1, 0x06, 0xB7, 0x2E, 0xEA, 0x6C, 0xD4, 0xEC, 0x72, 0x48}, + new byte[] {0x05, 0x8D, 0xC8, 0x0B, 0x33, 0xA5, 0xBF, 0x9D, 0x56, 0x98, 0xFA, 0xE0, 0xD3, 0x71, 0x5E, 0x1F}, + new byte[] {0xB8, 0x13, 0xC3, 0x5E, 0xC6, 0x44, 0x41, 0xE3, 0xDC, 0x3C, 0x16, 0xF5, 0xB4, 0x5E, 0x64, 0x84}, + new byte[] {0x98, 0x02, 0xC4, 0xE6, 0xEC, 0x9E, 0x9E, 0x2F, 0xFC, 0x63, 0x4C, 0xE4, 0x2F, 0xBB, 0x46, 0x68}, + new byte[] {0x99, 0x24, 0x4C, 0xD2, 0x58, 0xF5, 0x1B, 0xCB, 0xB0, 0x61, 0x9C, 0xA7, 0x38, 0x30, 0x07, 0x5F}, + new byte[] {0x02, 0x25, 0xD7, 0xBA, 0x63, 0xEC, 0xB9, 0x4A, 0x9D, 0x23, 0x76, 0x01, 0xB3, 0xF6, 0xAC, 0x17}, + new byte[] {0x60, 0x99, 0xF2, 0x81, 0x70, 0x56, 0x0E, 0x5F, 0x74, 0x7C, 0xB5, 0x20, 0xC0, 0xCD, 0xC2, 0x3C}, + new byte[] {0x76, 0x36, 0x8B, 0x43, 0x8F, 0x77, 0xD8, 0x7E, 0xFE, 0x5F, 0xB6, 0x11, 0x59, 0x39, 0x88, 0x5C}, + new byte[] {0x14, 0xA1, 0x15, 0xEB, 0x43, 0x4A, 0x1B, 0xA4, 0x90, 0x5E, 0x03, 0xB6, 0x17, 0xA1, 0x5C, 0x04}, + new byte[] {0xE6, 0x58, 0x03, 0xD9, 0xA7, 0x1A, 0xA8, 0x7F, 0x05, 0x9D, 0x22, 0x9D, 0xAF, 0x54, 0x53, 0xD0}, + new byte[] {0xBA, 0x34, 0x80, 0xB4, 0x28, 0xA7, 0xCA, 0x5F, 0x21, 0x64, 0x12, 0xF7, 0x0F, 0xBB, 0x73, 0x23}, + new byte[] {0x72, 0xAD, 0x35, 0xAC, 0x9A, 0xC3, 0x13, 0x0A, 0x77, 0x8C, 0xB1, 0x9D, 0x88, 0x55, 0x0B, 0x0C}, + new byte[] {0x84, 0x85, 0xC8, 0x48, 0x75, 0x08, 0x43, 0xBC, 0x9B, 0x9A, 0xEC, 0xA7, 0x9C, 0x7F, 0x60, 0x18}, + new byte[] {0xB5, 0xB1, 0x6E, 0xDE, 0x23, 0xA9, 0x7B, 0x0E, 0xA1, 0x7C, 0xDB, 0xA2, 0xDC, 0xDE, 0xC4, 0x6E}, + new byte[] {0xC8, 0x71, 0xFD, 0xB3, 0xBC, 0xC5, 0xD2, 0xF2, 0xE2, 0xD7, 0x72, 0x9D, 0xDF, 0x82, 0x68, 0x82}, + new byte[] {0x0A, 0xBB, 0x33, 0x6C, 0x96, 0xD4, 0xCD, 0xD8, 0xCB, 0x5F, 0x4B, 0xE0, 0xBA, 0xDB, 0x9E, 0x03}, + new byte[] {0x32, 0x29, 0x5B, 0xD5, 0xEA, 0xF7, 0xA3, 0x42, 0x16, 0xC8, 0x8E, 0x48, 0xFF, 0x50, 0xD3, 0x71}, + new byte[] {0x46, 0xF2, 0x5E, 0x8E, 0x4D, 0x2A, 0xA5, 0x40, 0x73, 0x0B, 0xC4, 0x6E, 0x47, 0xEE, 0x6F, 0x0A}, + new byte[] {0x5D, 0xC7, 0x11, 0x39, 0xD0, 0x19, 0x38, 0xBC, 0x02, 0x7F, 0xDD, 0xDC, 0xB0, 0x83, 0x7D, 0x9D}, + new byte[] {0x51, 0xDD, 0x65, 0xF0, 0x71, 0xA4, 0xE5, 0xEA, 0x6A, 0xAF, 0x12, 0x19, 0x41, 0x29, 0xB8, 0xF4}, + new byte[] {0x03, 0x76, 0x3C, 0x68, 0x65, 0xC6, 0x9B, 0x0F, 0xFE, 0x8F, 0xD8, 0xEE, 0xA4, 0x36, 0x16, 0xA0}, + new byte[] {0x7D, 0x50, 0xB8, 0x5C, 0xAF, 0x67, 0x69, 0xF0, 0xE5, 0x4A, 0xA8, 0x09, 0x8B, 0x0E, 0xBE, 0x1C}, + new byte[] {0x72, 0x68, 0x4B, 0x32, 0xAC, 0x3B, 0x33, 0x2F, 0x2A, 0x7A, 0xFC, 0x9E, 0x14, 0xD5, 0x6F, 0x6B}, + new byte[] {0x20, 0x1D, 0x31, 0x96, 0x4A, 0xD9, 0x9F, 0xBF, 0x32, 0xD5, 0xD6, 0x1C, 0x49, 0x1B, 0xD9, 0xFC}, + new byte[] {0xF8, 0xD8, 0x44, 0x63, 0xD6, 0x10, 0xD1, 0x2A, 0x44, 0x8E, 0x96, 0x90, 0xA6, 0xBB, 0x0B, 0xAD}, + new byte[] {0x5C, 0xD4, 0x05, 0x7F, 0xA1, 0x30, 0x60, 0x44, 0x0A, 0xD9, 0xB6, 0x74, 0x5F, 0x24, 0x4F, 0x4E}, + new byte[] {0xF4, 0x8A, 0xD6, 0x78, 0x59, 0x9C, 0x22, 0xC1, 0xD4, 0x11, 0x93, 0x3D, 0xF8, 0x45, 0xB8, 0x93}, + new byte[] {0xCA, 0xE7, 0xD2, 0x87, 0xA2, 0xEC, 0xC1, 0xCD, 0x94, 0x54, 0x2B, 0x5E, 0x1D, 0x94, 0x88, 0xB2}, + new byte[] {0xDE, 0x26, 0xD3, 0x7A, 0x39, 0x95, 0x6C, 0x2A, 0xD8, 0xC3, 0xA6, 0xAF, 0x21, 0xEB, 0xB3, 0x01}, + new byte[] {0x7C, 0xB6, 0x8B, 0x4D, 0xA3, 0x8D, 0x1D, 0xD9, 0x32, 0x67, 0x9C, 0xA9, 0x9F, 0xFB, 0x28, 0x52}, + new byte[] {0xA0, 0xB5, 0x56, 0xB4, 0x69, 0xAB, 0x36, 0x8F, 0x36, 0xDE, 0xC9, 0x09, 0x2E, 0xCB, 0x41, 0xB1}, + new byte[] {0x93, 0x9D, 0xE1, 0x9B, 0x72, 0x5F, 0xEE, 0xE2, 0x45, 0x2A, 0xBC, 0x17, 0x06, 0xD1, 0x47, 0x69}, + new byte[] {0xA4, 0xA4, 0xE6, 0x21, 0x38, 0x2E, 0xF1, 0xAF, 0x7B, 0x17, 0x7A, 0xE8, 0x42, 0xAD, 0x00, 0x31}, + new byte[] {0xC3, 0x7F, 0x13, 0xE8, 0xCF, 0x84, 0xDB, 0x34, 0x74, 0x7B, 0xC3, 0xA0, 0xF1, 0x9D, 0x3A, 0x73}, + new byte[] {0x2B, 0xF7, 0x83, 0x8A, 0xD8, 0x98, 0xE9, 0x5F, 0xA5, 0xF9, 0x01, 0xDA, 0x61, 0xFE, 0x35, 0xBB}, + new byte[] {0xC7, 0x04, 0x62, 0x1E, 0x71, 0x4A, 0x66, 0xEA, 0x62, 0xE0, 0x4B, 0x20, 0x3D, 0xB8, 0xC2, 0xE5}, + new byte[] {0xC9, 0x33, 0x85, 0x9A, 0xAB, 0x00, 0xCD, 0xCE, 0x4D, 0x8B, 0x8E, 0x9F, 0x3D, 0xE6, 0xC0, 0x0F}, + new byte[] {0x18, 0x42, 0x56, 0x1F, 0x2B, 0x5F, 0x34, 0xE3, 0x51, 0x3E, 0xB7, 0x89, 0x77, 0x43, 0x1A, 0x65}, + new byte[] {0xDC, 0xB0, 0xA0, 0x06, 0x5A, 0x50, 0xA1, 0x4E, 0x59, 0xAC, 0x97, 0x3F, 0x17, 0x58, 0xA3, 0xA3}, + new byte[] {0xC4, 0xDB, 0xAE, 0x83, 0xE2, 0x9C, 0xF2, 0x54, 0xA3, 0xDD, 0x37, 0x4E, 0x80, 0x7B, 0xF4, 0x25}, + new byte[] {0xBF, 0xAE, 0xEB, 0x49, 0x82, 0x65, 0xC5, 0x7C, 0x64, 0xB8, 0xC1, 0x7E, 0x19, 0x06, 0x44, 0x09}, + new byte[] {0x79, 0x7C, 0xEC, 0xC3, 0xB3, 0xEE, 0x0A, 0xC0, 0x3B, 0xD8, 0xE6, 0xC1, 0xE0, 0xA8, 0xB1, 0xA4}, + new byte[] {0x75, 0x34, 0xFE, 0x0B, 0xD6, 0xD0, 0xC2, 0x8D, 0x68, 0xD4, 0xE0, 0x2A, 0xE7, 0xD5, 0xD1, 0x55}, + new byte[] {0xFA, 0xB3, 0x53, 0x26, 0x97, 0x4F, 0x4E, 0xDF, 0xE4, 0xC3, 0xA8, 0x14, 0xC3, 0x2F, 0x0F, 0x88}, + new byte[] {0xEC, 0x97, 0xB3, 0x86, 0xB4, 0x33, 0xC6, 0xBF, 0x4E, 0x53, 0x9D, 0x95, 0xEB, 0xB9, 0x79, 0xE4}, + new byte[] {0xB3, 0x20, 0xA2, 0x04, 0xCF, 0x48, 0x06, 0x29, 0xB5, 0xDD, 0x8E, 0xFC, 0x98, 0xD4, 0x17, 0x7B}, + new byte[] {0x5D, 0xFC, 0x0D, 0x4F, 0x2C, 0x39, 0xDA, 0x68, 0x4A, 0x33, 0x74, 0xED, 0x49, 0x58, 0xA7, 0x3A}, + new byte[] {0xD7, 0x5A, 0x54, 0x22, 0xCE, 0xD9, 0xA3, 0xD6, 0x2B, 0x55, 0x7D, 0x8D, 0xE8, 0xBE, 0xC7, 0xEC}, + new byte[] {0x6B, 0x4A, 0xEE, 0x43, 0x45, 0xAE, 0x70, 0x07, 0xCF, 0x8D, 0xCF, 0x4E, 0x4A, 0xE9, 0x3C, 0xFA}, + new byte[] {0x2B, 0x52, 0x2F, 0x66, 0x4C, 0x2D, 0x11, 0x4C, 0xFE, 0x61, 0x31, 0x8C, 0x56, 0x78, 0x4E, 0xA6}, + new byte[] {0x3A, 0xA3, 0x4E, 0x44, 0xC6, 0x6F, 0xAF, 0x7B, 0xFA, 0xE5, 0x53, 0x27, 0xEF, 0xCF, 0xCC, 0x24}, + new byte[] {0x2B, 0x5C, 0x78, 0xBF, 0xC3, 0x8E, 0x49, 0x9D, 0x41, 0xC3, 0x3C, 0x5C, 0x7B, 0x27, 0x96, 0xCE}, + new byte[] {0xF3, 0x7E, 0xEA, 0xD2, 0xC0, 0xC8, 0x23, 0x1D, 0xA9, 0x9B, 0xFA, 0x49, 0x5D, 0xB7, 0x08, 0x1B}, + new byte[] {0x70, 0x8D, 0x4E, 0x6F, 0xD1, 0xF6, 0x6F, 0x1D, 0x1E, 0x1F, 0xCB, 0x02, 0xF9, 0xB3, 0x99, 0x26}, + new byte[] {0x0F, 0x67, 0x16, 0xE1, 0x80, 0x69, 0x9C, 0x51, 0xFC, 0xC7, 0xAD, 0x6E, 0x4F, 0xB8, 0x46, 0xC9}, + new byte[] {0x56, 0x0A, 0x49, 0x4A, 0x84, 0x4C, 0x8E, 0xD9, 0x82, 0xEE, 0x0B, 0x6D, 0xC5, 0x7D, 0x20, 0x8D}, + new byte[] {0x12, 0x46, 0x8D, 0x7E, 0x1C, 0x42, 0x20, 0x9B, 0xBA, 0x54, 0x26, 0x83, 0x5E, 0xB0, 0x33, 0x03}, + new byte[] {0xC4, 0x3B, 0xB6, 0xD6, 0x53, 0xEE, 0x67, 0x49, 0x3E, 0xA9, 0x5F, 0xBC, 0x0C, 0xED, 0x6F, 0x8A}, + new byte[] {0x2C, 0xC3, 0xCF, 0x8C, 0x28, 0x78, 0xA5, 0xA6, 0x63, 0xE2, 0xAF, 0x2D, 0x71, 0x5E, 0x86, 0xBA}, + new byte[] {0x83, 0x3D, 0xA7, 0x0C, 0xED, 0x6A, 0x20, 0x12, 0xD1, 0x96, 0xE6, 0xFE, 0x5C, 0x4D, 0x37, 0xC5}, + new byte[] {0xC7, 0x43, 0xD0, 0x67, 0x42, 0xEE, 0x90, 0xB8, 0xCA, 0x75, 0x50, 0x35, 0x20, 0xAD, 0xBC, 0xCE}, + new byte[] {0x8A, 0xE3, 0x66, 0x3F, 0x8D, 0x9E, 0x82, 0xA1, 0xED, 0xE6, 0x8C, 0x9C, 0xE8, 0x25, 0x6D, 0xAA}, + new byte[] {0x7F, 0xC9, 0x6F, 0x0B, 0xB1, 0x48, 0x5C, 0xA5, 0x5D, 0xD3, 0x64, 0xB7, 0x7A, 0xF5, 0xE4, 0xEA}, + new byte[] {0x91, 0xB7, 0x65, 0x78, 0x8B, 0xCB, 0x8B, 0xD4, 0x02, 0xED, 0x55, 0x3A, 0x66, 0x62, 0xD0, 0xAD}, + new byte[] {0x28, 0x24, 0xF9, 0x10, 0x1B, 0x8D, 0x0F, 0x7B, 0x6E, 0xB2, 0x63, 0xB5, 0xB5, 0x5B, 0x2E, 0xBB}, + new byte[] {0x30, 0xE2, 0x57, 0x5D, 0xE0, 0xA2, 0x49, 0xCE, 0xE8, 0xCF, 0x2B, 0x5E, 0x4D, 0x9F, 0x52, 0xC7}, + new byte[] {0x5E, 0xE5, 0x04, 0x39, 0x62, 0x32, 0x02, 0xFA, 0x85, 0x39, 0x3F, 0x72, 0xBB, 0x77, 0xFD, 0x1A}, + new byte[] {0xF8, 0x81, 0x74, 0xB1, 0xBD, 0xE9, 0xBF, 0xDD, 0x45, 0xE2, 0xF5, 0x55, 0x89, 0xCF, 0x46, 0xAB}, + new byte[] {0x7D, 0xF4, 0x92, 0x65, 0xE3, 0xFA, 0xD6, 0x78, 0xD6, 0xFE, 0x78, 0xAD, 0xBB, 0x3D, 0xFB, 0x63}, + new byte[] {0x74, 0x7F, 0xD6, 0x2D, 0xC7, 0xA1, 0xCA, 0x96, 0xE2, 0x7A, 0xCE, 0xFF, 0xAA, 0x72, 0x3F, 0xF7}, + new byte[] {0x1E, 0x58, 0xEB, 0xD0, 0x65, 0xBB, 0xF1, 0x68, 0xC5, 0xBD, 0xF7, 0x46, 0xBA, 0x7B, 0xE1, 0x00}, + new byte[] {0x24, 0x34, 0x7D, 0xAF, 0x5E, 0x4B, 0x35, 0x72, 0x7A, 0x52, 0x27, 0x6B, 0xA0, 0x54, 0x74, 0xDB}, + new byte[] {0x09, 0xB1, 0xC7, 0x05, 0xC3, 0x5F, 0x53, 0x66, 0x77, 0xC0, 0xEB, 0x36, 0x77, 0xDF, 0x83, 0x07}, + new byte[] {0xCC, 0xBE, 0x61, 0x5C, 0x05, 0xA2, 0x00, 0x33, 0x37, 0x8E, 0x59, 0x64, 0xA7, 0xDD, 0x70, 0x3D}, + new byte[] {0x0D, 0x47, 0x50, 0xBB, 0xFC, 0xB0, 0x02, 0x81, 0x30, 0xE1, 0x84, 0xDE, 0xA8, 0xD4, 0x84, 0x13}, + new byte[] {0x0C, 0xFD, 0x67, 0x9A, 0xF9, 0xB4, 0x72, 0x4F, 0xD7, 0x8D, 0xD6, 0xE9, 0x96, 0x42, 0x28, 0x8B}, + new byte[] {0x7A, 0xD3, 0x1A, 0x8B, 0x4B, 0xEF, 0xC2, 0xC2, 0xB3, 0x99, 0x01, 0xA9, 0xFE, 0x76, 0xB9, 0x87}, + new byte[] {0xBE, 0x78, 0x78, 0x17, 0xC7, 0xF1, 0x6F, 0x1A, 0xE0, 0xEF, 0x3B, 0xDE, 0x4C, 0xC2, 0xD7, 0x86}, + new byte[] {0x7C, 0xD8, 0xB8, 0x91, 0x91, 0x0A, 0x43, 0x14, 0xD0, 0x53, 0x3D, 0xD8, 0x4C, 0x45, 0xBE, 0x16}, + new byte[] {0x32, 0x72, 0x2C, 0x88, 0x07, 0xCF, 0x35, 0x7D, 0x4A, 0x2F, 0x51, 0x19, 0x44, 0xAE, 0x68, 0xDA}, + new byte[] {0x7E, 0x6B, 0xBF, 0xF6, 0xF6, 0x87, 0xB8, 0x98, 0xEE, 0xB5, 0x1B, 0x32, 0x16, 0xE4, 0x6E, 0x5D}, + new byte[] {0x08, 0xEA, 0x5A, 0x83, 0x49, 0xB5, 0x9D, 0xB5, 0x3E, 0x07, 0x79, 0xB1, 0x9A, 0x59, 0xA3, 0x54}, + new byte[] {0xF3, 0x12, 0x81, 0xBF, 0xE6, 0x9F, 0x51, 0xD1, 0x64, 0x08, 0x25, 0x21, 0xFF, 0xBB, 0x22, 0x61}, + new byte[] {0xAF, 0xFE, 0x8E, 0xB1, 0x3D, 0xD1, 0x7E, 0xD8, 0x0A, 0x61, 0x24, 0x1C, 0x95, 0x92, 0x56, 0xB6}, + new byte[] {0x92, 0xCD, 0xB4, 0xC2, 0x5B, 0xF2, 0x35, 0x5A, 0x23, 0x09, 0xE8, 0x19, 0xC9, 0x14, 0x42, 0x35}, + new byte[] {0xE1, 0xC6, 0x5B, 0x22, 0x6B, 0xE1, 0xDA, 0x02, 0xBA, 0x18, 0xFA, 0x21, 0x34, 0x9E, 0xF9, 0x6D}, + new byte[] {0x14, 0xEC, 0x76, 0xCE, 0x97, 0xF3, 0x8A, 0x0A, 0x34, 0x50, 0x6C, 0x53, 0x9A, 0x5C, 0x9A, 0xB4}, + new byte[] {0x1C, 0x9B, 0xC4, 0x90, 0xE3, 0x06, 0x64, 0x81, 0xFA, 0x59, 0xFD, 0xB6, 0x00, 0xBB, 0x28, 0x70}, + new byte[] {0x43, 0xA5, 0xCA, 0xCC, 0x0D, 0x6C, 0x2D, 0x3F, 0x2B, 0xD9, 0x89, 0x67, 0x6B, 0x3F, 0x7F, 0x57}, + new byte[] {0x00, 0xEF, 0xFD, 0x18, 0x08, 0xA4, 0x05, 0x89, 0x3C, 0x38, 0xFB, 0x25, 0x72, 0x70, 0x61, 0x06}, + new byte[] {0xEE, 0xAF, 0x49, 0xE0, 0x09, 0x87, 0x9B, 0xEF, 0xAA, 0xD6, 0x32, 0x6A, 0x32, 0x13, 0xC4, 0x29}, + new byte[] {0x8D, 0x26, 0xB9, 0x0F, 0x43, 0x1D, 0xBB, 0x08, 0xDB, 0x1D, 0xDA, 0xC5, 0xB5, 0x2C, 0x92, 0xED}, + new byte[] {0x57, 0x7C, 0x30, 0x60, 0xAE, 0x6E, 0xBE, 0xAE, 0x3A, 0xAB, 0x18, 0x19, 0xC5, 0x71, 0x68, 0x0B}, + new byte[] {0x11, 0x5A, 0x5D, 0x20, 0xD5, 0x3A, 0x8D, 0xD3, 0x9C, 0xC5, 0xAF, 0x41, 0x0F, 0x0F, 0x18, 0x6F}, + new byte[] {0x0D, 0x4D, 0x51, 0xAB, 0x23, 0x79, 0xBF, 0x80, 0x3A, 0xBF, 0xB9, 0x0E, 0x75, 0xFC, 0x14, 0xBF}, + new byte[] {0x99, 0x93, 0xDA, 0x3E, 0x7D, 0x2E, 0x5B, 0x15, 0xF2, 0x52, 0xA4, 0xE6, 0x6B, 0xB8, 0x5A, 0x98}, + new byte[] {0xF4, 0x28, 0x30, 0xA5, 0xFB, 0x0D, 0x8D, 0x76, 0x0E, 0xA6, 0x71, 0xC2, 0x2B, 0xDE, 0x66, 0x9D}, + new byte[] {0xFB, 0x5F, 0xEB, 0x7F, 0xC7, 0xDC, 0xDD, 0x69, 0x37, 0x01, 0x97, 0x9B, 0x29, 0x03, 0x5C, 0x47}, + new byte[] {0x02, 0x32, 0x6A, 0xE7, 0xD3, 0x96, 0xCE, 0x7F, 0x1C, 0x41, 0x9D, 0xD6, 0x52, 0x07, 0xED, 0x09}, + new byte[] {0x9C, 0x9B, 0x13, 0x72, 0xF8, 0xC6, 0x40, 0xCF, 0x1C, 0x62, 0xF5, 0xD5, 0x92, 0xDD, 0xB5, 0x82}, + new byte[] {0x03, 0xB3, 0x02, 0xE8, 0x5F, 0xF3, 0x81, 0xB1, 0x3B, 0x8D, 0xAA, 0x2A, 0x90, 0xFF, 0x5E, 0x61}, + new byte[] {0xBC, 0xD7, 0xF9, 0xD3, 0x2F, 0xAC, 0xF8, 0x47, 0xC0, 0xFB, 0x4D, 0x2F, 0x30, 0x9A, 0xBD, 0xA6}, + new byte[] {0xF5, 0x55, 0x96, 0xE9, 0x7F, 0xAF, 0x86, 0x7F, 0xAC, 0xB3, 0x3A, 0xE6, 0x9C, 0x8B, 0x6F, 0x93}, + new byte[] {0xEE, 0x29, 0x70, 0x93, 0xF9, 0x4E, 0x44, 0x59, 0x44, 0x17, 0x1F, 0x8E, 0x86, 0xE1, 0x70, 0xFC}, + new byte[] {0xE4, 0x34, 0x52, 0x0C, 0xF0, 0x88, 0xCF, 0xC8, 0xCD, 0x78, 0x1B, 0x6C, 0xCF, 0x8C, 0x48, 0xC4}, + new byte[] {0xC1, 0xBF, 0x66, 0x81, 0x8E, 0xF9, 0x53, 0xF2, 0xE1, 0x26, 0x6B, 0x6F, 0x55, 0x0C, 0xC9, 0xCD}, + new byte[] {0x56, 0x0F, 0xFF, 0x8F, 0x3C, 0x96, 0x49, 0x14, 0x45, 0x16, 0xF1, 0xBC, 0xBF, 0xCE, 0xA3, 0x0C}, + new byte[] {0x24, 0x08, 0xDC, 0x75, 0x37, 0x60, 0xA2, 0x9F, 0x05, 0x54, 0xB5, 0xF2, 0x43, 0x85, 0x73, 0x99}, + new byte[] {0xDD, 0xD5, 0xB5, 0x6A, 0x59, 0xC5, 0x5A, 0xE8, 0x3B, 0x96, 0x67, 0xC7, 0x5C, 0x2A, 0xE2, 0xDC}, + new byte[] {0xAA, 0x68, 0x67, 0x72, 0xE0, 0x2D, 0x44, 0xD5, 0xCD, 0xBB, 0x65, 0x04, 0xBC, 0xD5, 0xBF, 0x4E}, + new byte[] {0x1F, 0x17, 0xF0, 0x14, 0xE7, 0x77, 0xA2, 0xFE, 0x4B, 0x13, 0x6B, 0x56, 0xCD, 0x7E, 0xF7, 0xE9}, + new byte[] {0xC9, 0x35, 0x48, 0xCF, 0x55, 0x8D, 0x75, 0x03, 0x89, 0x6B, 0x2E, 0xEB, 0x61, 0x8C, 0xA9, 0x02}, + new byte[] {0xDE, 0x34, 0xC5, 0x41, 0xE7, 0xCA, 0x86, 0xE8, 0xBE, 0xA7, 0xC3, 0x1C, 0xEC, 0xE4, 0x36, 0x0F}, + new byte[] {0xDD, 0xE5, 0xFF, 0x55, 0x1B, 0x74, 0xF6, 0xF4, 0xE0, 0x16, 0xD7, 0xAB, 0x22, 0x31, 0x1B, 0x6A}, + new byte[] {0xB0, 0xE9, 0x35, 0x21, 0x33, 0x3F, 0xD7, 0xBA, 0xB4, 0x76, 0x2C, 0xCB, 0x4D, 0x80, 0x08, 0xD8}, + new byte[] {0x38, 0x14, 0x69, 0xC4, 0xC3, 0xF9, 0x1B, 0x96, 0x33, 0x63, 0x8E, 0x4D, 0x5F, 0x3D, 0xF0, 0x29}, + new byte[] {0xFA, 0x48, 0x6A, 0xD9, 0x8E, 0x67, 0x16, 0xEF, 0x6A, 0xB0, 0x87, 0xF5, 0x89, 0x45, 0x7F, 0x2A}, + new byte[] {0x32, 0x1A, 0x09, 0x12, 0x50, 0x14, 0x8A, 0x3E, 0x96, 0x3D, 0xEA, 0x02, 0x59, 0x32, 0xE1, 0x8F}, + new byte[] {0x4B, 0x00, 0xBE, 0x29, 0xBC, 0xB0, 0x28, 0x64, 0xCE, 0xFD, 0x43, 0xA9, 0x6F, 0xD9, 0x5C, 0xED}, + new byte[] {0x57, 0x7D, 0xC4, 0xFF, 0x02, 0x44, 0xE2, 0x80, 0x91, 0xF4, 0xCA, 0x0A, 0x75, 0x69, 0xFD, 0xA8}, + new byte[] {0x83, 0x53, 0x36, 0xC6, 0x18, 0x03, 0xE4, 0x3E, 0x4E, 0xB3, 0x0F, 0x6B, 0x6E, 0x79, 0x9B, 0x7A}, + new byte[] {0x5C, 0x92, 0x65, 0xFD, 0x7B, 0x59, 0x6A, 0xA3, 0x7A, 0x2F, 0x50, 0x9D, 0x85, 0xE9, 0x27, 0xF8}, + new byte[] {0x9A, 0x39, 0xFB, 0x89, 0xDF, 0x55, 0xB2, 0x60, 0x14, 0x24, 0xCE, 0xA6, 0xD9, 0x65, 0x0A, 0x9D}, + new byte[] {0x8B, 0x75, 0xBE, 0x91, 0xA8, 0xC7, 0x5A, 0xD2, 0xD7, 0xA5, 0x94, 0xA0, 0x1C, 0xBB, 0x95, 0x91}, + new byte[] {0x95, 0xC2, 0x1B, 0x8D, 0x05, 0xAC, 0xF5, 0xEC, 0x5A, 0xEE, 0x77, 0x81, 0x23, 0x95, 0xC4, 0xD7}, + new byte[] {0xB9, 0xA4, 0x61, 0x64, 0x36, 0x33, 0xFA, 0x5D, 0x94, 0x88, 0xE2, 0xD3, 0x28, 0x1E, 0x01, 0xA2}, + new byte[] {0xB8, 0xB0, 0x84, 0xFB, 0x9F, 0x4C, 0xFA, 0xF7, 0x30, 0xFE, 0x73, 0x25, 0xA2, 0xAB, 0x89, 0x7D}, + new byte[] {0x5F, 0x8C, 0x17, 0x9F, 0xC1, 0xB2, 0x1D, 0xF1, 0xF6, 0x36, 0x7A, 0x9C, 0xF7, 0xD3, 0xD4, 0x7C}, + }; + + public static readonly byte[] kirk1_key = { 0x98, 0xC9, 0x40, 0x97, 0x5C, 0x1D, 0x10, 0xE8, 0x7F, 0xE6, 0x0E, 0xA3, 0xFD, 0x03, 0xA8, 0xBA }; + public static readonly byte[] kirk16_key = { 0x47, 0x5E, 0x09, 0xF4, 0xA2, 0x37, 0xDA, 0x9B, 0xEF, 0xFF, 0x3B, 0xC0, 0x77, 0x14, 0x3D, 0x8A }; + + /* ECC Curves for Kirk 1 and Kirk 0x11 */ + // Common Curve paramters p and a + public static readonly byte[] ec_p = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + public static readonly byte[] ec_a = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC }; // mon + + // Kirk 0xC,0xD,0x10,0x11,(likely 0x12)- Unique curve parameters for b, N, and base point G for Kirk 0xC,0xD,0x10,0x11,(likely 0x12) service + // Since public key is variable, it is not specified here + public static readonly byte[] ec_b2 = { 0xA6, 0x8B, 0xED, 0xC3, 0x34, 0x18, 0x02, 0x9C, 0x1D, 0x3C, 0xE3, 0x3B, 0x9A, 0x32, 0x1F, 0xCC, 0xBB, 0x9E, 0x0F, 0x0B };// mon + public static readonly byte[] ec_N2 = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xB5, 0xAE, 0x3C, 0x52, 0x3E, 0x63, 0x94, 0x4F, 0x21, 0x27 }; + public static readonly byte[] Gx2 = { 0x12, 0x8E, 0xC4, 0x25, 0x64, 0x87, 0xFD, 0x8F, 0xDF, 0x64, 0xE2, 0x43, 0x7B, 0xC0, 0xA1, 0xF6, 0xD5, 0xAF, 0xDE, 0x2C }; + public static readonly byte[] Gy2 = { 0x59, 0x58, 0x55, 0x7E, 0xB1, 0xDB, 0x00, 0x12, 0x60, 0x42, 0x55, 0x24, 0xDB, 0xC3, 0x79, 0xD5, 0xAC, 0x5F, 0x4A, 0xDF }; + + // KIRK 1 - Unique curve parameters for b, N, and base point G + // Since public key is hard coded, it is also included + public static readonly byte[] ec_b1 = { 0x65, 0xD1, 0x48, 0x8C, 0x03, 0x59, 0xE2, 0x34, 0xAD, 0xC9, 0x5B, 0xD3, 0x90, 0x80, 0x14, 0xBD, 0x91, 0xA5, 0x25, 0xF9 }; + public static readonly byte[] ec_N1 = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x01, 0xB5, 0xC6, 0x17, 0xF2, 0x90, 0xEA, 0xE1, 0xDB, 0xAD, 0x8F }; + public static readonly byte[] Gx1 = { 0x22, 0x59, 0xAC, 0xEE, 0x15, 0x48, 0x9C, 0xB0, 0x96, 0xA8, 0x82, 0xF0, 0xAE, 0x1C, 0xF9, 0xFD, 0x8E, 0xE5, 0xF8, 0xFA }; + public static readonly byte[] Gy1 = { 0x60, 0x43, 0x58, 0x45, 0x6D, 0x0A, 0x1C, 0xB2, 0x90, 0x8D, 0xE9, 0x0F, 0x27, 0xD7, 0x5C, 0x82, 0xBE, 0xC1, 0x08, 0xC0 }; + + public static readonly byte[] Px1 = { 0xED, 0x9C, 0xE5, 0x82, 0x34, 0xE6, 0x1A, 0x53, 0xC6, 0x85, 0xD6, 0x4D, 0x51, 0xD0, 0x23, 0x6B, 0xC3, 0xB5, 0xD4, 0xB9 }; + public static readonly byte[] Py1 = { 0x04, 0x9D, 0xF1, 0xA0, 0x75, 0xC0, 0xE0, 0x4F, 0xB3, 0x44, 0x85, 0x8B, 0x61, 0xB7, 0x9B, 0x69, 0xA6, 0x3D, 0x2C, 0x39 }; + public static readonly byte[] ec_Priv1 = { 0xF3, 0x92, 0xE2, 0x64, 0x90, 0xB8, 0x0F, 0xD8, 0x89, 0xF2, 0xD9, 0x72, 0x2C, 0x1F, 0x34, 0xD7, 0x27, 0x4F, 0x98, 0x3D }; + + public static readonly byte[] Px2 = { 0x01, 0x21, 0xEA, 0x6E, 0xCD, 0xB2, 0x3A, 0x3E, 0x23, 0x75, 0x67, 0x1C, 0x53, 0x62, 0xE8, 0xE2, 0x8B, 0x1E, 0x78, 0x3B }; + public static readonly byte[] Py2 = { 0x1A, 0x27, 0x32, 0x15, 0x8B, 0x8C, 0xED, 0x98, 0x46, 0x6C, 0x18, 0xA3, 0xAC, 0x3B, 0x11, 0x06, 0xAF, 0xB4, 0xEC, 0x3B }; + public static readonly byte[] ec_Priv2 = { 0x14, 0xB0, 0x22, 0xE8, 0x92, 0xCF, 0x86, 0x14, 0xA4, 0x45, 0x57, 0xDB, 0x09, 0x5C, 0x92, 0x8D, 0xE9, 0xB8, 0x99, 0x70 }; + + public static readonly byte[] EdatPx = { 0x1F, 0x07, 0x2B, 0xCC, 0xC1, 0x62, 0xF2, 0xCF, 0xAE, 0xA0, 0xE7, 0xF4, 0xCD, 0xFD, 0x9C, 0xAE, 0xC6, 0xC4, 0x55, 0x21 }; + public static readonly byte[] EdatPy = { 0x53, 0x01, 0xF4, 0xE3, 0x70, 0xC3, 0xED, 0xE2, 0xD4, 0xF5, 0xDB, 0xC3, 0xA7, 0xDE, 0x8C, 0xAA, 0xE8, 0xAD, 0x5B, 0x7D }; + public static readonly byte[] EdatPirv = { 0xe5, 0xc4, 0xd0, 0xa8, 0x24, 0x9a, 0x6f, 0x27, 0xe5, 0xe0, 0xc9, 0xd5, 0x34, 0xf4, 0xda, 0x15, 0x22, 0x3f, 0x42, 0xad }; + + public static readonly byte[] Eboot_p = { 0xA5, 0x3E, 0x11, 0x3E, 0x46, 0xD8, 0xC9, 0xC1, 0xF0, 0x9D, 0x9B, 0xCB, 0x2A, 0x53, 0x73, 0xD3, 0x79, 0xF6, 0x9D, 0xA2, 0x8D, 0x09, 0x99, 0x9F, 0xED, 0x57, 0xA9, 0x0F }; + public static readonly byte[] Eboot_a = { 0xA5, 0x3E, 0x11, 0x3E, 0x46, 0xD8, 0xC9, 0xC1, 0xF0, 0x9D, 0x9B, 0xCB, 0x2A, 0x53, 0x73, 0xD3, 0x79, 0xF6, 0x9D, 0xA2, 0x8D, 0x09, 0x99, 0x9F, 0xED, 0x57, 0xA9, 0x0C }; + public static readonly byte[] Eboot_b = { 0x90, 0x65, 0x94, 0x1D, 0x29, 0x37, 0x4A, 0x8F, 0x11, 0xDD, 0x1E, 0x54, 0x01, 0x89, 0x43, 0x4E, 0x4A, 0x6E, 0xBF, 0xAF, 0x54, 0x77, 0xF6, 0xC1, 0x72, 0xF6, 0x85, 0x5E }; + public static readonly byte[] Eboot_N = { 0xA5, 0x3E, 0x11, 0x3E, 0x46, 0xD8, 0xC9, 0xC1, 0xF0, 0x9D, 0x9B, 0xCB, 0x2A, 0x52, 0x26, 0x98, 0xDE, 0xEF, 0x58, 0xDB, 0x1A, 0xD9, 0xAB, 0x7F, 0x04, 0xE3, 0xAE, 0x7F }; + public static readonly byte[] Eboot_Gx = { 0x7E, 0x06, 0x09, 0x82, 0x47, 0xE6, 0xB5, 0x9F, 0x31, 0x10, 0xBC, 0xBB, 0x3A, 0xB6, 0xC2, 0x50, 0xBC, 0x5A, 0xB0, 0x6C, 0x03, 0x2D, 0xAD, 0x43, 0x68, 0x4C, 0x24, 0x8F }; + public static readonly byte[] Eboot_Gy = { 0x0B, 0xD9, 0x41, 0x8D, 0xE8, 0xE3, 0xE4, 0x5D, 0x2D, 0x70, 0x1E, 0x02, 0x37, 0xFD, 0x7F, 0x2A, 0xDE, 0x0D, 0x48, 0xB7, 0x4C, 0xEE, 0xF2, 0xF1, 0xC8, 0xAC, 0x48, 0x4E }; + public static readonly byte[] Eboot_pub1x = { 0x5F, 0x9D, 0x17, 0x1A, 0x2B, 0xDD, 0xA8, 0xD4, 0x08, 0x78, 0xBF, 0x98, 0x5A, 0xC3, 0x26, 0xED, 0x5E, 0xFF, 0x43, 0xC9, 0x37, 0x6C, 0x77, 0xEC, 0x0A, 0x00, 0xC7, 0xBB }; + public static readonly byte[] Eboot_pub1y = { 0xA3, 0x44, 0xE4, 0x4E, 0x6E, 0xAC, 0x25, 0x52, 0x35, 0xF9, 0x54, 0xF5, 0xB6, 0x17, 0xC7, 0xBD, 0x49, 0xF1, 0x80, 0x26, 0x24, 0x54, 0xAA, 0xE1, 0xB6, 0x2A, 0x9F, 0x2C }; + public static readonly byte[] Eboot_priv1 = { 0x76, 0x74, 0x36, 0xA6, 0x99, 0x9D, 0x88, 0x48, 0x0E, 0xC8, 0x56, 0xF5, 0x5C, 0xEA, 0xBB, 0x43, 0x96, 0x85, 0x9E, 0x37, 0x45, 0x99, 0x40, 0x39, 0x21, 0xF5, 0x55, 0x98 }; + public static readonly byte[] Eboot_pub2x = { 0x67, 0x00, 0x2D, 0x9B, 0xB8, 0xE4, 0x2D, 0x2B, 0xF9, 0x61, 0x0B, 0x27, 0xFE, 0xAB, 0x9B, 0x34, 0x56, 0x15, 0x50, 0x92, 0x13, 0x12, 0xDF, 0xEE, 0x7A, 0x3A, 0x86, 0xEC }; + public static readonly byte[] Eboot_pub2y = { 0x6C, 0xA7, 0x14, 0x42, 0x6F, 0x6D, 0x4E, 0x96, 0x09, 0xA6, 0x38, 0xBF, 0x4A, 0xFB, 0x18, 0x2B, 0xFA, 0x50, 0xC8, 0x2F, 0xF2, 0xB4, 0xC5, 0xEC, 0x6C, 0xCD, 0x97, 0x65 }; + public static readonly byte[] Eboot_priv2 = { 0x60, 0x7A, 0x2E, 0x55, 0x68, 0xB4, 0xB9, 0xA0, 0x32, 0xF4, 0x52, 0x53, 0xCF, 0xED, 0x20, 0xDB, 0x2E, 0x6E, 0x44, 0x6C, 0x37, 0x82, 0xE8, 0x2A, 0x1A, 0xB9, 0xC9, 0x23 }; + public static readonly byte[][] Eboot_priv = { Eboot_priv1, Eboot_priv2 }; + public static readonly byte[][] Eboot_pubx = { Eboot_pub1x, Eboot_pub2x }; + public static readonly byte[][] Eboot_puby = { Eboot_pub1y, Eboot_pub2y }; + + public static readonly byte[] Eboot_hmacKey = { 0x54, 0x88, 0xA9, 0x81, 0x1C, 0x9A, 0x2C, 0xBC, 0xCC, 0x59, 0x6B, 0x1F, 0xAD, 0x1A, 0x7E, 0x29, 0xE0, 0x75, 0x84, 0x0F, 0x47, 0x43, 0x1F, 0x37, 0xAC, 0x06, 0x02, 0x46, 0x4A, 0x27, 0x9E, 0x02, 0xDF, 0x2E, 0x71, 0x65, 0xF1, 0x13, 0x7B, 0xF6, 0x9A, 0xE6, 0xDC, 0xB9, 0xDC, 0x38, 0x8C, 0x9D, 0xCC, 0xB3, 0x64, 0xC4, 0xCA, 0x26, 0xCB, 0x8F, 0x1A, 0xF0, 0x63, 0x8A, 0x6E, 0xAD, 0xB5, 0x4D }; + + + public static readonly byte[] VitaKirk18PubKey0x = { 0x5F, 0x9D, 0x17, 0x1A, 0x2B, 0xDD, 0xA8, 0xD4, 0x08, 0x78, 0xBF, 0x98, 0x5A, 0xC3, 0x26, 0xED, 0x5E, 0xFF, 0x43, 0xC9, 0x37, 0x6C, 0x77, 0xEC, 0x0A, 0x00, 0xC7, 0xBB }; + public static readonly byte[] VitaKirk18PubKey0y = { 0xA3, 0x44, 0xE4, 0x4E, 0x6E, 0xAC, 0x25, 0x52, 0x35, 0xF9, 0x54, 0xF5, 0xB6, 0x17, 0xC7, 0xBD, 0x49, 0xF1, 0x80, 0x26, 0x24, 0x54, 0xAA, 0xE1, 0xB6, 0x2A, 0x9F, 0x2C }; + + public static readonly byte[] VitaKirk18PubKey1x = { 0x67, 0x00, 0x2D, 0x9B, 0xB8, 0xE4, 0x2D, 0x2B, 0xF9, 0x61, 0x0B, 0x27, 0xFE, 0xAB, 0x9B, 0x34, 0x56, 0x15, 0x50, 0x92, 0x13, 0x12, 0xDF, 0xEE, 0x7A, 0x3A, 0x86, 0xEC }; + public static readonly byte[] VitaKirk18PubKey1y = { 0x6C, 0xA7, 0x14, 0x42, 0x6F, 0x6D, 0x4E, 0x96, 0x09, 0xA6, 0x38, 0xBF, 0x4A, 0xFB, 0x18, 0x2B, 0xFA, 0x50, 0xC8, 0x2F, 0xF2, 0xB4, 0xC5, 0xEC, 0x6C, 0xCD, 0x97, 0x65 }; + + public static readonly byte[] VitaKirk18PubKey1000x = { 0x64, 0xDD, 0xD3, 0x1E, 0x46, 0x7A, 0x90, 0x8F, 0x99, 0x0D, 0x63, 0xF4, 0x59, 0x32, 0x3C, 0x1C, 0xA3, 0xCE, 0xCF, 0x00, 0xA8, 0x1C, 0x57, 0x40, 0xA9, 0x6D, 0x2E, 0x1C }; + public static readonly byte[] VitaKirk18PubKey1000y = { 0x38, 0x61, 0xA1, 0x9D, 0x0A, 0xBD, 0xC5, 0xED, 0xDB, 0xD6, 0x04, 0xFA, 0x67, 0xC6, 0xDA, 0xB4, 0x28, 0x3C, 0x29, 0x67, 0x86, 0xD9, 0xA8, 0x07, 0xE0, 0xC1, 0x3B, 0xA8 }; + + public static readonly byte[] drmActRifSig = + { + 0x62, 0x27, 0xB0, 0x0A, 0x02, 0x85, 0x6F, 0xB0, + 0x41, 0x08, 0x87, 0x67, 0x19, 0xE0, 0xA0, 0x18, + 0x32, 0x91, 0xEE, 0xB9, 0x6E, 0x73, 0x6A, 0xBF, + 0x81, 0xF7, 0x0E, 0xE9, 0x16, 0x1B, 0x0D, 0xDE, + 0xB0, 0x26, 0x76, 0x1A, 0xFF, 0x7B, 0xC8, 0x5B + }; + + public static readonly byte[] drmRifKey = { 0xDA, 0x7D, 0x4B, 0x5E, 0x49, 0x9A, 0x4F, 0x53, 0xB1, 0xC1, 0xA1, 0x4A, 0x74, 0x84, 0x44, 0x3B }; + + public static readonly byte[] drmActdatKey = { 0x5E, 0x06, 0xE0, 0x4F, 0xD9, 0x4A, 0x71, 0xBF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }; + + public static readonly byte[] drmVersionKeyKey = + { + 0xF0, 0x79, 0xD5, 0x19, 0x8F, 0x23, 0xEF, 0xCE, + 0xB5, 0x4B, 0x9E, 0xCD, 0xCD, 0xFD, 0xD3, 0xD7, + 0x07, 0x3D, 0x9E, 0x9D, 0xA8, 0xFD, 0x3B, 0x2F, + 0x63, 0x18, 0x93, 0x2E, 0xF8, 0x57, 0xA6, 0x64, + 0x37, 0x49, 0xB7, 0x01, 0xCA, 0xE2, 0xE0, 0xC5, + 0x44, 0x2E, 0x06, 0xB6, 0x1E, 0xFF, 0x84, 0xF2, + 0x9D, 0x31, 0xB8, 0x5A, 0xC8, 0xFA, 0x16, 0x80, + 0x73, 0x60, 0x18, 0x82, 0x18, 0x77, 0x91, 0x9D, + + }; + + public static readonly byte[] DrmFixedKey = { 0x38, 0x20, 0xD0, 0x11, 0x07, 0xA3, 0xFF, 0x3E, 0x0A, 0x4C, 0x20, 0x85, 0x39, 0x10, 0xB5, 0x54 }; + } +} diff --git a/PspCrypto/Libraries.cs b/PspCrypto/Libraries.cs new file mode 100644 index 0000000..ea48f77 --- /dev/null +++ b/PspCrypto/Libraries.cs @@ -0,0 +1,8 @@ +using System; +using System.Collections.Generic; +using System.Text; + +internal static class Libraries +{ + internal const string CryptoNative = "PspCryptoHelper"; +} diff --git a/PspCrypto/Lz.cs b/PspCrypto/Lz.cs new file mode 100644 index 0000000..f6d8144 --- /dev/null +++ b/PspCrypto/Lz.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Text; +using SevenZip.Compression.LZMA; +using Decoder = SevenZip.Compression.LZMA.Decoder; + +namespace PspCrypto +{ + public class Lz + { + public static byte[] compress(byte[] in_buf) + { + //Decoder decoder = new Decoder(); + //using var inStream = new MemoryStream(@in); + //using var outStream = new MemoryStream(@out); + //byte[] properties = new byte[5]; + //inStream.Read(properties, 0, 5); + //decoder.SetDecoderProperties(properties); + //decoder.Code(inStream, outStream, insize, size, null); + //return 0; + var lzrc = new Lzrc(); + + // create a buffer hopefully big enough to hold compression result + byte[] compression_result = new byte[in_buf.Length + 0xffff]; // in worst case (i.e random data), + // compression makes the data larger + // so have to make sure theres extra space .. + + // compress data, and get the compressed data length + int compressed_length = lzrc.lzrc_compress(compression_result, compression_result.Length, in_buf, in_buf.Length); + + // resize array to actual compressed length ... + Array.Resize(ref compression_result, compressed_length); + + return compression_result; + } + public static int decompress(byte[] @out, byte[] @in, int size, int insize) + { + //Decoder decoder = new Decoder(); + //using var inStream = new MemoryStream(@in); + //using var outStream = new MemoryStream(@out); + //byte[] properties = new byte[5]; + //inStream.Read(properties, 0, 5); + //decoder.SetDecoderProperties(properties); + //decoder.Code(inStream, outStream, insize, size, null); + //return 0; + var lzrc = new Lzrc(); + lzrc.lzrc_decompress(@out, size, @in, insize); + return 0; + } + } +} diff --git a/PspCrypto/Lzrc.cs b/PspCrypto/Lzrc.cs new file mode 100644 index 0000000..31febb8 --- /dev/null +++ b/PspCrypto/Lzrc.cs @@ -0,0 +1,836 @@ +using System; +using System.Collections.Generic; +using System.Net.NetworkInformation; +using System.Text; + +namespace PspCrypto +{ + public class Lzrc + { + private byte[] input; + private int in_ptr; + private int in_len; + + private byte[] output; + private int out_ptr; + private int out_len; + + private uint range; + private uint code; + private uint out_code; + private byte lc; + private byte[][] bm_literal = new byte[8][]; + private byte[][] bm_dist_bits = new byte[8][]; + private byte[][] bm_dist = new byte[16][]; + private byte[][] bm_match = new byte[8][]; + private byte[][] bm_len = new byte[8][]; + + const int max_tbl_sz = 65280; + const int tbl_sz = 65536; + + static byte[] text_buf = new byte[tbl_sz]; + static int t_start, t_end, t_fill, sp_fill; + static int t_len, t_pos; + + static int[] prev = new int[tbl_sz], next = new int[tbl_sz]; + static int[] root = new int[tbl_sz]; + + static void Init(byte[][] arr, byte value, int length) + { + for (int i = 0; i < arr.Length; i++) + { + arr[i] = new byte[length]; + for (int j = 0; j < length; j++) + { + arr[i][j] = value; + } + } + } + + private byte rc_getbyte() + { + if (in_ptr == in_len) + { + throw new Exception("End of input!"); + } + + return input[in_ptr++]; + } + + void rc_putbyte(byte b) + { + if (out_ptr == out_len) + { + throw new Exception("Output overflow!"); + } + + output[out_ptr++] = b; + } + + void re_init(ref byte[] out_buf, int out_len, ref byte[] in_buf, int in_len) + { + input = in_buf; + this.in_len = in_len; + in_ptr = 0; + + output = out_buf; + this.out_len = out_len; + out_ptr = 0; + + range = 0xffffffff; + code = 0x00000000; + lc = 5; + out_code = 0xffffffff; + + re_putbyte(lc); + + +#if NP9660 + Init(bm_literal, 0x80, 2048); + Init(bm_dist_bits, 0x80, 312); + Init(bm_dist, 0x80, 144); + Init(bm_match, 0x80, 64); + Init(bm_len, 0x80, 248); +#else + Init(bm_literal, 0x80, 256); // 2048 2680 2656 + Init(bm_dist_bits, 0x80, 23); // 184 + Init(bm_dist, 0x80, 8); // 128 + Init(bm_match, 0x80, 8); // 64 + Init(bm_len, 0x80, 32); // 256 +#endif + //memset(re->bm_literal, 0x80, 2048); + //memset(re->bm_dist_bits, 0x80, 312); + //memset(re->bm_dist, 0x80, 144); + //memset(re->bm_match, 0x80, 64); + //memset(re->bm_len, 0x80, 248); + } + + void rc_init(byte[] out_buf, int out_len, byte[] in_buf, int in_len) + { + input = in_buf; + this.in_len = in_len; + in_ptr = 0; + + output = out_buf; + this.out_len = out_len; + out_ptr = 0; + + range = 0xffffffff; + lc = rc_getbyte(); + code = (uint)((rc_getbyte() << 24) | + (rc_getbyte() << 16) | + (rc_getbyte() << 8) | + rc_getbyte()); + out_code = 0xffffffff; + + Init(bm_literal, 0x80, 256); // 2048 2680 2656 + Init(bm_dist_bits, 0x80, 23); // 184 + Init(bm_dist, 0x80, 8); // 128 + Init(bm_match, 0x80, 8); // 64 + Init(bm_len, 0x80, 32); // 256 + } + + void normalize() + { + if (range < 0x01000000) + { + range <<= 8; + code = (code << 8) + input[in_ptr]; + in_ptr++; + } + } + + int rc_bit(byte[] probs, int index) + { + uint bound; + + normalize(); + + bound = (range >> 8) * probs[index]; + probs[index] -= (byte)(probs[index] >> 3); + + if (code < bound) + { + range = bound; + probs[index] += 31; + return 1; + } + else + { + code -= bound; + range -= bound; + return 0; + } + } + + int rc_bittree(byte[] probs, int index, int limit) + { + int number = 1; + do + { + number = (number << 1) + rc_bit(probs, index + number); + } while (number < limit); + + number -= limit; + + return number; + } + + int rc_number(byte[] prob, int index, int n) + { + int i, number = 1; + + if (n > 3) + { + number = (number << 1) + rc_bit(prob, index + 3); + if (n > 4) + { + number = (number << 1) + rc_bit(prob, index + 3); + } + + if (n > 5) + { + normalize(); + for (i = 0; i < n - 5; i++) + { + range >>= 1; + number <<= 1; + if (code < range) + { + number += 1; + } + else + { + code -= range; + } + } + } + } + + if (n > 0) + { + number = (number << 1) + rc_bit(prob, index + 0); + if (n > 1) + { + number = (number << 1) + rc_bit(prob, index + 1); + if (n > 2) + { + number = (number << 1) + rc_bit(prob, index + 2); + } + } + } + + return number; + } + + public void init_tree() + { + int i; + + for (i = 0; i < tbl_sz; i++) + { + root[i] = -1; + prev[i] = -1; + next[i] = -1; + } + + t_start = 0; + t_end = 0; + t_fill = 0; + sp_fill = 0; + } + + void fill_buffer() + { + //void *memcpy(void *dest, const void * src, size_t n) + + int content_size, back_size, front_size; + + if (sp_fill == in_len) + return; + + content_size = (t_fill < t_end) ? (max_tbl_sz + t_fill - t_end) : (t_fill - t_end); + if (content_size >= 509) + return; + + if (t_fill < t_start) + { + back_size = t_start - t_fill - 1; + if (sp_fill + back_size > in_len) + back_size = in_len - sp_fill; + + Array.ConstrainedCopy(input, sp_fill, text_buf, t_fill, back_size); + // memcpy(text_buf + t_fill, re->input + sp_fill, back_size); + + sp_fill += back_size; + t_fill += back_size; + } + else + { + back_size = max_tbl_sz - t_fill; + if (t_start == 0) + back_size -= 1; + if (sp_fill + back_size > in_len) + back_size = in_len - sp_fill; + + Array.ConstrainedCopy(input, sp_fill, text_buf, t_fill, back_size); + //memcpy(text_buf + t_fill, re->input + sp_fill, back_size); + + sp_fill += back_size; + t_fill += back_size; + + front_size = t_start; + if (t_start != 0) + front_size -= 1; + if (sp_fill + front_size > in_len) + front_size = in_len - sp_fill; + + Array.ConstrainedCopy(input, sp_fill, text_buf, 0, front_size); + //memcpy(text_buf, re->input + sp_fill, front_size); + + sp_fill += front_size; + + Array.ConstrainedCopy(text_buf, 255, text_buf, max_tbl_sz, front_size); + //memcpy(text_buf + max_tbl_sz, text_buf, 255); + + t_fill += front_size; + if (t_fill >= max_tbl_sz) + t_fill -= max_tbl_sz; + } + } + void remove_node(int p) + { + int t, q; + + if (prev[p] == -1) + return; + + t = text_buf[p + 0]; + t = (t << 8) | text_buf[p + 1]; + + q = next[p]; + if (q != -1) + prev[q] = prev[p]; + + if (prev[p] == -2) + root[t] = q; + else + next[prev[p]] = q; + + prev[p] = -1; + next[p] = -1; + } + + int insert_node(int pos, out int match_len, out int match_dist, int do_cmp) + { + //Span src, win; + int i, t, p; + int content_size; + + //src = text_buf[pos..]; + //win = text_buf[t_start..]; + content_size = (t_fill < pos) ? (max_tbl_sz + t_fill - pos) : (t_fill - pos); + t_len = 1; + t_pos = 0; + match_len = t_len; + match_dist = t_pos; + + if (in_ptr == in_len) + { + match_len = 256; + return 0; + } + + if (in_ptr == (in_len - 1)) + return 0; + + t = text_buf[pos+0]; + t = (t << 8) | text_buf[pos+1]; + if (root[t] == -1) + { + root[t] = pos; + prev[pos] = -2; + next[pos] = -1; + return 0; + } + + p = root[t]; + root[t] = pos; + prev[pos] = -2; + next[pos] = p; + + if (p != -1) + prev[p] = pos; + + while (do_cmp == 1 && p != -1) + { + for (i = 0; (i < 255 && i < content_size); i++) + { + if (text_buf[pos+i] != text_buf[p + i]) + break; + } + + if (i > t_len) + { + t_len = i; + t_pos = pos - p; + } + else if (i == t_len) + { + int mp = pos - p; + if (mp < 0) + mp += max_tbl_sz; + if (mp < t_pos) + { + t_len = i; + t_pos = pos - p; + } + } + if (i == 255) + { + remove_node(p); + break; + } + + p = next[p]; + } + + match_len = t_len; + match_dist = t_pos; + + return 1; + } + void update_tree(int length) + { + int i, win_size; + int tmp_len, tmp_pos; + + win_size = (t_end >= t_start) ? (t_end - t_start) : (max_tbl_sz + t_end - t_start); + + for (i = 0; i < length; i++) + { + if (win_size == 16384) + { + remove_node(t_start); + t_start += 1; + if (t_start == max_tbl_sz) + t_start = 0; + } + else + { + win_size += 1; + } + + if (i > 0) + { + insert_node(t_end, out tmp_len, out tmp_pos, 0); + } + t_end += 1; + if (t_end >= max_tbl_sz) + t_end -= max_tbl_sz; + } + } + void re_bittree(ref byte[] probs,int index, int limit, int number) + { + int n, tmp, bit; + + number += limit; + + // Get total bits used by number + tmp = number; + n = 0; + while (tmp > 1) + { + tmp >>= 1; + n++; + } + + do + { + + tmp = number >> n; + bit = (number >> (n - 1)) & 1; + re_bit(ref probs, index + tmp, bit); + + n -= 1; + } while (n > 0); + //number = (res_number - limit); + } + + void re_bit(ref byte[] prob, int index, int bit) + { + uint bound; + uint old_r, old_c; + byte old_p; + + re_normalize(); + + old_r = range; + old_c = code; + old_p = prob[index]; + + var pProb = prob[index]; + + bound = (range >> 8) * (pProb); + pProb -= (byte)(pProb >> 3); + + if (bit != 0) + { + range = bound; + pProb += 31; + } + else + { + code += bound; + if (code < old_c) + out_code += 1; + range -= bound; + } + + prob[index] = pProb; + } + + void re_normalize() + { + if (range < 0x01000000) + { + if (out_code != 0xffffffff) + { + if (out_code > 255) + { + int p, old_c; + p = out_ptr - 1; + do + { + old_c = output[p]; + output[p] += 1; + p -= 1; + } while (old_c == 0xff); + } + + re_putbyte((byte)(out_code & 0xff)); + } + out_code = (code >> 24) & 0xff; + range <<= 8; + code <<= 8; + } + } + + void re_putbyte(byte out_byte) + { + if (out_ptr == out_len) + { + throw new Exception("Output overflow!"); + } + + output[out_ptr++] = out_byte; + } + byte re_getbyte() + { + if (in_ptr == in_len) + { + throw new Exception("End of input!"); + } + + return input[in_ptr++]; + } + + void re_number(ref byte[] prob, int index, int n, int number) + { + int i; + UInt32 old_c; + + i = 1; + + if (n > 3) + { + re_bit(ref prob, index + 3,(number >> (n - i)) & 1); + i += 1; + if (n > 4) + { + re_bit(ref prob, index + 3, (number >> (n - i)) & 1); + i += 1; + if (n > 5) + { + re_normalize(); + for (i = 3; i < n - 2; i++) + { + range >>= 1; + if (((number >> (n - i)) & 1) == 0) + { + old_c = code; + code += range; + if (code < old_c) + out_code += 1; + } + } + } + } + } + + if (n > 0) + { + re_bit(ref prob, index + 0, (number >> (n - i - 0)) & 1); + if (n > 1) + { + re_bit(ref prob, index + 1, (number >> (n - i - 1)) & 1); + if (n > 2) + { + re_bit(ref prob, index + 2, (number >> (n - i - 2)) & 1); + } + } + } + } + void re_flush() + { + re_putbyte((byte)((out_code) & 0xff)); + re_putbyte((byte)((code >> 24) & 0xff)); + re_putbyte((byte)((code >> 16) & 0xff)); + re_putbyte((byte)((code >> 8) & 0xff)); + re_putbyte((byte)((code >> 0) & 0xff)); + } + public int lzrc_compress(byte[] out_buf, int out_len, byte[] in_buf, int in_len) + { + int match_step, re_state, len_state, dist_state; + int i, cur_byte, last_byte; + int match_len, len_bits; + int match_dist, dist_bits, limit; + int round = -1; + + len_state = 0; + + // initalize buffers to all 0x80 + re_init(ref out_buf, out_len, ref in_buf, in_len); + + // initalize the tree + init_tree(); + + // initalize variable to 0 ... + re_state = 0; + last_byte = 0; + match_len = 0; + match_dist = 0; + + while (true) + { + round += 1; + match_step = 0; + + fill_buffer(); + insert_node(t_end, out match_len, out match_dist, 1); + + if (match_len < 256) + { +#if NP9660 + if (match_len < 4 && match_dist > 255) +#else + if (match_len <= 4 && match_dist > 255) +#endif + match_len = 1; + update_tree(match_len); + } +#if NP9660 + if ((match_len == 1 || (match_len < 4 && match_dist > 255))) +#else + if ((match_len == 1 || (match_len <= 4 && match_dist > 255))) +#endif + { + re_bit(ref bm_match[re_state], match_step, 0); + + if (re_state > 0) + re_state -= 1; + + cur_byte = re_getbyte(); + re_bittree(ref bm_literal[((last_byte >> lc) & 0x07)], 0, 0x100, cur_byte); + + if (in_ptr == in_len) + { + re_normalize(); + re_flush(); + return out_ptr; + } + } + else + { + re_bit(ref bm_match[re_state], match_step, 1); + + // write bitstream length (8 bits, where 0 marks the end of it) + len_bits = 0; + for (i = 1; i < 8; i++) + { + match_step += 1; + if ((match_len - 1) < (1 << i)) + break; + re_bit(ref bm_match[re_state], match_step, 1); + len_bits += 1; + } + if (i != 8) + { + re_bit(ref bm_match[re_state], match_step, 0); + } + + if (len_bits > 0) + { + len_state = ((len_bits - 1) << 2) + ((in_ptr << (len_bits - 1)) & 0x03); + re_number(ref bm_len[re_state], len_state, len_bits, (match_len - 1)); + + if (in_ptr == in_len) + { + re_normalize(); + re_flush(); + return out_ptr; + } + } + + + // determine limit ... + dist_state = 0; + limit = 8; +#if NP9660 + if ( match_len > 3) +#else + if( (match_len) > 4 ) +#endif + { + dist_state += 7; +#if NP9660 + limit = 44; +#else + limit = 16; +#endif + } + + // find total 1s in the match_dist + dist_bits = 0; + if(match_dist != 0) { + while ((match_dist >> dist_bits) != 1) + dist_bits += 1; + } + else + { + throw new Exception("Match dist is 0.. uhh cant match with yourself.."); + } + + re_bittree(ref bm_dist_bits[len_bits], dist_state, limit, dist_bits); + + if (dist_bits > 0) + { + re_number(ref bm_dist[dist_bits], 0, dist_bits, match_dist); + } + + + in_ptr += match_len; + re_state = 6 + ((in_ptr + 1) & 1); + } + last_byte = input[in_ptr - 1]; + } + + } + + public int lzrc_decompress(byte[] out_buf, int out_len, byte[] in_buf, int in_len) + { + int match_step, rc_state, len_state, dist_state = 0; + int i, bit, cur_byte, last_byte; + int match_len, len_bits; + int match_dist, dist_bits, limit; + int match_src; + int round = -1; + + len_state = 0; + + rc_init(out_buf, out_len, in_buf, in_len); + + if ((lc & 0x80) != 0) + { + Buffer.BlockCopy(in_buf, 5, out_buf, 0, (int)code); + return (int)code; + } + + rc_state = 0; + last_byte = 0; + + while (true) + { + round += 1; + match_step = 0; + bit = rc_bit(bm_match[rc_state], match_step); + // if bit is 0, just copy from the bit tree into the output ? + if (bit == 0) + { + if (rc_state > 0) + { + rc_state -= 1; + } + + cur_byte = rc_bittree(bm_literal[(((out_ptr & 7) << 8) + last_byte >> lc) & 0x07], 0, 0x100); + + rc_putbyte((byte)cur_byte); + if (out_ptr == out_len) return out_ptr; + } + else // This essentially goes; "hey the bytes that go here, are the same ones that were already *here* + { + // Determine bit length? + len_bits = 0; + for (i = 0; i < 7; i++) + { + match_step += 1; + bit = rc_bit(bm_match[rc_state], match_step); + if (bit == 0) + break; + len_bits += 1; + } + + // Get the match length .. + if (len_bits == 0) + { + match_len = 1; + } + else + { + len_state = ((len_bits - 1) << 2) + ((out_ptr << (len_bits - 1)) & 0x03); + match_len = rc_number(bm_len[rc_state], len_state, len_bits); + //if ((match_len == 0xFF || match_len == 0xFE) && out_ptr == out_len) + //{ + // return out_ptr; + //} + } + + dist_state = 0; + limit = 8; + if (match_len > 3) + { + dist_state += 7; + limit = 16; + } + dist_bits = rc_bittree(bm_dist_bits[len_bits], dist_state, limit); + + if (dist_bits > 0) + { + match_dist = rc_number(bm_dist[dist_bits], 0, dist_bits); + } + else + { + match_dist = 1; + } + + match_src = out_ptr - match_dist; + if (match_dist > out_ptr || match_dist < 0) + { + //Console.WriteLine($"match_dist out of range! 0x{match_dist:x8}"); + //return -1; // test + throw new Exception($"match_dist out of range! 0x{match_dist:x8}"); + } + + + for (i = 0; i < match_len + 1; i++) + { + rc_putbyte(output[match_src++]); + } + rc_state = 6 + ((out_ptr + 1) & 1); + if (out_ptr == out_len) return out_ptr; + } + last_byte = output[out_ptr - 1]; + } + } + } +} diff --git a/PspCrypto/PspCrypto.csproj b/PspCrypto/PspCrypto.csproj new file mode 100644 index 0000000..ad7e34a --- /dev/null +++ b/PspCrypto/PspCrypto.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + true + + + + + + + + + + + + True + True + Resource.resx + + + + + + ResXFileCodeGenerator + Resource.Designer.cs + + + + diff --git a/PspCrypto/PspParameter.cs b/PspCrypto/PspParameter.cs new file mode 100644 index 0000000..0102eb6 --- /dev/null +++ b/PspCrypto/PspParameter.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ECDsaTest +{ + internal class PspParameter + { + public byte[] Kinv { get; set; } + public byte[] Rp { get; set; } + } +} diff --git a/PspCrypto/Resource.Designer.cs b/PspCrypto/Resource.Designer.cs new file mode 100644 index 0000000..f0820d3 --- /dev/null +++ b/PspCrypto/Resource.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PspCrypto { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PspCrypto.Resource", typeof(Resource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] @__sce_discinfo { + get { + object obj = ResourceManager.GetObject("__sce_discinfo", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/PspCrypto/Resource.resx b/PspCrypto/Resource.resx new file mode 100644 index 0000000..ac10a00 --- /dev/null +++ b/PspCrypto/Resource.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + __sce_discinfo;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/PspCrypto/RijndaelMod.cs b/PspCrypto/RijndaelMod.cs new file mode 100644 index 0000000..1061057 --- /dev/null +++ b/PspCrypto/RijndaelMod.cs @@ -0,0 +1,38 @@ +using System; +using System.Security.Cryptography; + +namespace PspCrypto +{ + public class RijndaelMod : Aes + { + public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[] rgbIV) + { + return CreateTransform(rgbKey, encrypting: false); + } + + public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[] rgbIV) + { + return CreateTransform(rgbKey, encrypting: true); + } + + public override void GenerateIV() + { + } + + public override void GenerateKey() + { + byte[] key = new byte[KeySize / BitsPerByte]; + RandomNumberGenerator.Fill(key); + Key = key; + } + + private ICryptoTransform CreateTransform(byte[] rgbKey, bool encrypting) + { + if (rgbKey == null) + throw new ArgumentNullException(nameof(rgbKey)); + return new RijndaelModTransform(rgbKey, BlockSizeValue, encrypting); + } + + private const int BitsPerByte = 8; + } +} diff --git a/PspCrypto/RijndaelModTransform.cs b/PspCrypto/RijndaelModTransform.cs new file mode 100644 index 0000000..c49bbf2 --- /dev/null +++ b/PspCrypto/RijndaelModTransform.cs @@ -0,0 +1,1099 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; +using System.Security; +using System.Security.Cryptography; +using System.Text; + +namespace PspCrypto +{ + class RijndaelModTransform : ICryptoTransform + { + private bool _isEncrypt; + + + private int _blockSizeBits; + private int _blockSizeBytes; + private int _inputBlockSize; + private int _outputBlockSize; + + + private int[] _encryptKeyExpansion; + private int[] _decryptKeyExpansion; + + private int _Nr; + private int _Nb; + private int _Nk; + + private int[] _encryptindex = null; + private int[] _decryptindex = null; + + private int[] _lastBlockBuffer; + private byte[] _depadBuffer; + private byte[] _shiftRegister; + + internal RijndaelModTransform(byte[] rgbKey, + int blockSize, bool isEncrypt) + { + if (rgbKey == null) + throw new ArgumentNullException(nameof(rgbKey)); + Contract.EndContractBlock(); + + _blockSizeBits = blockSize; + _blockSizeBytes = blockSize / 8; + _isEncrypt = isEncrypt; + _Nb = blockSize / 32; + _Nk = rgbKey.Length / 4; + + int S1 = _Nb > 6 ? 3 : 2; + int S2 = _Nb > 6 ? 4 : 3; + + + // Precompute the modulus operations: these are performance killers when called frequently + int[] encryptindex1 = new int[_Nb]; + int[] encryptindex2 = new int[_Nb]; + int[] encryptindex3 = new int[_Nb]; + + int[] decryptindex1 = new int[_Nb]; + int[] decryptindex2 = new int[_Nb]; + int[] decryptindex3 = new int[_Nb]; + + + for (int j = 0; j < _Nb; j++) + { + encryptindex1[j] = (j + 1) % _Nb; + encryptindex2[j] = (j + S1) % _Nb; + encryptindex3[j] = (j + S2) % _Nb; + decryptindex1[j] = (j - 1 + _Nb) % _Nb; + decryptindex2[j] = (j - S1 + _Nb) % _Nb; + decryptindex3[j] = (j - S2 + _Nb) % _Nb; + } + + + _encryptindex = new int[_Nb * 3]; + Array.Copy(encryptindex1, 0, _encryptindex, 0, _Nb); + Array.Copy(encryptindex2, 0, _encryptindex, _Nb, _Nb); + Array.Copy(encryptindex3, 0, _encryptindex, _Nb * 2, _Nb); + + _decryptindex = new int[_Nb * 3]; + Array.Copy(decryptindex1, 0, _decryptindex, 0, _Nb); + Array.Copy(decryptindex2, 0, _decryptindex, _Nb, _Nb); + Array.Copy(decryptindex3, 0, _decryptindex, _Nb * 2, _Nb); + + _inputBlockSize = _blockSizeBytes; + _outputBlockSize = _blockSizeBytes; + + + GenerateKeyExpansion(rgbKey); + } + + public void Dispose() + { + Dispose(true); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + // We need to always zeroize the following fields because they contain sensitive data + if (_lastBlockBuffer != null) + { + Array.Clear(_lastBlockBuffer, 0, _lastBlockBuffer.Length); + _lastBlockBuffer = null; + } + if (_encryptKeyExpansion != null) + { + Array.Clear(_encryptKeyExpansion, 0, _encryptKeyExpansion.Length); + _encryptKeyExpansion = null; + } + if (_decryptKeyExpansion != null) + { + Array.Clear(_decryptKeyExpansion, 0, _decryptKeyExpansion.Length); + _decryptKeyExpansion = null; + } + if (_depadBuffer != null) + { + Array.Clear(_depadBuffer, 0, _depadBuffer.Length); + _depadBuffer = null; + } + if (_shiftRegister != null) + { + Array.Clear(_shiftRegister, 0, _shiftRegister.Length); + _shiftRegister = null; + } + } + } + public bool CanReuseTransform => true; + public bool CanTransformMultipleBlocks => true; + public int InputBlockSize => _inputBlockSize; + public int OutputBlockSize => _outputBlockSize; + + public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset) + { // Note: special handling required if decrypting & using padding because the padding adds to the end of the last + // block, we have to buffer an entire block's worth of bytes in case what I just transformed turns out to be + // the last block Then in TransformFinalBlock we strip off the padding. + + if (inputBuffer == null) throw new ArgumentNullException(nameof(inputBuffer)); + if (outputBuffer == null) throw new ArgumentNullException(nameof(outputBuffer)); + if (inputOffset < 0) throw new ArgumentOutOfRangeException(nameof(inputOffset)); + if (inputCount <= 0 || (inputCount % InputBlockSize != 0) || (inputCount > inputBuffer.Length)) throw new ArgumentException("Argument_InvalidValue"); + if ((inputBuffer.Length - inputCount) < inputOffset) throw new ArgumentException("Argument_InvalidOffLen"); + Contract.EndContractBlock(); + + if (_isEncrypt) + { + return EncryptData(inputBuffer,inputOffset,inputCount,ref outputBuffer,outputOffset,false); + } + else + { + return DecryptData(inputBuffer, inputOffset, inputCount, ref outputBuffer, outputOffset, false); + } + } + + public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + if (inputBuffer == null) throw new ArgumentNullException(nameof(inputBuffer)); + if (inputOffset < 0) throw new ArgumentOutOfRangeException(nameof(inputOffset), "ArgumentOutOfRange_NeedNonNegNum"); + if (inputCount < 0 || (inputCount > inputBuffer.Length)) throw new ArgumentException("Argument_InvalidValue"); + if ((inputBuffer.Length - inputCount) < inputOffset) throw new ArgumentException("Argument_InvalidOffLen"); + Contract.EndContractBlock(); + if (_isEncrypt) + { + byte[] transformedBytes = null; + EncryptData(inputBuffer, + inputOffset, + inputCount, + ref transformedBytes, + 0, + true); + return transformedBytes; + } + else + { + if (inputCount % InputBlockSize != 0) + throw new CryptographicException("Cryptography_SSD_InvalidDataSize"); + byte[] transformedBytes = null; + DecryptData(inputBuffer, + inputOffset, + inputCount, + ref transformedBytes, + 0, + true); + return transformedBytes; + } + } + + [SecuritySafeCritical] + private unsafe int EncryptData(byte[] inputBuffer, + int inputOffset, + int inputCount, + ref byte[] outputBuffer, + int outputOffset, + bool fLast) + { + + if (inputBuffer.Length < inputOffset + inputCount) + throw new CryptographicException("Cryptography_InsufficientBuffer"); + + int lonelyBytes = inputCount % _inputBlockSize; + + int workBaseIndex = inputOffset, index = 0; + + if (lonelyBytes != 0) + throw new CryptographicException("Cryptography_SSE_InvalidDataSize"); + + if (outputBuffer == null) + { + outputBuffer = new byte[inputCount]; + outputOffset = 0; + } + else + { + if (outputBuffer.Length - outputOffset < inputCount) + throw new CryptographicException("Cryptography_InsufficientBuffer"); + } + + fixed (int* encryptindex = _encryptindex) + { + fixed (int* encryptKeyExpansion = _encryptKeyExpansion) + { + fixed (int* T = s_T) + { + fixed (int* TF = s_TF) + { + int* work = stackalloc int[_Nb]; + int* temp = stackalloc int[_Nb]; + + int iNumBlocks = inputCount / _inputBlockSize; + int transformCount = outputOffset; + for (int blockNum = 0; blockNum < iNumBlocks; ++blockNum) + { + fixed (byte* inputPtr = inputBuffer) + { + Buffer.MemoryCopy(inputPtr + workBaseIndex, work, _blockSizeBytes, _blockSizeBytes); + } + + Enc(encryptindex, encryptKeyExpansion, T, TF, work, temp); + + for (int i = 0; i < _Nb; ++i) + { + outputBuffer[transformCount++] = (byte)(temp[i] & 0xFF); + outputBuffer[transformCount++] = (byte)(temp[i] >> 8 & 0xFF); + outputBuffer[transformCount++] = (byte)(temp[i] >> 16 & 0xFF); + outputBuffer[transformCount++] = (byte)(temp[i] >> 24 & 0xFF); + } + workBaseIndex += _inputBlockSize; + } + } + } + } + } + + return inputCount; + } + + + [System.Security.SecuritySafeCritical] // auto-generated + private unsafe int DecryptData(IReadOnlyList inputBuffer, + int inputOffset, + int inputCount, + ref byte[] outputBuffer, + int outputOffset, + bool fLast) + { + if (inputBuffer.Count < inputOffset + inputCount) + throw new CryptographicException("Cryptography_InsufficientBuffer"); + + if (outputBuffer == null) + { + outputBuffer = new byte[inputCount]; + outputOffset = 0; + } + else + { + if ((outputBuffer.Length - outputOffset) < inputCount) + throw new CryptographicException("Cryptography_InsufficientBuffer"); + } + + fixed (int* encryptindex = _encryptindex) + { + fixed (int* encryptKeyExpansion = _encryptKeyExpansion) + { + fixed (int* decryptindex = _decryptindex) + { + fixed (int* decryptKeyExpansion = _decryptKeyExpansion) + { + fixed (int* T = s_T) + { + fixed (int* TF = s_TF) + { + fixed (int* iT = s_iT) + { + fixed (int* iTF = s_iTF) + { + int* work = stackalloc int[_Nb]; + int* temp = stackalloc int[_Nb]; + + int iNumBlocks = inputCount / _inputBlockSize; + int workBaseIndex = inputOffset, index = 0, transformCount = outputOffset; + for (int blockNum = 0; blockNum < iNumBlocks; ++blockNum) + { + index = workBaseIndex; + for (int i = 0; i < _Nb; ++i) + { + int i0 = inputBuffer[index++]; + int i1 = inputBuffer[index++]; + int i2 = inputBuffer[index++]; + int i3 = inputBuffer[index++]; + work[i] = i3 << 24 | i2 << 16 | i1 << 8 | i0; + } + Dec(decryptindex, decryptKeyExpansion, iT, iTF, work, temp); + for (int i = 0; i < _Nb; ++i) + { + outputBuffer[transformCount++] = (byte)(temp[i] & 0xFF); + outputBuffer[transformCount++] = (byte)(temp[i] >> 8 & 0xFF); + outputBuffer[transformCount++] = (byte)(temp[i] >> 16 & 0xFF); + outputBuffer[transformCount++] = (byte)(temp[i] >> 24 & 0xFF); + } + workBaseIndex += _inputBlockSize; + } + if (fLast == false) + return inputCount; + return outputBuffer.Length; + } + } + } + } + } + } + } + } + } + + // + // AES encryption function. + // + [System.Security.SecurityCritical] // auto-generated + private unsafe void Enc(int* encryptindex, int* encryptKeyExpansion, int* T, int* TF, int* work, int* temp) + { + for (int i = 0; i < _Nb; ++i) + { + work[i] ^= encryptKeyExpansion[i]; + } + + int* _encryptindex; + int* _encryptKeyExpansion = &encryptKeyExpansion[_Nb]; + for (int r = 1; r < _Nr; ++r) + { + _encryptindex = encryptindex; + for (int i = 0; i < _Nb; ++i) + { + temp[i] = T[0 + (work[i] & 0xFF)] ^ + T[256 + ((work[_encryptindex[0]] >> 8) & 0xFF)] ^ + T[512 + ((work[_encryptindex[_Nb]] >> 16) & 0xFF)] ^ + T[768 + ((work[_encryptindex[_Nb * 2]] >> 24) & 0xFF)] ^ + *_encryptKeyExpansion; + _encryptindex++; + _encryptKeyExpansion++; + } + + for (int i = 0; i < _Nb; ++i) + { + work[i] = temp[i]; + } + } + + _encryptindex = encryptindex; + for (int i = 0; i < _Nb; ++i) + { + temp[i] = TF[0 + (work[i] & 0xFF)] ^ + TF[256 + ((work[_encryptindex[0]] >> 8) & 0xFF)] ^ + TF[512 + ((work[_encryptindex[_Nb]] >> 16) & 0xFF)] ^ + TF[768 + ((work[_encryptindex[_Nb * 2]] >> 24) & 0xFF)] ^ + *_encryptKeyExpansion; + _encryptindex++; + _encryptKeyExpansion++; + } + } + + // + // AES decryption function. + // + [System.Security.SecurityCritical] // auto-generated + unsafe private void Dec(int* decryptindex, int* decryptKeyExpansion, int* iT, int* iTF, int* work, int* temp) + { + int keyIndex = _Nb * _Nr; + for (int i = 0; i < _Nb; ++i) + { + work[i] ^= decryptKeyExpansion[keyIndex]; + keyIndex++; + } + + int* _decryptindex; + int* _decryptKeyExpansion; + for (int r = 1; r < _Nr; ++r) + { + keyIndex -= 2 * _Nb; + _decryptindex = decryptindex; + _decryptKeyExpansion = &decryptKeyExpansion[keyIndex]; + for (int i = 0; i < _Nb; ++i) + { + temp[i] = iT[0 + ((work[i]) & 0xFF)] ^ + iT[256 + ((work[_decryptindex[0]] >> 8) & 0xFF)] ^ + iT[512 + ((work[_decryptindex[_Nb]] >> 16) & 0xFF)] ^ + iT[768 + ((work[_decryptindex[_Nb * 2]] >> 24) & 0xFF)] ^ + *_decryptKeyExpansion; + keyIndex++; + _decryptindex++; + _decryptKeyExpansion++; + } + for (int i = 0; i < _Nb; ++i) + { + work[i] = temp[i]; + } + } + + keyIndex = 0; + _decryptindex = decryptindex; + _decryptKeyExpansion = &decryptKeyExpansion[keyIndex]; + for (int i = 0; i < _Nb; ++i) + { + temp[i] = iTF[0 + ((work[i]) & 0xFF)] ^ + iTF[256 + ((work[_decryptindex[0]] >> 8) & 0xFF)] ^ + iTF[512 + ((work[_decryptindex[_Nb]] >> 16) & 0xFF)] ^ + iTF[768 + ((work[_decryptindex[_Nb * 2]] >> 24) & 0xFF)] ^ + *_decryptKeyExpansion; + _decryptindex++; + _decryptKeyExpansion++; + } + } + + // + // Key expansion routine. + // + + private void GenerateKeyExpansion(byte[] rgbKey) + { + switch (_blockSizeBits > rgbKey.Length * 8 ? _blockSizeBits : rgbKey.Length * 8) + { + case 128: + _Nr = 10; + break; + default: + throw new CryptographicException("InvalidKeySize"); + } + + _encryptKeyExpansion = new int[_Nb * (_Nr + 1)]; + _decryptKeyExpansion = new int[_Nb * (_Nr + 1)]; + int iTemp; + + int index = 0; + for (int i = 0; i < _Nk; ++i) + { + int i0 = rgbKey[index++]; + int i1 = rgbKey[index++]; + int i2 = rgbKey[index++]; + int i3 = rgbKey[index++]; + _encryptKeyExpansion[i] = i3 << 24 | i2 << 16 | i1 << 8 | i0; + } + + for (int i = _Nk; i < _Nb * (_Nr + 1); ++i) + { + iTemp = _encryptKeyExpansion[i - 1]; + + if (i % _Nk == 0) + { + iTemp = SubWord(rot3(iTemp)); + iTemp = iTemp ^ s_Rcon[(i / _Nk) - 1]; + } + else if (i % _Nk == 4) + { + iTemp = SubWord(iTemp); + } + + _encryptKeyExpansion[i] = _encryptKeyExpansion[i - _Nk] ^ iTemp; + } + + for (int i = 0; i < _Nb; ++i) + { + _decryptKeyExpansion[i] = _encryptKeyExpansion[i]; + _decryptKeyExpansion[_Nb * _Nr + i] = _encryptKeyExpansion[_Nb * _Nr + i]; + } + + for (int i = _Nb; i < _Nb * _Nr; ++i) + { + int key = _encryptKeyExpansion[i]; + int mul02 = MulX(key); + int mul04 = MulX(mul02); + int mul08 = MulX(mul04); + int mul09 = key ^ mul08; + _decryptKeyExpansion[i] = mul02 ^ mul04 ^ mul08 ^ rot3(mul02 ^ mul09) ^ rot2(mul04 ^ mul09) ^ rot1(mul09); + } + } + + private static int rot1(int val) + { + return (val << 8 & unchecked((int)0xFFFFFF00)) | (val >> 24 & unchecked((int)0x000000FF)); + } + + private static int rot2(int val) + { + return (val << 16 & unchecked((int)0xFFFF0000)) | (val >> 16 & unchecked((int)0x0000FFFF)); + } + + private static int rot3(int val) + { + return (val << 24 & unchecked((int)0xFF000000)) | (val >> 8 & unchecked((int)0x00FFFFFF)); + } + + private static int SubWord(int a) + { + return s_Sbox[a & 0xFF] | + s_Sbox[a >> 8 & 0xFF] << 8 | + s_Sbox[a >> 16 & 0xFF] << 16 | + s_Sbox[a >> 24 & 0xFF] << 24; + } + + private static int MulX(int x) + { + int u = x & unchecked((int)0x80808080); + return ((x & unchecked((int)0x7f7f7f7f)) << 1) ^ ((u - (u >> 7 & 0x01FFFFFF)) & 0x1b1b1b1b); + } + + private static readonly byte[] s_Sbox = new byte[] { + 99, 124, 119, 123, 242, 107, 111, 197, 48, 1, 103, 43, 254, 215, 171, 118, + 202, 130, 201, 125, 250, 89, 71, 240, 173, 212, 162, 175, 156, 164, 114, 192, + 183, 253, 147, 38, 54, 63, 247, 204, 52, 165, 229, 241, 113, 216, 49, 21, + 4, 199, 35, 195, 24, 150, 5, 154, 7, 18, 128, 226, 235, 39, 178, 117, + 9, 131, 44, 26, 27, 110, 90, 160, 82, 59, 214, 179, 41, 227, 47, 132, + 83, 209, 0, 237, 32, 252, 177, 91, 106, 203, 190, 57, 74, 76, 88, 207, + 208, 239, 170, 251, 67, 77, 51, 133, 69, 249, 2, 127, 80, 60, 159, 168, + 81, 163, 64, 143, 146, 157, 56, 245, 188, 182, 218, 33, 16, 255, 243, 210, + 205, 12, 19, 236, 95, 151, 68, 23, 196, 167, 126, 61, 100, 93, 25, 115, + 96, 129, 79, 220, 34, 42, 144, 136, 70, 238, 184, 20, 222, 94, 11, 219, + 224, 50, 58, 10, 73, 6, 36, 92, 194, 211, 172, 98, 145, 149, 228, 121, + 231, 200, 55, 109, 141, 213, 78, 169, 108, 86, 244, 234, 101, 122, 174, 8, + 186, 120, 37, 46, 28, 166, 180, 198, 232, 221, 116, 31, 75, 189, 139, 138, + 112, 62, 181, 102, 72, 3, 246, 14, 97, 53, 87, 185, 134, 193, 29, 158, + 225, 248, 152, 17, 105, 217, 142, 148, 155, 30, 135, 233, 206, 85, 40, 223, + 140, 161, 137, 13, 191, 230, 66, 104, 65, 153, 45, 15, 176, 84, 187, 22 }; + + private static readonly int[] s_Rcon = new int[] { + 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, + 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, + 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91 }; + + private static readonly int[] s_T = new int[4 * 256] + { + // s_T1 + -1520213050, -2072216328, -1720223762, -1921287178, 234025727, -1117033514, -1318096930, 1422247313, + 1345335392, 50397442, -1452841010, 2099981142, 436141799, 1658312629, -424957107, -1703512340, + 1170918031, -1652391393, 1086966153, -2021818886, 368769775, -346465870, -918075506, 200339707, + -324162239, 1742001331, -39673249, -357585083, -1080255453, -140204973, -1770884380, 1539358875, + -1028147339, 486407649, -1366060227, 1780885068, 1513502316, 1094664062, 49805301, 1338821763, + 1546925160, -190470831, 887481809, 150073849, -1821281822, 1943591083, 1395732834, 1058346282, + 201589768, 1388824469, 1696801606, 1589887901, 672667696, -1583966665, 251987210, -1248159185, + 151455502, 907153956, -1686077413, 1038279391, 652995533, 1764173646, -843926913, -1619692054, + 453576978, -1635548387, 1949051992, 773462580, 756751158, -1301385508, -296068428, -73359269, + -162377052, 1295727478, 1641469623, -827083907, 2066295122, 1055122397, 1898917726, -1752923117, + -179088474, 1758581177, 0, 753790401, 1612718144, 536673507, -927878791, -312779850, + -1100322092, 1187761037, -641810841, 1262041458, -565556588, -733197160, -396863312, 1255133061, + 1808847035, 720367557, -441800113, 385612781, -985447546, -682799718, 1429418854, -1803188975, + -817543798, 284817897, 100794884, -2122350594, -263171936, 1144798328, -1163944155, -475486133, + -212774494, -22830243, -1069531008, -1970303227, -1382903233, -1130521311, 1211644016, 83228145, + -541279133, -1044990345, 1977277103, 1663115586, 806359072, 452984805, 250868733, 1842533055, + 1288555905, 336333848, 890442534, 804056259, -513843266, -1567123659, -867941240, 957814574, + 1472513171, -223893675, -2105639172, 1195195770, -1402706744, -413311558, 723065138, -1787595802, + -1604296512, -1736343271, -783331426, 2145180835, 1713513028, 2116692564, -1416589253, -2088204277, + -901364084, 703524551, -742868885, 1007948840, 2044649127, -497131844, 487262998, 1994120109, + 1004593371, 1446130276, 1312438900, 503974420, -615954030, 168166924, 1814307912, -463709000, + 1573044895, 1859376061, -273896381, -1503501628, -1466855111, -1533700815, 937747667, -1954973198, + 854058965, 1137232011, 1496790894, -1217565222, -1936880383, 1691735473, -766620004, -525751991, + -1267962664, -95005012, 133494003, 636152527, -1352309302, -1904575756, -374428089, 403179536, + -709182865, -2005370640, 1864705354, 1915629148, 605822008, -240736681, -944458637, 1371981463, + 602466507, 2094914977, -1670089496, 555687742, -582268010, -591544991, -2037675251, -2054518257, + -1871679264, 1111375484, -994724495, -1436129588, -666351472, 84083462, 32962295, 302911004, + -1553899070, 1597322602, -111716434, -793134743, -1853454825, 1489093017, 656219450, -1180787161, + 954327513, 335083755, -1281845205, 856756514, -1150719534, 1893325225, -1987146233, -1483434957, + -1231316179, 572399164, -1836611819, 552200649, 1238290055, -11184726, 2015897680, 2061492133, + -1886614525, -123625127, -2138470135, 386731290, -624967835, 837215959, -968736124, -1201116976, + -1019133566, -1332111063, 1999449434, 286199582, -877612933, -61582168, -692339859, 974525996, + + // s_T2 + 1667483301, 2088564868, 2004348569, 2071721613, -218956019, 1802229437, 1869602481, -976907948, + 808476752, 16843267, 1734856361, 724260477, -16849127, -673729182, -1414836762, 1987505306, + -892694715, -2105401443, -909539008, 2105408135, -84218091, 1499050731, 1195871945, -252642549, + -1381154324, -724257945, -1566416899, -1347467798, -1667488833, -1532734473, 1920132246, -1061119141, + -1212713534, -33693412, -1819066962, 640044138, 909536346, 1061125697, -134744830, -859012273, + 875849820, -1515892236, -437923532, -235800312, 1903288979, -656888973, 825320019, 353708607, + 67373068, -943221422, 589514341, -1010590370, 404238376, -1768540255, 84216335, -1701171275, + 117902857, 303178806, -2139087973, -488448195, -336868058, 656887401, -1296924723, 1970662047, + 151589403, -2088559202, 741103732, 437924910, 454768173, 1852759218, 1515893998, -1600103429, + 1381147894, 993752653, -690571423, -1280082482, 690573947, -471605954, 791633521, -2071719017, + 1397991157, -774784664, 0, -303185620, 538984544, -50535649, -1313769016, 1532737261, + 1785386174, -875852474, -1094817831, 960066123, 1246401758, 1280088276, 1482207464, -808483510, + -791626901, -269499094, -1431679003, -67375850, 1128498885, 1296931543, 859006549, -2054876780, + 1162185423, -101062384, 33686534, 2139094657, 1347461360, 1010595908, -1616960070, -1465365533, + 1364304627, -1549574658, 1077969088, -1886452342, -1835909203, -1650646596, 943222856, -168431356, + -1128504353, -1229555775, -623202443, 555827811, 269492272, -6886, -202113778, -757940371, + -842170036, 202119188, 320022069, -320027857, 1600110305, -1751698014, 1145342156, 387395129, + -993750185, -1482205710, 2122251394, 1027439175, 1684326572, 1566423783, 421081643, 1936975509, + 1616953504, -2122245736, 1330618065, -589520001, 572671078, 707417214, -1869595733, -2004350077, + 1179028682, -286341335, -1195873325, 336865340, -555833479, 1583267042, 185275933, -606360202, + -522134725, 842163286, 976909390, 168432670, 1229558491, 101059594, 606357612, 1549580516, + -1027432611, -741098130, -1397996561, 1650640038, -1852753496, -1785384540, -454765769, 2038035083, + -404237006, -926381245, 926379609, 1835915959, -1920138868, -707415708, 1313774802, -1448523296, + 1819072692, 1448520954, -185273593, -353710299, 1701169839, 2054878350, -1364310039, 134746136, + -1162186795, 2021191816, 623200879, 774790258, 471611428, -1499047951, -1263242297, -960063663, + -387396829, -572677764, 1953818780, 522141217, 1263245021, -1111662116, -1953821306, -1970663547, + 1886445712, 1044282434, -1246400060, 1718013098, 1212715224, 50529797, -151587071, 235805714, + 1633796771, 892693087, 1465364217, -1179031088, -2038032495, -1044276904, 488454695, -1633802311, + -505292488, -117904621, -1734857805, 286335539, 1768542907, -640046736, -1903294583, -1802226777, + -1684329034, 505297954, -2021190254, -370554592, -825325751, 1431677695, 673730680, -538991238, + -1936981105, -1583261192, -1987507840, 218962455, -1077975590, -421079247, 1111655622, 1751699640, + 1094812355, -1718015568, 757946999, 252648977, -1330611253, 1414834428, -1145344554, 370551866, + + // s_T3 + 1673962851, 2096661628, 2012125559, 2079755643, -218165774, 1809235307, 1876865391, -980331323, + 811618352, 16909057, 1741597031, 727088427, -18408962, -675978537, -1420958037, 1995217526, + -896580150, -2111857278, -913751863, 2113570685, -84994566, 1504897881, 1200539975, -251982864, + -1388188499, -726439980, -1570767454, -1354372433, -1675378788, -1538000988, 1927583346, -1063560256, + -1217019209, -35578627, -1824674157, 642542118, 913070646, 1065238847, -134937865, -863809588, + 879254580, -1521355611, -439274267, -235337487, 1910674289, -659852328, 828527409, 355090197, + 67636228, -946515257, 591815971, -1013096765, 405809176, -1774739050, 84545285, -1708149350, + 118360327, 304363026, -2145674368, -488686110, -338876693, 659450151, -1300247118, 1978310517, + 152181513, -2095210877, 743994412, 439627290, 456535323, 1859957358, 1521806938, -1604584544, + 1386542674, 997608763, -692624938, -1283600717, 693271337, -472039709, 794718511, -2079090812, + 1403450707, -776378159, 0, -306107155, 541089824, -52224004, -1317418831, 1538714971, + 1792327274, -879933749, -1100490306, 963791673, 1251270218, 1285084236, 1487988824, -813348145, + -793023536, -272291089, -1437604438, -68348165, 1132905795, 1301993293, 862344499, -2062445435, + 1166724933, -102166279, 33818114, 2147385727, 1352724560, 1014514748, -1624917345, -1471421528, + 1369633617, -1554121053, 1082179648, -1895462257, -1841320558, -1658733411, 946882616, -168753931, + -1134305348, -1233665610, -626035238, 557998881, 270544912, -1762561, -201519373, -759206446, + -847164211, 202904588, 321271059, -322752532, 1606345055, -1758092649, 1149815876, 388905239, + -996976700, -1487539545, 2130477694, 1031423805, 1690872932, 1572530013, 422718233, 1944491379, + 1623236704, -2129028991, 1335808335, -593264676, 574907938, 710180394, -1875137648, -2012511352, + 1183631942, -288937490, -1200893000, 338181140, -559449634, 1589437022, 185998603, -609388837, + -522503200, 845436466, 980700730, 169090570, 1234361161, 101452294, 608726052, 1555620956, + -1029743166, -742560045, -1404833876, 1657054818, -1858492271, -1791908715, -455919644, 2045938553, + -405458201, -930397240, 929978679, 1843050349, -1929278323, -709794603, 1318900302, -1454776151, + 1826141292, 1454176854, -185399308, -355523094, 1707781989, 2062847610, -1371018834, 135272456, + -1167075910, 2029029496, 625635109, 777810478, 473441308, -1504185946, -1267480652, -963161658, + -389340184, -576619299, 1961401460, 524165407, 1268178251, -1117659971, -1962047861, -1978694262, + 1893765232, 1048330814, -1250835275, 1724688998, 1217452104, 50726147, -151584266, 236720654, + 1640145761, 896163637, 1471084887, -1184247623, -2045275770, -1046914879, 490350365, -1641563746, + -505857823, -118811656, -1741966440, 287453969, 1775418217, -643206951, -1912108658, -1808554092, + -1691502949, 507257374, -2028629369, -372694807, -829994546, 1437269845, 676362280, -542803233, + -1945923700, -1587939167, -1995865975, 219813645, -1083843905, -422104602, 1115997762, 1758509160, + 1099088705, -1725321063, 760903469, 253628687, -1334064208, 1420360788, -1150429509, 371997206, + + // s_T4 + -962239645, -125535108, -291932297, -158499973, -15863054, -692229269, -558796945, -1856715323, + 1615867952, 33751297, -827758745, 1451043627, -417726722, -1251813417, 1306962859, -325421450, + -1891251510, 530416258, -1992242743, -91783811, -283772166, -1293199015, -1899411641, -83103504, + 1106029997, -1285040940, 1610457762, 1173008303, 599760028, 1408738468, -459902350, -1688485696, + 1975695287, -518193667, 1034851219, 1282024998, 1817851446, 2118205247, -184354825, -2091922228, + 1750873140, 1374987685, -785062427, -116854287, -493653647, -1418471208, 1649619249, 708777237, + 135005188, -1789737017, 1181033251, -1654733885, 807933976, 933336726, 168756485, 800430746, + 235472647, 607523346, 463175808, -549592350, -853087253, 1315514151, 2144187058, -358648459, + 303761673, 496927619, 1484008492, 875436570, 908925723, -592286098, -1259447718, 1543217312, + -1527360942, 1984772923, -1218324778, 2110698419, 1383803177, -583080989, 1584475951, 328696964, + -1493871789, -1184312879, 0, -1054020115, 1080041504, -484442884, 2043195825, -1225958565, + -725718422, -1924740149, 1742323390, 1917532473, -1797371318, -1730917300, -1326950312, -2058694705, + -1150562096, -987041809, 1340451498, -317260805, -2033892541, -1697166003, 1716859699, 294946181, + -1966127803, -384763399, 67502594, -25067649, -1594863536, 2017737788, 632987551, 1273211048, + -1561112239, 1576969123, -2134884288, 92966799, 1068339858, 566009245, 1883781176, -251333131, + 1675607228, 2009183926, -1351230758, 1113792801, 540020752, -451215361, -49351693, -1083321646, + -2125673011, 403966988, 641012499, -1020269332, -1092526241, 899848087, -1999879100, 775493399, + -1822964540, 1441965991, -58556802, 2051489085, -928226204, -1159242403, 841685273, -426413197, + -1063231392, 429425025, -1630449841, -1551901476, 1147544098, 1417554474, 1001099408, 193169544, + -1932900794, -953553170, 1809037496, 675025940, -1485185314, -1126015394, 371002123, -1384719397, + -616832800, 1683370546, 1951283770, 337512970, -1831122615, 201983494, 1215046692, -1192993700, + -1621245246, -1116810285, 1139780780, -995728798, 967348625, 832869781, -751311644, -225740423, + -718084121, -1958491960, 1851340599, -625513107, 25988493, -1318791723, -1663938994, 1239460265, + -659264404, -1392880042, -217582348, -819598614, -894474907, -191989126, 1206496942, 270010376, + 1876277946, -259491720, 1248797989, 1550986798, 941890588, 1475454630, 1942467764, -1756248378, + -886839064, -1585652259, -392399756, 1042358047, -1763882165, 1641856445, 226921355, 260409994, + -527404944, 2084716094, 1908716981, -861247898, -1864873912, 100991747, -150866186, 470945294, + -1029480095, 1784624437, -1359390889, 1775286713, 395413126, -1722236479, 975641885, 666476190, + -650583583, -351012616, 733190296, 573772049, -759469719, -1452221991, 126455438, 866620564, + 766942107, 1008868894, 361924487, -920589847, -2025206066, -1426107051, 1350051880, -1518673953, + 59739276, 1509466529, 159418761, 437718285, 1708834751, -684595482, -2067381694, -793221016, + -2101132991, 699439513, 1517759789, 504434447, 2076946608, -1459858348, 1842789307, 742004246 }; + + private static readonly int[] s_TF = new int[4 * 256] + { + // s_TF1 + 99, 124, 119, 123, 242, 107, 111, 197, + 48, 1, 103, 43, 254, 215, 171, 118, + 202, 130, 201, 125, 250, 89, 71, 240, + 173, 212, 162, 175, 156, 164, 114, 192, + 183, 253, 147, 38, 54, 63, 247, 204, + 52, 165, 229, 241, 113, 216, 49, 21, + 4, 199, 35, 195, 24, 150, 5, 154, + 7, 18, 128, 226, 235, 39, 178, 117, + 9, 131, 44, 26, 27, 110, 90, 160, + 82, 59, 214, 179, 41, 227, 47, 132, + 83, 209, 0, 237, 32, 252, 177, 91, + 106, 203, 190, 57, 74, 76, 88, 207, + 208, 239, 170, 251, 67, 77, 51, 133, + 69, 249, 2, 127, 80, 60, 159, 168, + 81, 163, 64, 143, 146, 157, 56, 245, + 188, 182, 218, 33, 16, 255, 243, 210, + 205, 12, 19, 236, 95, 151, 68, 23, + 196, 167, 126, 61, 100, 93, 25, 115, + 96, 129, 79, 220, 34, 42, 144, 136, + 70, 238, 184, 20, 222, 94, 11, 219, + 224, 50, 58, 10, 73, 6, 36, 92, + 194, 211, 172, 98, 145, 149, 228, 121, + 231, 200, 55, 109, 141, 213, 78, 169, + 108, 86, 244, 234, 101, 122, 174, 8, + 186, 120, 37, 46, 28, 166, 180, 198, + 232, 221, 116, 31, 75, 189, 139, 138, + 112, 62, 181, 102, 72, 3, 246, 14, + 97, 53, 87, 185, 134, 193, 29, 158, + 225, 248, 152, 17, 105, 217, 142, 148, + 155, 30, 135, 233, 206, 85, 40, 223, + 140, 161, 137, 13, 191, 230, 66, 104, + 65, 153, 45, 15, 176, 84, 187, 22, + + // s_TF2 + 25344, 31744, 30464, 31488, 61952, 27392, 28416, 50432, + 12288, 256, 26368, 11008, 65024, 55040, 43776, 30208, + 51712, 33280, 51456, 32000, 64000, 22784, 18176, 61440, + 44288, 54272, 41472, 44800, 39936, 41984, 29184, 49152, + 46848, 64768, 37632, 9728, 13824, 16128, 63232, 52224, + 13312, 42240, 58624, 61696, 28928, 55296, 12544, 5376, + 1024, 50944, 8960, 49920, 6144, 38400, 1280, 39424, + 1792, 4608, 32768, 57856, 60160, 9984, 45568, 29952, + 2304, 33536, 11264, 6656, 6912, 28160, 23040, 40960, + 20992, 15104, 54784, 45824, 10496, 58112, 12032, 33792, + 21248, 53504, 0, 60672, 8192, 64512, 45312, 23296, + 27136, 51968, 48640, 14592, 18944, 19456, 22528, 52992, + 53248, 61184, 43520, 64256, 17152, 19712, 13056, 34048, + 17664, 63744, 512, 32512, 20480, 15360, 40704, 43008, + 20736, 41728, 16384, 36608, 37376, 40192, 14336, 62720, + 48128, 46592, 55808, 8448, 4096, 65280, 62208, 53760, + 52480, 3072, 4864, 60416, 24320, 38656, 17408, 5888, + 50176, 42752, 32256, 15616, 25600, 23808, 6400, 29440, + 24576, 33024, 20224, 56320, 8704, 10752, 36864, 34816, + 17920, 60928, 47104, 5120, 56832, 24064, 2816, 56064, + 57344, 12800, 14848, 2560, 18688, 1536, 9216, 23552, + 49664, 54016, 44032, 25088, 37120, 38144, 58368, 30976, + 59136, 51200, 14080, 27904, 36096, 54528, 19968, 43264, + 27648, 22016, 62464, 59904, 25856, 31232, 44544, 2048, + 47616, 30720, 9472, 11776, 7168, 42496, 46080, 50688, + 59392, 56576, 29696, 7936, 19200, 48384, 35584, 35328, + 28672, 15872, 46336, 26112, 18432, 768, 62976, 3584, + 24832, 13568, 22272, 47360, 34304, 49408, 7424, 40448, + 57600, 63488, 38912, 4352, 26880, 55552, 36352, 37888, + 39680, 7680, 34560, 59648, 52736, 21760, 10240, 57088, + 35840, 41216, 35072, 3328, 48896, 58880, 16896, 26624, + 16640, 39168, 11520, 3840, 45056, 21504, 47872, 5632, + + // s_TF3 + 6488064, 8126464, 7798784, 8060928, 15859712, 7012352, 7274496, 12910592, + 3145728, 65536, 6750208, 2818048, 16646144, 14090240, 11206656, 7733248, + 13238272, 8519680, 13172736, 8192000, 16384000, 5832704, 4653056, 15728640, + 11337728, 13893632, 10616832, 11468800, 10223616, 10747904, 7471104, 12582912, + 11993088, 16580608, 9633792, 2490368, 3538944, 4128768, 16187392, 13369344, + 3407872, 10813440, 15007744, 15794176, 7405568, 14155776, 3211264, 1376256, + 262144, 13041664, 2293760, 12779520, 1572864, 9830400, 327680, 10092544, + 458752, 1179648, 8388608, 14811136, 15400960, 2555904, 11665408, 7667712, + 589824, 8585216, 2883584, 1703936, 1769472, 7208960, 5898240, 10485760, + 5373952, 3866624, 14024704, 11730944, 2686976, 14876672, 3080192, 8650752, + 5439488, 13697024, 0, 15532032, 2097152, 16515072, 11599872, 5963776, + 6946816, 13303808, 12451840, 3735552, 4849664, 4980736, 5767168, 13565952, + 13631488, 15663104, 11141120, 16449536, 4390912, 5046272, 3342336, 8716288, + 4521984, 16318464, 131072, 8323072, 5242880, 3932160, 10420224, 11010048, + 5308416, 10682368, 4194304, 9371648, 9568256, 10289152, 3670016, 16056320, + 12320768, 11927552, 14286848, 2162688, 1048576, 16711680, 15925248, 13762560, + 13434880, 786432, 1245184, 15466496, 6225920, 9895936, 4456448, 1507328, + 12845056, 10944512, 8257536, 3997696, 6553600, 6094848, 1638400, 7536640, + 6291456, 8454144, 5177344, 14417920, 2228224, 2752512, 9437184, 8912896, + 4587520, 15597568, 12058624, 1310720, 14548992, 6160384, 720896, 14352384, + 14680064, 3276800, 3801088, 655360, 4784128, 393216, 2359296, 6029312, + 12713984, 13828096, 11272192, 6422528, 9502720, 9764864, 14942208, 7929856, + 15138816, 13107200, 3604480, 7143424, 9240576, 13959168, 5111808, 11075584, + 7077888, 5636096, 15990784, 15335424, 6619136, 7995392, 11403264, 524288, + 12189696, 7864320, 2424832, 3014656, 1835008, 10878976, 11796480, 12976128, + 15204352, 14483456, 7602176, 2031616, 4915200, 12386304, 9109504, 9043968, + 7340032, 4063232, 11862016, 6684672, 4718592, 196608, 16121856, 917504, + 6356992, 3473408, 5701632, 12124160, 8781824, 12648448, 1900544, 10354688, + 14745600, 16252928, 9961472, 1114112, 6881280, 14221312, 9306112, 9699328, + 10158080, 1966080, 8847360, 15269888, 13500416, 5570560, 2621440, 14614528, + 9175040, 10551296, 8978432, 851968, 12517376, 15073280, 4325376, 6815744, + 4259840, 10027008, 2949120, 983040, 11534336, 5505024, 12255232, 1441792, + + // s_TF4 + 1660944384, 2080374784, 1996488704, 2063597568, -234881024, 1795162112, 1862270976, -989855744, + 805306368, 16777216, 1728053248, 721420288, -33554432, -687865856, -1426063360, 1979711488, + -905969664, -2113929216, -922746880, 2097152000, -100663296, 1493172224, 1191182336, -268435456, + -1392508928, -738197504, -1577058304, -1358954496, -1677721600, -1543503872, 1912602624, -1073741824, + -1224736768, -50331648, -1828716544, 637534208, 905969664, 1056964608, -150994944, -872415232, + 872415232, -1526726656, -452984832, -251658240, 1895825408, -671088640, 822083584, 352321536, + 67108864, -956301312, 587202560, -1023410176, 402653184, -1778384896, 83886080, -1711276032, + 117440512, 301989888, -2147483648, -503316480, -352321536, 654311424, -1308622848, 1962934272, + 150994944, -2097152000, 738197504, 436207616, 452984832, 1845493760, 1509949440, -1610612736, + 1375731712, 989855744, -704643072, -1291845632, 687865856, -486539264, 788529152, -2080374784, + 1392508928, -788529152, 0, -318767104, 536870912, -67108864, -1325400064, 1526726656, + 1778384896, -889192448, -1107296256, 956301312, 1241513984, 1275068416, 1476395008, -822083584, + -805306368, -285212672, -1442840576, -83886080, 1124073472, 1291845632, 855638016, -2063597568, + 1157627904, -117440512, 33554432, 2130706432, 1342177280, 1006632960, -1627389952, -1476395008, + 1358954496, -1560281088, 1073741824, -1895825408, -1845493760, -1660944384, 939524096, -184549376, + -1140850688, -1241513984, -637534208, 553648128, 268435456, -16777216, -218103808, -771751936, + -855638016, 201326592, 318767104, -335544320, 1593835520, -1761607680, 1140850688, 385875968, + -1006632960, -1493172224, 2113929216, 1023410176, 1677721600, 1560281088, 419430400, 1929379840, + 1610612736, -2130706432, 1325400064, -603979776, 570425344, 704643072, -1879048192, -2013265920, + 1174405120, -301989888, -1207959552, 335544320, -570425344, 1577058304, 184549376, -620756992, + -536870912, 838860800, 973078528, 167772160, 1224736768, 100663296, 603979776, 1543503872, + -1040187392, -754974720, -1409286144, 1644167168, -1862270976, -1795162112, -469762048, 2030043136, + -419430400, -939524096, 922746880, 1828716544, -1929379840, -721420288, 1308622848, -1459617792, + 1811939328, 1442840576, -201326592, -369098752, 1694498816, 2046820352, -1375731712, 134217728, + -1174405120, 2013265920, 620756992, 771751936, 469762048, -1509949440, -1275068416, -973078528, + -402653184, -587202560, 1946157056, 520093696, 1258291200, -1124073472, -1962934272, -1979711488, + 1879048192, 1040187392, -1258291200, 1711276032, 1207959552, 50331648, -167772160, 234881024, + 1627389952, 889192448, 1459617792, -1191182336, -2046820352, -1056964608, 486539264, -1644167168, + -520093696, -134217728, -1744830464, 285212672, 1761607680, -654311424, -1912602624, -1811939328, + -1694498816, 503316480, -2030043136, -385875968, -838860800, 1426063360, 671088640, -553648128, + -1946157056, -1593835520, -1996488704, 218103808, -1090519040, -436207616, 1107296256, 1744830464, + 1090519040, -1728053248, 754974720, 251658240, -1342177280, 1409286144, -1157627904, 369098752 }; + + private static readonly int[] s_iT = new int[4 * 256] + { + // s_iT1 + 1353184337, 1399144830, -1012656358, -1772214470, -882136261, -247096033, -1420232020, -1828461749, + 1442459680, -160598355, -1854485368, 625738485, -52959921, -674551099, -2143013594, -1885117771, + 1230680542, 1729870373, -1743852987, -507445667, 41234371, 317738113, -1550367091, -956705941, + -413167869, -1784901099, -344298049, -631680363, 763608788, -752782248, 694804553, 1154009486, + 1787413109, 2021232372, 1799248025, -579749593, -1236278850, 397248752, 1722556617, -1271214467, + 407560035, -2110711067, 1613975959, 1165972322, -529046351, -2068943941, 480281086, -1809118983, + 1483229296, 436028815, -2022908268, -1208452270, 601060267, -503166094, 1468997603, 715871590, + 120122290, 63092015, -1703164538, -1526188077, -226023376, -1297760477, -1167457534, 1552029421, + 723308426, -1833666137, -252573709, -1578997426, -839591323, -708967162, 526529745, -1963022652, + -1655493068, -1604979806, 853641733, 1978398372, 971801355, -1427152832, 111112542, 1360031421, + -108388034, 1023860118, -1375387939, 1186850381, -1249028975, 90031217, 1876166148, -15380384, + 620468249, -1746289194, -868007799, 2006899047, -1119688528, -2004121337, 945494503, -605108103, + 1191869601, -384875908, -920746760, 0, -2088337399, 1223502642, -1401941730, 1316117100, + -67170563, 1446544655, 517320253, 658058550, 1691946762, 564550760, -783000677, 976107044, + -1318647284, 266819475, -761860428, -1634624741, 1338359936, -1574904735, 1766553434, 370807324, + 179999714, -450191168, 1138762300, 488053522, 185403662, -1379431438, -1180125651, -928440812, + -2061897385, 1275557295, -1143105042, -44007517, -1624899081, -1124765092, -985962940, 880737115, + 1982415755, -590994485, 1761406390, 1676797112, -891538985, 277177154, 1076008723, 538035844, + 2099530373, -130171950, 288553390, 1839278535, 1261411869, -214912292, -330136051, -790380169, + 1813426987, -1715900247, -95906799, 577038663, -997393240, 440397984, -668172970, -275762398, + -951170681, -1043253031, -22885748, 906744984, -813566554, 685669029, 646887386, -1530942145, + -459458004, 227702864, -1681105046, 1648787028, -1038905866, -390539120, 1593260334, -173030526, + -1098883681, 2090061929, -1456614033, -1290656305, 999926984, -1484974064, 1852021992, 2075868123, + 158869197, -199730834, 28809964, -1466282109, 1701746150, 2129067946, 147831841, -420997649, + -644094022, -835293366, -737566742, -696471511, -1347247055, 824393514, 815048134, -1067015627, + 935087732, -1496677636, -1328508704, 366520115, 1251476721, -136647615, 240176511, 804688151, + -1915335306, 1303441219, 1414376140, -553347356, -474623586, 461924940, -1205916479, 2136040774, + 82468509, 1563790337, 1937016826, 776014843, 1511876531, 1389550482, 861278441, 323475053, + -1939744870, 2047648055, -1911228327, -1992551445, -299390514, 902390199, -303751967, 1018251130, + 1507840668, 1064563285, 2043548696, -1086863501, -355600557, 1537932639, 342834655, -2032450440, + -2114736182, 1053059257, 741614648, 1598071746, 1925389590, 203809468, -1958134744, 1100287487, + 1895934009, -558691320, -1662733096, -1866377628, 1636092795, 1890988757, 1952214088, 1113045200, + + // s_iT2 + -1477160624, 1698790995, -1541989693, 1579629206, 1806384075, 1167925233, 1492823211, 65227667, + -97509291, 1836494326, 1993115793, 1275262245, -672837636, -886389289, 1144333952, -1553812081, + 1521606217, 465184103, 250234264, -1057071647, 1966064386, -263421678, -1756983901, -103584826, + 1603208167, -1668147819, 2054012907, 1498584538, -2084645843, 561273043, 1776306473, -926314940, + -1983744662, 2039411832, 1045993835, 1907959773, 1340194486, -1383534569, -1407137434, 986611124, + 1256153880, 823846274, 860985184, 2136171077, 2003087840, -1368671356, -1602093540, 722008468, + 1749577816, -45773031, 1826526343, -126135625, -747394269, 38499042, -1893735593, -1420466646, + 686535175, -1028313341, 2076542618, 137876389, -2027409166, -1514200142, 1778582202, -2112426660, + 483363371, -1267095662, -234359824, -496415071, -187013683, -1106966827, 1647628575, -22625142, + 1395537053, 1442030240, -511048398, -336157579, -326956231, -278904662, -1619960314, 275692881, + -1977532679, 115185213, 88006062, -1108980410, -1923837515, 1573155077, -737803153, 357589247, + -73918172, -373434729, 1128303052, -1629919369, 1122545853, -1953953912, 1528424248, -288851493, + 175939911, 256015593, 512030921, 0, -2038429309, -315936184, 1880170156, 1918528590, + -15794693, 948244310, -710001378, 959264295, -653325724, -1503893471, 1415289809, 775300154, + 1728711857, -413691121, -1762741038, -1852105826, -977239985, 551313826, 1266113129, 437394454, + -1164713462, 715178213, -534627261, 387650077, 218697227, -947129683, -1464455751, -1457646392, + 435246981, 125153100, -577114437, 1618977789, 637663135, -177054532, 996558021, 2130402100, + 692292470, -970732580, -51530136, -236668829, -600713270, -2057092592, 580326208, 298222624, + 608863613, 1035719416, 855223825, -1591097491, 798891339, 817028339, 1384517100, -473860144, + 380840812, -1183798887, 1217663482, 1693009698, -1929598780, 1072734234, 746411736, -1875696913, + 1313441735, -784803391, -1563783938, 198481974, -2114607409, -562387672, -1900553690, -1079165020, + -1657131804, -1837608947, -866162021, 1182684258, 328070850, -1193766680, -147247522, -1346141451, + -2141347906, -1815058052, 768962473, 304467891, -1716729797, 2098729127, 1671227502, -1153705093, + 2015808777, 408514292, -1214583807, -1706064984, 1855317605, -419452290, -809754360, -401215514, + -1679312167, 913263310, 161475284, 2091919830, -1297862225, 591342129, -1801075152, 1721906624, + -1135709129, -897385306, -795811664, -660131051, -1744506550, -622050825, 1355644686, -158263505, + -699566451, -1326496947, 1303039060, 76997855, -1244553501, -2006299621, 523026872, 1365591679, + -362898172, 898367837, 1955068531, 1091304238, 493335386, -757362094, 1443948851, 1205234963, + 1641519756, 211892090, 351820174, 1007938441, 665439982, -916342987, -451091987, -1320715716, + -539845543, 1945261375, -837543815, 935818175, -839429142, -1426235557, 1866325780, -616269690, + -206583167, -999769794, 874788908, 1084473951, -1021503886, 635616268, 1228679307, -1794244799, + 27801969, -1291056930, -457910116, -1051302768, -2067039391, -1238182544, 1550600308, 1471729730, + + // s_iT3 + -195997529, 1098797925, 387629988, 658151006, -1422144661, -1658851003, -89347240, -481586429, + 807425530, 1991112301, -863465098, 49620300, -447742761, 717608907, 891715652, 1656065955, + -1310832294, -1171953893, -364537842, -27401792, 801309301, 1283527408, 1183687575, -747911431, + -1895569569, -1844079204, 1841294202, 1385552473, -1093390973, 1951978273, -532076183, -913423160, + -1032492407, -1896580999, 1486449470, -1188569743, -507595185, -1997531219, 550069932, -830622662, + -547153846, 451248689, 1368875059, 1398949247, 1689378935, 1807451310, -2114052960, 150574123, + 1215322216, 1167006205, -560691348, 2069018616, 1940595667, 1265820162, 534992783, 1432758955, + -340654296, -1255210046, -981034373, 936617224, 674296455, -1088179547, 50510442, 384654466, + -813028580, 2041025204, 133427442, 1766760930, -630862348, 84334014, 886120290, -1497068802, + 775200083, -207445931, -1979370783, -156994069, -2096416276, 1614850799, 1901987487, 1857900816, + 557775242, -577356538, 1054715397, -431143235, 1418835341, -999226019, 100954068, 1348534037, + -1743182597, -1110009879, 1082772547, -647530594, -391070398, -1995994997, 434583643, -931537938, + 2090944266, 1115482383, -2064070370, 0, -2146860154, 724715757, 287222896, 1517047410, + 251526143, -2062592456, -1371726123, 758523705, 252339417, 1550328230, 1536938324, 908343854, + 168604007, 1469255655, -290139498, -1692688751, -1065332795, -597581280, 2002413899, 303830554, + -1813902662, -1597971158, 574374880, 454171927, 151915277, -1947030073, -1238517336, 504678569, + -245922535, 1974422535, -1712407587, 2141453664, 33005350, 1918680309, 1715782971, -77908866, + 1133213225, 600562886, -306812676, -457677839, 836225756, 1665273989, -1760346078, -964419567, + 1250262308, -1143801795, -106032846, 700935585, -1642247377, -1294142672, -2045907886, -1049112349, + -1288999914, 1890163129, -1810761144, -381214108, -56048500, -257942977, 2102843436, 857927568, + 1233635150, 953795025, -896729438, -728222197, -173617279, 2057644254, -1210440050, -1388337985, + 976020637, 2018512274, 1600822220, 2119459398, -1913208301, -661591880, 959340279, -1014827601, + 1570750080, -798393197, -714102483, 634368786, -1396163687, 403744637, -1662488989, 1004239803, + 650971512, 1500443672, -1695809097, 1334028442, -1780062866, -5603610, -1138685745, 368043752, + -407184997, 1867173430, -1612000247, -1339435396, -1540247630, 1059729699, -1513738092, -1573535642, + 1316239292, -2097371446, -1864322864, -1489824296, 82922136, -331221030, -847311280, -1860751370, + 1299615190, -280801872, -1429449651, -1763385596, -778116171, 1783372680, 750893087, 1699118929, + 1587348714, -1946067659, -2013629580, 201010753, 1739807261, -611167534, 283718486, -697494713, + -677737375, -1590199796, -128348652, 334203196, -1446056409, 1639396809, 484568549, 1199193265, + -761505313, -229294221, 337148366, -948715721, -145495347, -44082262, 1038029935, 1148749531, + -1345682957, 1756970692, 607661108, -1547542720, 488010435, -490992603, 1009290057, 234832277, + -1472630527, 201907891, -1260872476, 1449431233, -881106556, 852848822, 1816687708, -1194311081, + + // s_iT4 + 1364240372, 2119394625, 449029143, 982933031, 1003187115, 535905693, -1398056710, 1267925987, + 542505520, -1376359050, -2003732788, -182105086, 1341970405, -975713494, 645940277, -1248877726, + -565617999, 627514298, 1167593194, 1575076094, -1023249105, -2129465268, -1918658746, 1808202195, + 65494927, 362126482, -1075086739, -1780852398, -735214658, 1490231668, 1227450848, -1908094775, + 1969916354, -193431154, -1721024936, 668823993, -1095348255, -266883704, -916018144, 2108963534, + 1662536415, -444452582, -1755303087, 1648721747, -1310689436, -1148932501, -31678335, -107730168, + 1884842056, -1894122171, -1803064098, 1387788411, -1423715469, 1927414347, -480800993, 1714072405, + -1308153621, 788775605, -2036696123, -744159177, 821200680, 598910399, 45771267, -312704490, + -1976886065, -1483557767, -202313209, 1319232105, 1707996378, 114671109, -786472396, -997523802, + 882725678, -1566550541, 87220618, -1535775754, 188345475, 1084944224, 1577492337, -1118760850, + 1056541217, -1774385443, -575797954, 1296481766, -1850372780, 1896177092, 74437638, 1627329872, + 421854104, -694687299, -1983102144, 1735892697, -1329773848, 126389129, -415737063, 2044456648, + -1589179780, 2095648578, -121037180, 0, 159614592, 843640107, 514617361, 1817080410, + -33816818, 257308805, 1025430958, 908540205, 174381327, 1747035740, -1680780197, 607792694, + 212952842, -1827674281, -1261267218, 463376795, -2142255680, 1638015196, 1516850039, 471210514, + -502613357, -1058723168, 1011081250, 303896347, 235605257, -223492213, 767142070, 348694814, + 1468340721, -1353971851, -289677927, -1543675777, -140564991, 1555887474, 1153776486, 1530167035, + -1955190461, -874723805, -1234633491, -1201409564, -674571215, 1108378979, 322970263, -2078273082, + -2055396278, -755483205, -1374604551, -949116631, 491466654, -588042062, 233591430, 2010178497, + 728503987, -1449543312, 301615252, 1193436393, -1463513860, -1608892432, 1457007741, 586125363, + -2016981431, -641609416, -1929469238, -1741288492, -1496350219, -1524048262, -635007305, 1067761581, + 753179962, 1343066744, 1788595295, 1415726718, -155053171, -1863796520, 777975609, -2097827901, + -1614905251, 1769771984, 1873358293, -810347995, -935618132, 279411992, -395418724, -612648133, + -855017434, 1861490777, -335431782, -2086102449, -429560171, -1434523905, 554225596, -270079979, + -1160143897, 1255028335, -355202657, 701922480, 833598116, 707863359, -969894747, 901801634, + 1949809742, -56178046, -525283184, 857069735, -246769660, 1106762476, 2131644621, 389019281, + 1989006925, 1129165039, -866890326, -455146346, -1629243951, 1276872810, -1044898004, 1182749029, + -1660622242, 22885772, -93096825, -80854773, -1285939865, -1840065829, -382511600, 1829980118, + -1702075945, 930745505, 1502483704, -343327725, -823253079, -1221211807, -504503012, 2050797895, + -1671831598, 1430221810, 410635796, 1941911495, 1407897079, 1599843069, -552308931, 2022103876, + -897453137, -1187068824, 942421028, -1033944925, 376619805, -1140054558, 680216892, -12479219, + 963707304, 148812556, -660806476, 1687208278, 2069988555, -714033614, 1215585388, -800958536 }; + + private static readonly int[] s_iTF = new int[4 * 256] + { + // s_iTF1 + 82, 9, 106, 213, 48, 54, 165, 56, + 191, 64, 163, 158, 129, 243, 215, 251, + 124, 227, 57, 130, 155, 47, 255, 135, + 52, 142, 67, 68, 196, 222, 233, 203, + 84, 123, 148, 50, 166, 194, 35, 61, + 238, 76, 149, 11, 66, 250, 195, 78, + 8, 46, 161, 102, 40, 217, 36, 178, + 118, 91, 162, 73, 109, 139, 209, 37, + 114, 248, 246, 100, 134, 104, 152, 22, + 212, 164, 92, 204, 93, 101, 182, 146, + 108, 112, 72, 80, 253, 237, 185, 218, + 94, 21, 70, 87, 167, 141, 157, 132, + 144, 216, 171, 0, 140, 188, 211, 10, + 247, 228, 88, 5, 184, 179, 69, 6, + 208, 44, 30, 143, 202, 63, 15, 2, + 193, 175, 189, 3, 1, 19, 138, 107, + 58, 145, 17, 65, 79, 103, 220, 234, + 151, 242, 207, 206, 240, 180, 230, 115, + 150, 172, 116, 34, 231, 173, 53, 133, + 226, 249, 55, 232, 28, 117, 223, 110, + 71, 241, 26, 113, 29, 41, 197, 137, + 111, 183, 98, 14, 170, 24, 190, 27, + 252, 86, 62, 75, 198, 210, 121, 32, + 154, 219, 192, 254, 120, 205, 90, 244, + 31, 221, 168, 51, 136, 7, 199, 49, + 177, 18, 16, 89, 39, 128, 236, 95, + 96, 81, 127, 169, 25, 181, 74, 13, + 45, 229, 122, 159, 147, 201, 156, 239, + 160, 224, 59, 77, 174, 42, 245, 176, + 200, 235, 187, 60, 131, 83, 153, 97, + 23, 43, 4, 126, 186, 119, 214, 38, + 225, 105, 20, 99, 85, 33, 12, 125, + + // s_iTF2 + 20992, 2304, 27136, 54528, 12288, 13824, 42240, 14336, + 48896, 16384, 41728, 40448, 33024, 62208, 55040, 64256, + 31744, 58112, 14592, 33280, 39680, 12032, 65280, 34560, + 13312, 36352, 17152, 17408, 50176, 56832, 59648, 51968, + 21504, 31488, 37888, 12800, 42496, 49664, 8960, 15616, + 60928, 19456, 38144, 2816, 16896, 64000, 49920, 19968, + 2048, 11776, 41216, 26112, 10240, 55552, 9216, 45568, + 30208, 23296, 41472, 18688, 27904, 35584, 53504, 9472, + 29184, 63488, 62976, 25600, 34304, 26624, 38912, 5632, + 54272, 41984, 23552, 52224, 23808, 25856, 46592, 37376, + 27648, 28672, 18432, 20480, 64768, 60672, 47360, 55808, + 24064, 5376, 17920, 22272, 42752, 36096, 40192, 33792, + 36864, 55296, 43776, 0, 35840, 48128, 54016, 2560, + 63232, 58368, 22528, 1280, 47104, 45824, 17664, 1536, + 53248, 11264, 7680, 36608, 51712, 16128, 3840, 512, + 49408, 44800, 48384, 768, 256, 4864, 35328, 27392, + 14848, 37120, 4352, 16640, 20224, 26368, 56320, 59904, + 38656, 61952, 52992, 52736, 61440, 46080, 58880, 29440, + 38400, 44032, 29696, 8704, 59136, 44288, 13568, 34048, + 57856, 63744, 14080, 59392, 7168, 29952, 57088, 28160, + 18176, 61696, 6656, 28928, 7424, 10496, 50432, 35072, + 28416, 46848, 25088, 3584, 43520, 6144, 48640, 6912, + 64512, 22016, 15872, 19200, 50688, 53760, 30976, 8192, + 39424, 56064, 49152, 65024, 30720, 52480, 23040, 62464, + 7936, 56576, 43008, 13056, 34816, 1792, 50944, 12544, + 45312, 4608, 4096, 22784, 9984, 32768, 60416, 24320, + 24576, 20736, 32512, 43264, 6400, 46336, 18944, 3328, + 11520, 58624, 31232, 40704, 37632, 51456, 39936, 61184, + 40960, 57344, 15104, 19712, 44544, 10752, 62720, 45056, + 51200, 60160, 47872, 15360, 33536, 21248, 39168, 24832, + 5888, 11008, 1024, 32256, 47616, 30464, 54784, 9728, + 57600, 26880, 5120, 25344, 21760, 8448, 3072, 32000, + + // s_iTF3 + 5373952, 589824, 6946816, 13959168, 3145728, 3538944, 10813440, 3670016, + 12517376, 4194304, 10682368, 10354688, 8454144, 15925248, 14090240, 16449536, + 8126464, 14876672, 3735552, 8519680, 10158080, 3080192, 16711680, 8847360, + 3407872, 9306112, 4390912, 4456448, 12845056, 14548992, 15269888, 13303808, + 5505024, 8060928, 9699328, 3276800, 10878976, 12713984, 2293760, 3997696, + 15597568, 4980736, 9764864, 720896, 4325376, 16384000, 12779520, 5111808, + 524288, 3014656, 10551296, 6684672, 2621440, 14221312, 2359296, 11665408, + 7733248, 5963776, 10616832, 4784128, 7143424, 9109504, 13697024, 2424832, + 7471104, 16252928, 16121856, 6553600, 8781824, 6815744, 9961472, 1441792, + 13893632, 10747904, 6029312, 13369344, 6094848, 6619136, 11927552, 9568256, + 7077888, 7340032, 4718592, 5242880, 16580608, 15532032, 12124160, 14286848, + 6160384, 1376256, 4587520, 5701632, 10944512, 9240576, 10289152, 8650752, + 9437184, 14155776, 11206656, 0, 9175040, 12320768, 13828096, 655360, + 16187392, 14942208, 5767168, 327680, 12058624, 11730944, 4521984, 393216, + 13631488, 2883584, 1966080, 9371648, 13238272, 4128768, 983040, 131072, + 12648448, 11468800, 12386304, 196608, 65536, 1245184, 9043968, 7012352, + 3801088, 9502720, 1114112, 4259840, 5177344, 6750208, 14417920, 15335424, + 9895936, 15859712, 13565952, 13500416, 15728640, 11796480, 15073280, 7536640, + 9830400, 11272192, 7602176, 2228224, 15138816, 11337728, 3473408, 8716288, + 14811136, 16318464, 3604480, 15204352, 1835008, 7667712, 14614528, 7208960, + 4653056, 15794176, 1703936, 7405568, 1900544, 2686976, 12910592, 8978432, + 7274496, 11993088, 6422528, 917504, 11141120, 1572864, 12451840, 1769472, + 16515072, 5636096, 4063232, 4915200, 12976128, 13762560, 7929856, 2097152, + 10092544, 14352384, 12582912, 16646144, 7864320, 13434880, 5898240, 15990784, + 2031616, 14483456, 11010048, 3342336, 8912896, 458752, 13041664, 3211264, + 11599872, 1179648, 1048576, 5832704, 2555904, 8388608, 15466496, 6225920, + 6291456, 5308416, 8323072, 11075584, 1638400, 11862016, 4849664, 851968, + 2949120, 15007744, 7995392, 10420224, 9633792, 13172736, 10223616, 15663104, + 10485760, 14680064, 3866624, 5046272, 11403264, 2752512, 16056320, 11534336, + 13107200, 15400960, 12255232, 3932160, 8585216, 5439488, 10027008, 6356992, + 1507328, 2818048, 262144, 8257536, 12189696, 7798784, 14024704, 2490368, + 14745600, 6881280, 1310720, 6488064, 5570560, 2162688, 786432, 8192000, + + // s_iTF4 + 1375731712, 150994944, 1778384896, -721420288, 805306368, 905969664, -1526726656, 939524096, + -1090519040, 1073741824, -1560281088, -1644167168, -2130706432, -218103808, -687865856, -83886080, + 2080374784, -486539264, 956301312, -2113929216, -1694498816, 788529152, -16777216, -2030043136, + 872415232, -1912602624, 1124073472, 1140850688, -1006632960, -570425344, -385875968, -889192448, + 1409286144, 2063597568, -1811939328, 838860800, -1509949440, -1040187392, 587202560, 1023410176, + -301989888, 1275068416, -1795162112, 184549376, 1107296256, -100663296, -1023410176, 1308622848, + 134217728, 771751936, -1593835520, 1711276032, 671088640, -654311424, 603979776, -1308622848, + 1979711488, 1526726656, -1577058304, 1224736768, 1828716544, -1962934272, -788529152, 620756992, + 1912602624, -134217728, -167772160, 1677721600, -2046820352, 1744830464, -1744830464, 369098752, + -738197504, -1543503872, 1543503872, -872415232, 1560281088, 1694498816, -1241513984, -1845493760, + 1811939328, 1879048192, 1207959552, 1342177280, -50331648, -318767104, -1191182336, -637534208, + 1577058304, 352321536, 1174405120, 1459617792, -1493172224, -1929379840, -1660944384, -2080374784, + -1879048192, -671088640, -1426063360, 0, -1946157056, -1140850688, -754974720, 167772160, + -150994944, -469762048, 1476395008, 83886080, -1207959552, -1291845632, 1157627904, 100663296, + -805306368, 738197504, 503316480, -1895825408, -905969664, 1056964608, 251658240, 33554432, + -1056964608, -1358954496, -1124073472, 50331648, 16777216, 318767104, -1979711488, 1795162112, + 973078528, -1862270976, 285212672, 1090519040, 1325400064, 1728053248, -603979776, -369098752, + -1761607680, -234881024, -822083584, -838860800, -268435456, -1275068416, -436207616, 1929379840, + -1778384896, -1409286144, 1946157056, 570425344, -419430400, -1392508928, 889192448, -2063597568, + -503316480, -117440512, 922746880, -402653184, 469762048, 1962934272, -553648128, 1845493760, + 1191182336, -251658240, 436207616, 1895825408, 486539264, 687865856, -989855744, -1996488704, + 1862270976, -1224736768, 1644167168, 234881024, -1442840576, 402653184, -1107296256, 452984832, + -67108864, 1442840576, 1040187392, 1258291200, -973078528, -771751936, 2030043136, 536870912, + -1711276032, -620756992, -1073741824, -33554432, 2013265920, -855638016, 1509949440, -201326592, + 520093696, -587202560, -1476395008, 855638016, -2013265920, 117440512, -956301312, 822083584, + -1325400064, 301989888, 268435456, 1493172224, 654311424, -2147483648, -335544320, 1593835520, + 1610612736, 1358954496, 2130706432, -1459617792, 419430400, -1258291200, 1241513984, 218103808, + 754974720, -452984832, 2046820352, -1627389952, -1828716544, -922746880, -1677721600, -285212672, + -1610612736, -536870912, 989855744, 1291845632, -1375731712, 704643072, -184549376, -1342177280, + -939524096, -352321536, -1157627904, 1006632960, -2097152000, 1392508928, -1728053248, 1627389952, + 385875968, 721420288, 67108864, 2113929216, -1174405120, 1996488704, -704643072, 637534208, + -520093696, 1761607680, 335544320, 1660944384, 1426063360, 553648128, 201326592, 2097152000 }; + } +} diff --git a/PspCrypto/SHA224Managed_OLD.cs b/PspCrypto/SHA224Managed_OLD.cs new file mode 100644 index 0000000..81bb30a --- /dev/null +++ b/PspCrypto/SHA224Managed_OLD.cs @@ -0,0 +1,272 @@ +using System; +using System.Collections.Generic; +using System.Text; +using PspCrypto.Security.Cryptography; + +namespace PspCrypto +{ + public class SHA224Managed_OLD : SHA224 + { + + private const int BLOCK_SIZE_BYTES = 64; + + private uint[] _H; + private ulong count; + private byte[] _ProcessingBuffer; // Used to start data when passed less than a block worth. + private int _ProcessingBufferCount; // Counts how much data we have stored that still needs processed. + private uint[] buff; + + public SHA224Managed_OLD() + { + _H = new uint[8]; + _ProcessingBuffer = new byte[BLOCK_SIZE_BYTES]; + buff = new uint[64]; + Initialize(); + } + + private uint Ch(uint u, uint v, uint w) + { + return (u & v) ^ (~u & w); + } + + private uint Maj(uint u, uint v, uint w) + { + return (u & v) ^ (u & w) ^ (v & w); + } + + private uint Ro0(uint x) + { + return ((x >> 7) | (x << 25)) + ^ ((x >> 18) | (x << 14)) + ^ (x >> 3); + } + + private uint Ro1(uint x) + { + return ((x >> 17) | (x << 15)) + ^ ((x >> 19) | (x << 13)) + ^ (x >> 10); + } + + private uint Sig0(uint x) + { + return ((x >> 2) | (x << 30)) + ^ ((x >> 13) | (x << 19)) + ^ ((x >> 22) | (x << 10)); + } + + private uint Sig1(uint x) + { + return ((x >> 6) | (x << 26)) + ^ ((x >> 11) | (x << 21)) + ^ ((x >> 25) | (x << 7)); + } + + + public void HashData2(ReadOnlySpan data, Span hash) + { + var tmp = ComputeHash(data.ToArray()); + tmp.CopyTo(hash); + } + + protected override void HashCore(byte[] rgb, int start, int size) + { + int i; + State = 1; + + if (_ProcessingBufferCount != 0) + { + if (size < (BLOCK_SIZE_BYTES - _ProcessingBufferCount)) + { + System.Buffer.BlockCopy(rgb, start, _ProcessingBuffer, _ProcessingBufferCount, size); + _ProcessingBufferCount += size; + return; + } + else + { + i = (BLOCK_SIZE_BYTES - _ProcessingBufferCount); + System.Buffer.BlockCopy(rgb, start, _ProcessingBuffer, _ProcessingBufferCount, i); + ProcessBlock(_ProcessingBuffer, 0); + _ProcessingBufferCount = 0; + start += i; + size -= i; + } + } + + for (i = 0; i < size - size % BLOCK_SIZE_BYTES; i += BLOCK_SIZE_BYTES) + { + ProcessBlock(rgb, start + i); + } + + if (size % BLOCK_SIZE_BYTES != 0) + { + System.Buffer.BlockCopy(rgb, size - size % BLOCK_SIZE_BYTES + start, _ProcessingBuffer, 0, size % BLOCK_SIZE_BYTES); + _ProcessingBufferCount = size % BLOCK_SIZE_BYTES; + } + } + + protected override byte[] HashFinal() + { + byte[] hash = new byte[28]; + int i, j; + + ProcessFinalBlock(_ProcessingBuffer, 0, _ProcessingBufferCount); + + for (i = 0; i < 7; i++) + { + for (j = 0; j < 4; j++) + { + hash[i * 4 + j] = (byte)(_H[i] >> (24 - j * 8)); + } + } + + State = 0; + return hash; + } + + public override void Initialize() + { + count = 0; + _ProcessingBufferCount = 0; + + _H[0] = 0xC1059ED8; + _H[1] = 0x367CD507; + _H[2] = 0x3070DD17; + _H[3] = 0xF70E5939; + _H[4] = 0xFFC00B31; + _H[5] = 0x68581511; + _H[6] = 0x64F98FA7; + _H[7] = 0xBEFA4FA4; + } + + private void ProcessBlock(byte[] inputBuffer, int inputOffset) + { + uint a, b, c, d, e, f, g, h; + uint t1, t2; + int i; + uint[] K1 = _K1; + uint[] buff = this.buff; + + count += BLOCK_SIZE_BYTES; + + for (i = 0; i < 16; i++) + { + buff[i] = (uint)(((inputBuffer[inputOffset + 4 * i]) << 24) + | ((inputBuffer[inputOffset + 4 * i + 1]) << 16) + | ((inputBuffer[inputOffset + 4 * i + 2]) << 8) + | ((inputBuffer[inputOffset + 4 * i + 3]))); + } + + + for (i = 16; i < 64; i++) + { + t1 = buff[i - 15]; + t1 = (((t1 >> 7) | (t1 << 25)) ^ ((t1 >> 18) | (t1 << 14)) ^ (t1 >> 3)); + + t2 = buff[i - 2]; + t2 = (((t2 >> 17) | (t2 << 15)) ^ ((t2 >> 19) | (t2 << 13)) ^ (t2 >> 10)); + buff[i] = t2 + buff[i - 7] + t1 + buff[i - 16]; + } + + a = _H[0]; + b = _H[1]; + c = _H[2]; + d = _H[3]; + e = _H[4]; + f = _H[5]; + g = _H[6]; + h = _H[7]; + + for (i = 0; i < 64; i++) + { + t1 = h + (((e >> 6) | (e << 26)) ^ ((e >> 11) | (e << 21)) ^ ((e >> 25) | (e << 7))) + ((e & f) ^ (~e & g)) + K1[i] + buff[i]; + + t2 = (((a >> 2) | (a << 30)) ^ ((a >> 13) | (a << 19)) ^ ((a >> 22) | (a << 10))); + t2 = t2 + ((a & b) ^ (a & c) ^ (b & c)); + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + + _H[0] += a; + _H[1] += b; + _H[2] += c; + _H[3] += d; + _H[4] += e; + _H[5] += f; + _H[6] += g; + _H[7] += h; + } + + private void ProcessFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount) + { + ulong total = count + (ulong)inputCount; + int paddingSize = (56 - (int)(total % BLOCK_SIZE_BYTES)); + + if (paddingSize < 1) + paddingSize += BLOCK_SIZE_BYTES; + + byte[] fooBuffer = new byte[inputCount + paddingSize + 8]; + + for (int i = 0; i < inputCount; i++) + { + fooBuffer[i] = inputBuffer[i + inputOffset]; + } + + fooBuffer[inputCount] = 0x80; + for (int i = inputCount + 1; i < inputCount + paddingSize; i++) + { + fooBuffer[i] = 0x00; + } + + // I deal in bytes. The algorithm deals in bits. + ulong size = total << 3; + AddLength(size, fooBuffer, inputCount + paddingSize); + ProcessBlock(fooBuffer, 0); + + if (inputCount + paddingSize + 8 == 128) + { + ProcessBlock(fooBuffer, 64); + } + } + + internal void AddLength(ulong length, byte[] buffer, int position) + { + buffer[position++] = (byte)(length >> 56); + buffer[position++] = (byte)(length >> 48); + buffer[position++] = (byte)(length >> 40); + buffer[position++] = (byte)(length >> 32); + buffer[position++] = (byte)(length >> 24); + buffer[position++] = (byte)(length >> 16); + buffer[position++] = (byte)(length >> 8); + buffer[position] = (byte)(length); + } + + // SHA-224/256 Constants + // Represent the first 32 bits of the fractional parts of the + // cube roots of the first sixty-four prime numbers + public readonly static uint[] _K1 = { + 0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, + 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, + 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, + 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, + 0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, + 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, + 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, + 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967, + 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, + 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, + 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, + 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, + 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, + 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, + 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, + 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2 + }; + } +} diff --git a/PspCrypto/SafeHandles/SafeBignumHandle.cs b/PspCrypto/SafeHandles/SafeBignumHandle.cs new file mode 100644 index 0000000..36bbe94 --- /dev/null +++ b/PspCrypto/SafeHandles/SafeBignumHandle.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace ECDsaTest.SafeHandles +{ + internal sealed class SafeBignumHandle : SafeHandle + { + private SafeBignumHandle() : + base(IntPtr.Zero, ownsHandle: true) + { + } + + internal SafeBignumHandle(IntPtr handle, bool ownsHandle) + : base(handle, ownsHandle) + { + } + + protected override bool ReleaseHandle() + { + Interop.Crypto.BigNumDestroy(handle); + SetHandle(IntPtr.Zero); + return true; + } + + public override bool IsInvalid + { + get { return handle == IntPtr.Zero; } + } + } +} diff --git a/PspCrypto/SafeHandles/SafeEcKeyHandle.cs b/PspCrypto/SafeHandles/SafeEcKeyHandle.cs new file mode 100644 index 0000000..1057bae --- /dev/null +++ b/PspCrypto/SafeHandles/SafeEcKeyHandle.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; + +namespace ECDsaTest.SafeHandles +{ + internal sealed class SafeEcKeyHandle : SafeHandle + { + private SafeEcKeyHandle() : + base(IntPtr.Zero, ownsHandle: true) + { + } + + protected override bool ReleaseHandle() + { + Interop.Crypto.EcKeyDestroy(handle); + SetHandle(IntPtr.Zero); + return true; + } + + public override bool IsInvalid + { + get { return handle == IntPtr.Zero; } + } + + internal static SafeEcKeyHandle DuplicateHandle(IntPtr handle) + { + Debug.Assert(handle != IntPtr.Zero); + + // Reliability: Allocate the SafeHandle before calling EC_KEY_up_ref so + // that we don't lose a tracked reference in low-memory situations. + SafeEcKeyHandle safeHandle = new SafeEcKeyHandle(); + + if (!Interop.Crypto.EcKeyUpRef(handle)) + { + throw Interop.Crypto.CreateOpenSslCryptographicException(); + } + + safeHandle.SetHandle(handle); + return safeHandle; + } + } +} diff --git a/PspCrypto/SceDdrdb.cs b/PspCrypto/SceDdrdb.cs new file mode 100644 index 0000000..dacd1c7 --- /dev/null +++ b/PspCrypto/SceDdrdb.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace PspCrypto +{ + public static class SceDdrdb + { + public static int sceDdrdbHash(ReadOnlySpan src, int size, Span digest) + { + SHA1.HashData(src[..size], digest); + return 0; + } + + public static int sceDdrdbSigvry(ReadOnlySpan pubKey, ReadOnlySpan hash, ReadOnlySpan sig) + { + Span buff = stackalloc byte[Marshal.SizeOf()]; + ref KIRKEngine.KIRK_CMD17_BUFFER buffer = ref MemoryMarshal.AsRef(buff); + pubKey[..0x28].CopyTo(buffer.public_key.point); + hash[..20].CopyTo(buffer.message_hash); + sig[..0x28].CopyTo(buffer.signature.sig); + return KIRKEngine.sceUtilsBufferCopyWithRange(null, 0, buff, 100, KIRKEngine.KIRK_CMD_ECDSA_VERIFY); + } + } +} diff --git a/PspCrypto/SceMemlmd.cs b/PspCrypto/SceMemlmd.cs new file mode 100644 index 0000000..2f831c2 --- /dev/null +++ b/PspCrypto/SceMemlmd.cs @@ -0,0 +1,513 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace PspCrypto +{ + public static class SceMemlmd + { + private static readonly Memory MemlmdKirkMemory = new byte[0x150]; // DAT_00002604 + private static readonly Memory MemlmdMem_BFC00000 = new byte[0x3000]; + private static readonly Memory MemlmdMem_BFC00280 = MemlmdMem_BFC00000.Slice(0x280, 0xC0); + private static readonly Memory MemlmdMem_BFC00340 = MemlmdMem_BFC00000.Slice(0x340, 0x300); + private static readonly Memory MemlmdMem_BFC00A00 = MemlmdMem_BFC00000.Slice(0xA00, 0x200); + private static readonly Memory MemlmdMem_2780 = new byte[0x200]; + + private static readonly byte[] key7A90 = + { + 0x77, 0x3F, 0x4B, 0xE1, 0x4C, 0x0A, 0xB4, 0x52, 0x67, 0x2B, 0x67, 0x56, 0x82, 0x4C, 0xCF, 0x42, + 0xAA, 0x37, 0xFF, 0xC0, 0x89, 0x41, 0xE5, 0x63, 0x5E, 0x84, 0xE9, 0xFB, 0x53, 0xDA, 0x94, 0x9E, + 0x9B, 0xB7, 0xC2, 0xA4, 0x22, 0x9F, 0xDF, 0x1F + }; + internal static readonly byte[] key_4C940AF0 = { 0xA8, 0xB1, 0x47, 0x77, 0xDC, 0x49, 0x6A, 0x6F, 0x38, 0x4C, 0x4D, 0x96, 0xBD, 0x49, 0xEC, 0x9B }; + internal static readonly byte[] key_4C940BF0 = { 0x3B, 0x9B, 0x1A, 0x56, 0x21, 0x80, 0x14, 0xED, 0x8E, 0x8B, 0x08, 0x42, 0xFA, 0x2C, 0xDC, 0x3A }; + internal static readonly byte[] key_4C9410F0 = { 0x31, 0x1F, 0x98, 0xD5, 0x7B, 0x58, 0x95, 0x45, 0x32, 0xAB, 0x3A, 0xE3, 0x89, 0x32, 0x4B, 0x34 }; + internal static readonly byte[] key_4C9412F0 = { 0x26, 0x38, 0x0A, 0xAC, 0xA5, 0xD8, 0x74, 0xD1, 0x32, 0xB7, 0x2A, 0xBF, 0x79, 0x9E, 0x6D, 0xDB }; + internal static readonly byte[] key_4C9413F0 = { 0x53, 0xE7, 0xAB, 0xB9, 0xC6, 0x4A, 0x4B, 0x77, 0x92, 0x17, 0xB5, 0x74, 0x0A, 0xDA, 0xA9, 0xEA }; + internal static readonly byte[] key_4C9414F0 = { 0x45, 0xEF, 0x5C, 0x5D, 0xED, 0x81, 0x99, 0x84, 0x12, 0x94, 0x8F, 0xAB, 0xE8, 0x05, 0x6D, 0x7D }; + internal static readonly byte[] key_4C9415F0 = { 0x70, 0x1B, 0x08, 0x25, 0x22, 0xA1, 0x4D, 0x3B, 0x69, 0x21, 0xF9, 0x71, 0x0A, 0xA8, 0x41, 0xA9 }; + internal static readonly byte[] key_4C949AF0 = { 0x48, 0x58, 0xAA, 0x38, 0x78, 0x9A, 0x6C, 0x0D, 0x42, 0xEA, 0xC8, 0x19, 0x23, 0x34, 0x4D, 0xF0 }; + internal static readonly byte[] key_4C949BF0 = { 0x20, 0x00, 0x5B, 0x67, 0x48, 0x77, 0x02, 0x60, 0xCF, 0x0C, 0xAB, 0x7E, 0xAE, 0x0C, 0x55, 0xA1 }; + internal static readonly byte[] key_4C949CF0 = { 0x3F, 0x67, 0x09, 0xA1, 0x47, 0x71, 0xD6, 0x9E, 0x27, 0x7C, 0x7B, 0x32, 0x67, 0x0E, 0x65, 0x8A }; + internal static readonly byte[] key_4C949DF0 = { 0x9B, 0x92, 0x99, 0x91, 0xA2, 0xE8, 0xAA, 0x4A, 0x87, 0x10, 0xA0, 0x9A, 0xBF, 0x88, 0xC0, 0xAC }; + internal static readonly byte[] key_4C949EF0 = { 0x90, 0x22, 0x66, 0xE9, 0x59, 0x11, 0x9B, 0x99, 0x67, 0x39, 0x49, 0x81, 0xAB, 0x98, 0x08, 0xA6 }; + internal static readonly byte[] key_4C949FF0 = { 0xA0, 0xA5, 0x55, 0x0A, 0xFA, 0xB2, 0x16, 0x62, 0x05, 0xDC, 0x4B, 0x8E, 0xDA, 0xD5, 0xA5, 0xCA }; + internal static readonly byte[] key_4C94A0F0 = { 0x78, 0x96, 0xAE, 0x9C, 0xE7, 0x89, 0x2D, 0xF5, 0x34, 0x9C, 0x29, 0x36, 0xD1, 0xF9, 0xE8, 0x3C }; + internal static readonly byte[] key_4C94A1F0 = { 0x71, 0x44, 0x53, 0xB6, 0xE6, 0x75, 0x3F, 0xF0, 0x8D, 0x5E, 0xB4, 0xB2, 0xEA, 0x06, 0x23, 0x6A }; + internal static readonly byte[] key_4C9491F0 = { 0x85, 0x93, 0x1F, 0xED, 0x2C, 0x4D, 0xA4, 0x53, 0x59, 0x9C, 0x3F, 0x16, 0xF3, 0x50, 0xDE, 0x46 }; + internal static readonly byte[] key_4C9494F0 = { 0x76, 0xF2, 0x6C, 0x0A, 0xCA, 0x3A, 0xBA, 0x4E, 0xAC, 0x76, 0xD2, 0x40, 0xF5, 0xC3, 0xBF, 0xF9 }; + internal static readonly byte[] key_4C9490F0 = { 0xFA, 0x79, 0x09, 0x36, 0xE6, 0x19, 0xE8, 0xA4, 0xA9, 0x41, 0x37, 0x18, 0x81, 0x02, 0xE9, 0xB3 }; + + internal static readonly byte[] key_00000000 = + { + 0x6A, 0x19, 0x71, 0xF3, 0x18, 0xDE, 0xD3, 0xA2, 0x6D, 0x3B, 0xDE, 0xC7, 0xBE, 0x98, 0xE2, 0x4C, + 0xE3, 0xDC, 0xDF, 0x42, 0x7B, 0x5B, 0x12, 0x28, 0x7D, 0xC0, 0x7A, 0x59, 0x86, 0xF0, 0xF5, 0xB5, + 0x58, 0xD8, 0x64, 0x18, 0x84, 0x24, 0x7F, 0xE9, 0x57, 0xAB, 0x4F, 0xC6, 0x92, 0x6D, 0x70, 0x29, + 0xD3, 0x61, 0x87, 0x87, 0xD0, 0xAE, 0x2C, 0xE7, 0x37, 0x77, 0xC7, 0x3C, 0x96, 0x7E, 0x21, 0x1F, + 0x65, 0x95, 0xC0, 0x61, 0x57, 0xAC, 0x64, 0xD8, 0x5A, 0x6D, 0x14, 0xD2, 0x9C, 0x54, 0xC6, 0x68, + 0x5D, 0xF5, 0xC3, 0xF0, 0x50, 0xDA, 0xEA, 0x19, 0x43, 0xA7, 0xAD, 0xC3, 0x2A, 0x14, 0xCA, 0xC8, + 0x4C, 0x83, 0x86, 0x18, 0xAE, 0x86, 0x49, 0xFB, 0x4F, 0x45, 0x75, 0xD2, 0xC3, 0xD6, 0xE1, 0x13, + 0x69, 0x37, 0xC6, 0x90, 0xCF, 0xF9, 0x79, 0xA1, 0x77, 0x3A, 0x3E, 0xBB, 0xBB, 0xD5, 0x3B, 0x84, + 0x1B, 0x9A, 0xB8, 0x79, 0xF0, 0xD3, 0x5F, 0x6F, 0x4C, 0xC0, 0x28, 0x87, 0xBC, 0xAE, 0xDA, 0x00 + }; + + internal static readonly byte[] key_01000000 = + { + 0x50, 0xCC, 0x03, 0xAC, 0x3F, 0x53, 0x1A, 0xFA, 0x0A, 0xA4, 0x34, 0x23, 0x86, 0x61, 0x7F, 0x97, + 0x84, 0x1C, 0x1A, 0x1D, 0x08, 0xD4, 0x50, 0xB6, 0xD9, 0x73, 0x27, 0x80, 0xD1, 0xDE, 0xEE, 0xCA, + 0x49, 0x8B, 0x84, 0x37, 0xDB, 0xF0, 0x70, 0xA2, 0xA6, 0x2B, 0x09, 0x4D, 0x3B, 0x29, 0xDE, 0x0B, + 0xE1, 0x6F, 0x04, 0x7A, 0xC4, 0x18, 0x7A, 0x69, 0x73, 0xBF, 0x02, 0xD8, 0xA1, 0xD0, 0x58, 0x7E, + 0x69, 0xCE, 0xAC, 0x5E, 0x1B, 0x0A, 0xF8, 0x19, 0xE6, 0x9A, 0xC0, 0xDE, 0xA0, 0xB2, 0xCE, 0x04, + 0x43, 0xC0, 0x9D, 0x50, 0x5D, 0x0A, 0xD7, 0xFD, 0xC6, 0x53, 0xAA, 0x13, 0xDD, 0x2C, 0x3B, 0x2B, + 0xBF, 0xAB, 0x7C, 0xF5, 0xA0, 0x4A, 0x79, 0xE3, 0xF1, 0x7B, 0x2E, 0xB2, 0xA3, 0xAC, 0x8E, 0x0A, + 0x38, 0x9B, 0x9E, 0xAA, 0xEC, 0x2B, 0xA3, 0x75, 0x13, 0x75, 0x77, 0x98, 0x6A, 0x66, 0x92, 0x65, + 0xBC, 0x97, 0x80, 0x0E, 0x32, 0x88, 0x9F, 0x64, 0xBA, 0x99, 0x8A, 0x72, 0x96, 0x9F, 0xE1, 0xE0 + }; + + internal static readonly byte[] key_16D59E03 = { 0xC3, 0x24, 0x89, 0xD3, 0x80, 0x87, 0xB2, 0x4E, 0x4C, 0xD7, 0x49, 0xE4, 0x9D, 0x1D, 0x34, 0xD1 }; + + internal static readonly byte[] key_4467415D = + { + 0x66, 0x0F, 0xCB, 0x3B, 0x30, 0x75, 0xE3, 0x10, 0x0A, 0x95, 0x65, 0xC7, 0x3C, 0x93, 0x87, 0x22, + 0xF3, 0xA4, 0xB1, 0xE8, 0x9A, 0xFB, 0x53, 0x52, 0x8F, 0x64, 0xB2, 0xDA, 0xB7, 0x76, 0xB9, 0x56, + 0x96, 0xB6, 0x4C, 0x02, 0xE6, 0x9B, 0xAE, 0xED, 0x86, 0x48, 0xBA, 0xA6, 0x4F, 0x23, 0x15, 0x03, + 0x1F, 0xC4, 0xF7, 0x3A, 0x05, 0xC3, 0x3C, 0xE2, 0x2F, 0x36, 0xC4, 0x26, 0xF2, 0x42, 0x40, 0x1F, + 0x97, 0xEE, 0x9C, 0xC6, 0xD9, 0x68, 0xE0, 0xE7, 0xE3, 0x9F, 0xCE, 0x05, 0xE8, 0xD1, 0x8B, 0x1B, + 0x57, 0x34, 0x3D, 0x0D, 0xDF, 0xA8, 0x64, 0xBF, 0x8F, 0x4C, 0x37, 0x3F, 0x93, 0xD5, 0x45, 0x9E, + 0x2B, 0x25, 0x2C, 0x62, 0x74, 0xDE, 0xC1, 0x53, 0xAB, 0x6D, 0xDF, 0x2C, 0xCE, 0x5A, 0x6B, 0x1F, + 0x5E, 0x24, 0x4A, 0xFB, 0x7D, 0xFF, 0xE8, 0xF5, 0x19, 0x77, 0xEF, 0xCC, 0x74, 0xFE, 0x8B, 0x63, + 0x31, 0xAE, 0x99, 0x05, 0x7F, 0x51, 0xF2, 0x72, 0x6A, 0x20, 0x8D, 0x1C, 0xAC, 0x4C, 0xF6, 0x50 + }; + + internal static readonly byte[] key_CFEF05F0 = { 0xCA, 0xFB, 0xBF, 0xC7, 0x50, 0xEA, 0xB4, 0x40, 0x8E, 0x44, 0x5C, 0x63, 0x53, 0xCE, 0x80, 0xB1 }; + internal static readonly byte[] key_CFEF06F0 = { 0x9F, 0x67, 0x1A, 0x7A, 0x22, 0xF3, 0x59, 0x0B, 0xAA, 0x6D, 0xA4, 0xC6, 0x8B, 0xD0, 0x03, 0x77 }; + internal static readonly byte[] key_CFEF08F0 = { 0x2E, 0x00, 0xF6, 0xF7, 0x52, 0xCF, 0x95, 0x5A, 0xA1, 0x26, 0xB4, 0x84, 0x9B, 0x58, 0x76, 0x2F }; + + public static int memlmd_EF73E85B(Span modData, int size, out int newSize) => KernelModuleDecrypt(modData, size, out newSize, 0); + + public static int memlmd_CF03556B(Span modData, int size, out int newSize) => KernelModuleDecrypt(modData, size, out newSize, 1); + + static int KernelModuleDecrypt(Span modData, int size, out int newSize, int use_polling) + { + int ret; + newSize = 0; + Span local_80 = stackalloc byte[48]; + Span local_50 = stackalloc byte[32]; + if (modData.IsEmpty) + { + return -0xc9; + } + if (size < 0x160) + { + return -0xca; + } + //if ((modData[0] & 0x3f) != 0) + //{ + // return -0xcb; + //} + + //if ((0x220202 >> (MemoryMarshal.Read(modData) >> 0x1b)) == 0) + //{ + // return -0xcc; + //} + modData[..0x150].CopyTo(MemlmdKirkMemory.Span); + var hdr = MemoryMarshal.Read(MemlmdKirkMemory.Span); + int? keySeed = null; + bool? keyFlag = null; + Span key = null; + if (hdr.tag == 0x4C94A1F0) + { + keySeed = 0x43; + keyFlag = true; + key = key_4C94A1F0; + } + else if (hdr.tag == 0x4C949BF0) + { + keySeed = 0x43; + keyFlag = true; + key = key_4C949BF0; + } + else if (hdr.tag == 0xB1B9C434) + { + } + else + { + if (hdr.tag == 0x4C9491F0) + { + key = key_4C9491F0; + } + else if (hdr.tag == 0x4C9494F0) + { + key = key_4C9494F0; + } + else if (hdr.tag == 0x4C9490F0) + { + key = key_4C9490F0; + } + else + { + if (hdr.tag == 0x00000000) + { + key = key_00000000; + keySeed = 0x42; + } + else if (hdr.tag == 0x01000000) + { + key = key_01000000; + keySeed = 0x43; + } + keyFlag = false; + goto keytagout; + } + keySeed = 0x43; + keyFlag = true; + } + keytagout: + //if (keyFlag == true && size < 0x160) + //{ + // return -0xca; + //} + if (keyFlag == true) + { + for (var i = 0; i < 0x30; i++) + { + if (hdr.sCheck[i] != 0) + { + ret = -0x12e; + goto errout; + } + } + } + if (keyFlag == false) + { + // TODO blacklistCheck + } + newSize = MemoryMarshal.Read(hdr.sizeInfo); + if (size - 0x150 < newSize) + { + ret = -0xce; + goto errout; + } + if (keyFlag == true) + { + for (byte i = 0; i < 9; i++) + { + key.CopyTo(MemlmdMem_BFC00A00.Span[(i * 0x10 + 0x14)..]); + MemlmdMem_BFC00A00.Span[i * 0x10 + 0x14] = i; + } + ref var refHdr = ref MemoryMarshal.AsRef(modData); + refHdr.sCheck.Slice(0x30, 0x28).CopyTo(MemlmdMem_2780.Span); + refHdr.sCheck.Slice(0x30, 0x28).Fill(0); + MemlmdMem_2780.Span.Slice(0, 0x28).CopyTo(local_80); + var tmp = size - 4; + MemoryMarshal.Write(modData, ref tmp); + if (use_polling == 0) + { + ret = KIRKEngine.sceUtilsBufferCopyWithRange(modData, size, modData, size, KIRKEngine.KIRK_CMD_SHA1_HASH); + } + else + { + // TODO + ret = -1; + } + if (ret == 0) + { + modData.Slice(0, 0x14).CopyTo(local_50); + MemlmdKirkMemory.Span.Slice(0, 0x20).CopyTo(modData); + key7A90.CopyTo(MemlmdMem_BFC00340); + local_50.Slice(0, 0x14).CopyTo(MemlmdMem_BFC00340.Span[0x28..]); + local_80.Slice(0, 0x28).CopyTo(MemlmdMem_BFC00340.Span[0x3C..]); + if (use_polling == 0) + { + ret = KIRKEngine.sceUtilsBufferCopyWithRange(null, 0, MemlmdMem_BFC00340.Span, 100, KIRKEngine.KIRK_CMD_ECDSA_VERIFY); + } + else + { + // TODO + ret = -1; + } + if (ret == 0) + { + // clear key_4C9494F0 psp 660 + // clear key_4C9495F0 psp 660 + // clear key_4C94A1F0 psv + } + else + { + ret = -0x132; + goto errout; + } + } + else + { + if (ret < 0) + { + ret = -0x66; + } + else if (ret == 0xc) + { + ret = -0x6b; + } + else + { + ret = -0x69; + } + goto errout; + } + } + else + { + key[..0x90].CopyTo(MemlmdMem_BFC00A00.Span[0x14..]); + } + ret = Kirk7(MemlmdMem_BFC00A00.Span, 0x90, keySeed.Value, use_polling); + if (ret == 0) + { + MemlmdMem_BFC00A00[..0x90].CopyTo(MemlmdMem_BFC00280); + if (keyFlag == true) + { + hdr.CheckData[..0x5C].CopyTo(MemlmdMem_BFC00A00.Span); + hdr.keyData4.CopyTo(MemlmdMem_BFC00A00[0x5C..].Span); + hdr.sha1Hash.CopyTo(MemlmdMem_BFC00A00[0x6C..].Span); + hdr.keyData.CopyTo(MemlmdMem_BFC00A00[0x80..].Span); + hdr.cmacDataHash.CopyTo(MemlmdMem_BFC00A00[0xB0..].Span); + hdr.sizeInfo.CopyTo(MemlmdMem_BFC00A00[0xC0..].Span); + hdr.RawHdr.CopyTo(MemlmdMem_BFC00A00[0xD0..].Span); + MemlmdMem_BFC00A00.Slice(0x34, 0x28).Span.Fill(0); + } + else + { + hdr.CheckData.CopyTo(MemlmdMem_BFC00A00.Span); + hdr.keyData50.CopyTo(MemlmdMem_BFC00A00[0x80..].Span); + hdr.RawHdr.CopyTo(MemlmdMem_BFC00A00[0xD0..].Span); + } + if (keyFlag == true) + { + MemlmdMem_BFC00A00.Slice(0x5C, 0x60).CopyTo(MemlmdMem_BFC00340[0x14..]); + ret = Kirk7(MemlmdMem_BFC00340.Span, 0x60, keySeed.Value, use_polling); + if (ret == 0) + { + MemlmdMem_BFC00340.Slice(0, 0x60).CopyTo(MemlmdMem_BFC00A00[0x5C..]); + } + else + { + goto kirkerr; + } + } + if (keyFlag == true) + { + MemlmdMem_BFC00A00.Slice(0x6C, 0x14).CopyTo(MemlmdMem_BFC00340); + MemlmdMem_BFC00A00.Slice(0x5C, 0x10).CopyTo(MemlmdMem_BFC00A00[0x70..]); + MemlmdMem_BFC00A00.Slice(0x18, 0x58).Span.Fill(0); + MemlmdMem_BFC00A00[..4].CopyTo(MemlmdMem_BFC00A00[4..]); + var tmp = 0x14c; + MemoryMarshal.Write(MemlmdMem_BFC00A00.Span, ref tmp); + MemlmdMem_BFC00280[..0x10].CopyTo(MemlmdMem_BFC00A00[8..]); + MemlmdMem_BFC00280[..0x10].Span.Fill(0); + } + else + { + MemlmdMem_BFC00A00.Slice(4, 0x14).CopyTo(MemlmdMem_BFC00340); + var tmp = 0x14c; + MemoryMarshal.Write(MemlmdMem_BFC00A00.Span, ref tmp); + MemlmdMem_BFC00280[..0x14].CopyTo(MemlmdMem_BFC00A00[4..]); + } + if (use_polling == 0) + { + ret = KIRKEngine.sceUtilsBufferCopyWithRange(MemlmdMem_BFC00A00.Span, 0x150, MemlmdMem_BFC00A00.Span, 0x150, KIRKEngine.KIRK_CMD_SHA1_HASH); + } + else + { + // TODO + ret = -1; + } + if (ret == 0) + { + if (!MemlmdMem_BFC00A00[..0x14].Span.SequenceEqual(MemlmdMem_BFC00340.Span[..0x14])) + { + ret = -0x12e; + goto errout; + } + if (keyFlag == true) + { + for (var i = 0; i < 0x40; i++) + { + MemlmdMem_BFC00A00[0x80..].Span[i] ^= MemlmdMem_BFC00280[0x10..].Span[i]; + } + MemlmdMem_BFC00280.Slice(0x10, 0x40).Span.Fill(0); + ret = Kirk7(MemlmdMem_BFC00A00[0x6C..].Span, 0x40, keySeed.Value, use_polling); + if (ret != 0) + { + goto kirkerr; + } + for (var i = 0; i < 0x40; i++) + { + modData[0x40..][i] = (byte)(MemlmdMem_BFC00A00[0x6C..].Span[i] ^ MemlmdMem_BFC00280[0x50..].Span[i]); + } + MemlmdMem_BFC00280.Slice(0x50, 0x40).Span.Fill(0); + modData.Slice(0x80, 0x30).Fill(0); + + ref var cmd1Hdr = ref MemoryMarshal.AsRef(modData[0x40..]); + cmd1Hdr.mode = 1; + MemlmdMem_BFC00A00.Slice(0xc0, 0x10).Span.CopyTo(cmd1Hdr.off70); // DAT_00009e80 + modData.Slice(0xc0, 0x10).Fill(0); + MemlmdMem_BFC00A00.Slice(0xd0, 0x80).Span.CopyTo(modData[0xd0..]); // psp hdr size 0x80 + } + else + { + for (var i = 0; i < 0x70; i++) + { + MemlmdMem_BFC00A00[0x40..].Span[i] ^= MemlmdMem_BFC00280[0x14..].Span[i]; + } + ret = Kirk7(MemlmdMem_BFC00A00[0x2C..].Span, 0x70, keySeed.Value, use_polling); + if (ret == 0) + { + for (var i = 0; i < 0x70; i++) + { + modData[0x40..][i] = (byte)(MemlmdMem_BFC00A00[0x2C..].Span[i] ^ MemlmdMem_BFC00280[0x20..].Span[i]); + } + MemlmdMem_BFC00A00.Slice(0xB0, 0xA0).Span.CopyTo(modData[0xB0..]); + ref var cmd1ecdsaHdr = ref MemoryMarshal.AsRef(modData[0x40..]); + if (cmd1ecdsaHdr.ecdsa_hash != 1) + { + ret = -0x12f; + goto errout; + } + } + else + { + // goto kirk err; + goto kirkerr; + } + } + if (use_polling == 0) + { + // File.WriteAllBytes("wrongdata", modData.ToArray()); + ret = KIRKEngine.sceUtilsBufferCopyWithRange(modData, size, modData[0x40..], size - 0x40, KIRKEngine.KIRK_CMD_DECRYPT_PRIVATE); + } + else + { + // TODO + } + if (ret == 0) + { + goto rout; + } + } + } + errout: + + MemlmdKirkMemory.Span.Fill(0); + newSize = 0; + rout: + MemlmdMem_BFC00A00.Span.Fill(0); + + return ret; + kirkerr: + ret ^= 0xC; + ret = -0x6a; + goto errout; + } + + static int Kirk7(Span data, int size, int seed, int use_polling) + { + KIRKEngine.KIRK_AES128CBC_HEADER hdr = new KIRKEngine.KIRK_AES128CBC_HEADER + { + mode = KIRKEngine.KIRK_MODE_DECRYPT_CBC, + keyseed = seed, + data_size = size + }; + MemoryMarshal.Write(data, ref hdr); + //using (var ms = new MemoryStream(data)) + //{ + // ms.Seek(offset, SeekOrigin.Begin); + // using var bw = new BinaryWriter(ms); + // bw.Write(KIRKEngine.KIRK_MODE_DECRYPT_CBC); + // bw.Write(0); + // bw.Write(0); + // bw.Write(seed); + // bw.Write(size); + //} + if (use_polling == 0) + { + KIRKEngine.sceUtilsBufferCopyWithRange(data, size + 20, data, size + 20, + KIRKEngine.KIRK_CMD_DECRYPT_IV_0); + } + return 0; + } + static int Kirk8(Span buf, int size, int use_polling) + { + int retv; + ref var hdr = ref Utils.AsRef(buf); + hdr.mode = KIRKEngine.KIRK_MODE_DECRYPT_CBC; + hdr.keyseed = 0x0100; + hdr.data_size = size; + + retv = KIRKEngine.sceUtilsBufferCopyWithRange(buf, size + 0x14, buf, size, KIRKEngine.KIRK_CMD_DECRYPT_IV_FUSE); + + return retv; + } + + private static readonly byte[] key_XOR_2304 = + { + 0x71, 0xF6, 0xA8, 0x31, 0x1E, 0xE0, 0xFF, 0x1E, 0x50, 0xBA, 0x6C, 0xD2, 0x98, 0x2D, 0xD6, 0x2D + }; + + private static readonly byte[] key_XOR_2314 = + { + 0xAA, 0x85, 0x4D, 0xB0, 0xFF, 0xCA, 0x47, 0xEB, 0x38, 0x7F, 0xD7, 0xE4, 0x3D, 0x62, 0xB0, 0x10 + }; + + static int memlmd_6192F715(Span modData, int size) => KernelModuleSignCheck(modData, size, 0); + + static int memlmd_EA94592C(Span modData, int size) => KernelModuleSignCheck(modData, size, 1); + + static int KernelModuleSignCheck(Span modData, int size, int use_polling) + { + int ret = -0xc9; + if (modData.IsEmpty) + { + goto errorout; + } + if (size < 0x15F) + { + ret = -0xca; + goto errorout; + } + modData.Slice(0x80, 0xD0).CopyTo(MemlmdMem_BFC00A00.Span.Slice(0x14)); + + for (var i = 0; i < 0xD0; i++) + { + MemlmdMem_BFC00A00[0x14..].Span[i] ^= key_XOR_2314[i & 0xF]; + } + ret = Kirk8(MemlmdMem_BFC00A00.Span, 0xD0, use_polling); + if (ret == 0) + { + for (var i = 0; i < 0xD0; i++) + { + MemlmdMem_BFC00A00.Span[i] ^= key_XOR_2304[i & 0xF]; + } + MemlmdMem_BFC00A00.Slice(0x40, 0x90).Span.CopyTo(modData[0x80..]); + MemlmdMem_BFC00A00.Slice(0, 0x40).Span.CopyTo(modData[0x110..]); + } + else + { + if (ret < 0) + { + ret = -0x67; + } + else if (ret != 0xc) + { + ret = -0x6a; + } + else + { + ret = -0x67; + } + } + errorout: + MemlmdMem_BFC00A00.Slice(0, 0x164).Span.Fill(0); + return ret; + } + } +} diff --git a/PspCrypto/SceMesgLed.cs b/PspCrypto/SceMesgLed.cs new file mode 100644 index 0000000..8ed6a99 --- /dev/null +++ b/PspCrypto/SceMesgLed.cs @@ -0,0 +1,2662 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using Elf32_Half = System.UInt16; +using Elf32_Word = System.UInt32; +using Elf32_Addr = System.UInt32; +using Elf32_Off = System.UInt32; +using System.Linq; + +namespace PspCrypto +{ + public static class SceMesgLed + { + #region sceMesgLed_driver + private static readonly byte[] key_2E5E90F0 = { 0x67, 0xE4, 0x8F, 0x4C, 0x08, 0xA0, 0x7D, 0xB1, 0x5F, 0x51, 0xA7, 0x72, 0x98, 0xA8, 0x2D, 0x7E }; + private static readonly byte[] key_2E5E90F0_xor = { 0x69, 0xBA, 0x55, 0x34, 0xF0, 0xC0, 0xD6, 0x71, 0xE3, 0x1F, 0xDB, 0x97, 0xE0, 0x7C, 0xD2, 0x2A }; + private static readonly byte[] key_2E5E80F0 = { 0x0F, 0x74, 0xAF, 0x43, 0x75, 0xCD, 0xDA, 0x39, 0x81, 0x56, 0xD9, 0x61, 0x3E, 0x16, 0xC8, 0x92 }; + private static readonly byte[] key_2E5E80F0_xor = { 0x69, 0xBA, 0x55, 0x34, 0xF0, 0xC0, 0xD6, 0x71, 0xE3, 0x1F, 0xDB, 0x97, 0xE0, 0x7C, 0xD2, 0x2A }; + private static readonly byte[] key_2E5E11F0 = { 0x75, 0xEB, 0xE8, 0x43, 0xF4, 0x87, 0x8F, 0xD0, 0x14, 0x7F, 0x7E, 0x39, 0xAD, 0xAF, 0x04, 0x9D }; + private static readonly byte[] key_2E5E11F0_xor = { 0x69, 0xBA, 0x55, 0x34, 0xF0, 0xC0, 0xD6, 0x71, 0xE3, 0x1F, 0xDB, 0x97, 0xE0, 0x7C, 0xD2, 0x2A }; + private static readonly byte[] key_2E5E12F0 = { 0x8A, 0x7B, 0xC9, 0xD6, 0x52, 0x58, 0x88, 0xEA, 0x51, 0x83, 0x60, 0xCA, 0x16, 0x79, 0xE2, 0x07 }; + private static readonly byte[] key_2E5E13F0 = { 0xFF, 0xA4, 0x68, 0xC3, 0x31, 0xCA, 0xB7, 0x4C, 0xF1, 0x23, 0xFF, 0x01, 0x65, 0x3D, 0x26, 0x36 }; + private static readonly byte[] key_2E5E10F0 = { 0x9D, 0x5C, 0x5B, 0xAF, 0x8C, 0xD8, 0x69, 0x7E, 0x51, 0x9F, 0x70, 0x96, 0xE6, 0xD5, 0xC4, 0xE8 }; + private static readonly byte[] key_2E5E10F0_xor = { 0x69, 0xBA, 0x55, 0x34, 0xF0, 0xC0, 0xD6, 0x71, 0xE3, 0x1F, 0xDB, 0x97, 0xE0, 0x7C, 0xD2, 0x2A }; + + public static int sceMesgLed_driver_31D6D8AA(Span modData, int size, out int newSize, ReadOnlySpan versionKey) + { + var ret = Decrypt(0x2E5E90F0, key_2E5E90F0, 0x48, modData, size, out newSize, 0, null, 10, key_2E5E90F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x2E5E80F0, key_2E5E80F0, 0x48, modData, size, out newSize, 0, null, 7, key_2E5E80F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x2E5E13F0, key_2E5E13F0, 0x48, modData, size, out newSize, 0, null, 5, key_2E5E11F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x2E5E12F0, key_2E5E12F0, 0x48, modData, size, out newSize, 0, null, 5, key_2E5E11F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x2E5E11F0, key_2E5E11F0, 0x48, modData, size, out newSize, 0, null, 5, key_2E5E11F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x2E5E10F0, key_2E5E10F0, 0x48, modData, size, out newSize, 0, null, 5, key_2E5E10F0_xor, versionKey); + } + } + } + } + } + return ret; + } + + private static readonly byte[] key_38029AF0 = { 0xF9, 0x4A, 0x6B, 0x96, 0x79, 0x3F, 0xEE, 0x0A, 0x04, 0xC8, 0x8D, 0x7E, 0x5F, 0x38, 0x3A, 0xCF }; + private static readonly byte[] key_380293F0 = { 0xCB, 0x93, 0x12, 0x38, 0x31, 0xC0, 0x2D, 0x2E, 0x7A, 0x18, 0x5C, 0xAC, 0x92, 0x93, 0xAB, 0x32 }; + private static readonly byte[] key_380292F0 = { 0xD1, 0xB0, 0xAE, 0xC3, 0x24, 0x36, 0x13, 0x49, 0xD6, 0x49, 0xD7, 0x88, 0xEA, 0xA4, 0x99, 0x86 }; + private static readonly byte[] key_380291F0 = { 0x86, 0xA0, 0x7D, 0x4D, 0xB3, 0x6B, 0xA2, 0xFD, 0xF4, 0x15, 0x85, 0x70, 0x2D, 0x6A, 0x0D, 0x3A }; + private static readonly byte[] key_380290F0 = { 0xF9, 0x4A, 0x6B, 0x96, 0x79, 0x3F, 0xEE, 0x0A, 0x04, 0xC8, 0x8D, 0x7E, 0x5F, 0x38, 0x3A, 0xCF }; + private static readonly byte[] key_02000000 = + { + 0x72, 0x81, 0x2F, 0xB3, 0x39, 0x5A, 0x3D, 0xBD, 0x38, 0x8A, 0x10, 0x74, 0x96, 0x55, 0xB1, 0xDF, + 0x88, 0x9F, 0xEE, 0xA1, 0xB5, 0x71, 0x74, 0x89, 0x56, 0xE1, 0xA3, 0xBB, 0x7E, 0x9F, 0xC3, 0xC2, + 0x9E, 0xF8, 0x9B, 0xB9, 0x87, 0xBD, 0x22, 0x88, 0x57, 0xDE, 0x1B, 0x88, 0xC9, 0x9A, 0x3B, 0x1A, + 0xBA, 0xBD, 0xC7, 0xA6, 0x58, 0xCB, 0x8F, 0xA1, 0x0E, 0xDF, 0x64, 0x3B, 0x4A, 0x96, 0x96, 0xCB, + 0x36, 0xD0, 0x4F, 0x2D, 0x32, 0xDD, 0x19, 0xAB, 0xE1, 0xD6, 0x54, 0xFE, 0x97, 0x13, 0x57, 0x5C, + 0x7A, 0x68, 0x05, 0x71, 0x34, 0x7D, 0x31, 0x1E, 0x33, 0x66, 0xDD, 0x6D, 0x7B, 0x76, 0x17, 0x1B, + 0x25, 0x9B, 0xAF, 0x21, 0x79, 0x17, 0x72, 0x10, 0xFD, 0xB5, 0x55, 0x35, 0xA9, 0xBE, 0x55, 0xAE, + 0x72, 0x45, 0xCE, 0x55, 0xA2, 0x70, 0x80, 0xE5, 0xAD, 0xD0, 0xBE, 0xB9, 0xE4, 0x7E, 0x02, 0xA9, + 0x92, 0x46, 0xC3, 0x35, 0x05, 0xF1, 0x7A, 0x93, 0xC1, 0x3A, 0x1A, 0x48, 0x99, 0x3B, 0x3C, 0x1B + }; + + // sceKernelWaitSema + public static int sceMesgLed_driver_5C3A61FE(Span modData, int size, out int newSize) + { + var ret = Decrypt(0x38029AF0, key_38029AF0, 0x5A, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x380293F0, key_380293F0, 0x5A, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x380292F0, key_380292F0, 0x5A, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x380291F0, key_380291F0, 0x5A, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x380290F0, key_380290F0, 0x5A, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x02000000, key_02000000, 0x45, modData, size, out newSize, 0, null, 8, null, null); + } + } + } + } + } + + return ret; + } + + // sceKernelPollSema + static int sceMesgLed_driver_3783B0AD(Span modData, int size, out int newSize) => sceMesgLed_driver_5C3A61FE(modData, size, out newSize); + + private static readonly byte[] key_EFD228F0 = { 0x22, 0xDD, 0x51, 0x6E, 0xF2, 0xD2, 0x1C, 0xA1, 0xE9, 0xD5, 0xBD, 0x88, 0x49, 0x52, 0xDE, 0x5B }; + private static readonly byte[] key_EFD21EF0 = { 0x30, 0x33, 0x7F, 0xFE, 0x67, 0xE1, 0x95, 0x8D, 0xF2, 0xC2, 0xD1, 0x70, 0x8A, 0xD5, 0x4A, 0xE5 }; + private static readonly byte[] key_EFD210F0 = { 0xE2, 0x7E, 0xDE, 0xBC, 0x27, 0x3D, 0x41, 0xD4, 0x03, 0x4A, 0x1A, 0x2B, 0xAA, 0xEE, 0x4A, 0x21 }; + private static readonly byte[] key_0A000000 = + { + 0x80, 0xE4, 0x89, 0x5A, 0x27, 0xDB, 0x39, 0x7D, 0x6B, 0x7B, 0x7B, 0xF2, 0x3D, 0xDF, 0x92, 0xDD, + 0x50, 0xD4, 0xB5, 0x72, 0xAC, 0x6A, 0xF8, 0x7C, 0x67, 0x29, 0x42, 0x32, 0x61, 0x37, 0x79, 0x13, + 0x74, 0xE4, 0x5F, 0xB9, 0x57, 0x56, 0x1A, 0x14, 0xF3, 0x10, 0x5D, 0x8B, 0x7C, 0x74, 0x5C, 0x2F, + 0xBB, 0x96, 0x6B, 0xE4, 0x00, 0x67, 0xEC, 0xF6, 0x28, 0x81, 0xFC, 0x61, 0x01, 0xF0, 0x42, 0x83, + 0x71, 0x7F, 0xCE, 0xB6, 0xD4, 0x59, 0x84, 0xCE, 0xE9, 0xAB, 0x38, 0x5B, 0x7C, 0xD1, 0xB7, 0xFB, + 0x30, 0xDC, 0x87, 0xDF, 0x3B, 0x0C, 0x78, 0xC8, 0x35, 0xF7, 0xA8, 0xC7, 0x2F, 0x33, 0xC7, 0xA0, + 0x31, 0xDD, 0x23, 0x40, 0xE2, 0x77, 0xDF, 0xC2, 0x06, 0xD7, 0x4E, 0x78, 0x4C, 0xA4, 0xD4, 0x22, + 0x73, 0x49, 0x2D, 0x98, 0xEE, 0x17, 0xA1, 0x9A, 0x90, 0xF7, 0x96, 0x48, 0xDB, 0x6D, 0x81, 0x71, + 0xF1, 0xDB, 0x07, 0x85, 0x91, 0x68, 0xE5, 0x26, 0x95, 0xEA, 0xDF, 0xEE, 0xA1, 0xB8, 0x9D, 0x36 + }; + + // sceKernelWaitSema + public static int sceMesgLed_driver_B2CDAC3F(Span modData, int size, out int newSize) + { + var ret = Decrypt(0xEFD228F0, key_EFD228F0, 0x4D, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xEFD21EF0, key_EFD21EF0, 0x4D, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xEFD210F0, key_EFD210F0, 0x4D, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x0A000000, key_0A000000, 0x4D, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + return ret; + } + + // sceKernelPollSema + static int sceMesgLed_driver_B5596BE4(Span modData, int size, out int newSize) => sceMesgLed_driver_B2CDAC3F(modData, size, out newSize); + + private static readonly byte[] key_0B000000 = + { + 0x0B, 0x01, 0x1C, 0xE7, 0x31, 0x15, 0x6B, 0x83, 0x3E, 0x26, 0x0D, 0xCC, 0x69, 0x36, 0x12, 0xCB, + 0xA7, 0xFD, 0x26, 0x66, 0x93, 0x2A, 0x6E, 0x1A, 0x91, 0x2E, 0xC6, 0xFC, 0xD8, 0x2F, 0x00, 0x13, + 0x5A, 0xE2, 0xDF, 0xB6, 0xA2, 0xE4, 0x27, 0xC8, 0x18, 0xC3, 0x50, 0x50, 0xB7, 0xE9, 0x4A, 0xED, + 0xCC, 0x3C, 0x30, 0xFD, 0x10, 0x6A, 0x2B, 0x0A, 0x22, 0xCB, 0xC6, 0xE0, 0x20, 0x65, 0x12, 0xEB, + 0x7D, 0x4E, 0x2A, 0x37, 0x0B, 0x0A, 0xEF, 0x88, 0xDA, 0x06, 0x54, 0xD4, 0x30, 0xAF, 0xCD, 0xCA, + 0x9A, 0xF9, 0xDA, 0x1A, 0xB0, 0x1B, 0xBB, 0x62, 0x0C, 0xDB, 0xF8, 0x44, 0x73, 0x56, 0x14, 0x8E, + 0x93, 0xB1, 0x2C, 0xFD, 0x67, 0xE2, 0x5D, 0xCB, 0x48, 0x5B, 0xD9, 0xB3, 0x54, 0x14, 0xD7, 0x9F, + 0x79, 0x9C, 0x24, 0xE9, 0xC2, 0x7A, 0x4E, 0x8C, 0x4D, 0x24, 0x19, 0x94, 0xFF, 0xC9, 0xC2, 0x2D, + 0x23, 0x63, 0x51, 0xB8, 0xFA, 0xD6, 0x7F, 0xE6, 0x5E, 0xBC, 0x32, 0xB2, 0x02, 0x13, 0xC4, 0x76 + }; + //private static readonly byte[] key_0B000000_blackList = + //{ + // 0xFD, 0xB7, 0xC4, 0xDD, 0x64, 0x48, 0x2C, 0x6C, 0x53, 0xFE, 0x58, 0x42, 0xA1, 0x13, 0x3A, 0xD3, + // 0x4A, 0x07, 0x0E, 0xD2, 0xEA, 0xBA, 0x2F, 0xAD, 0x21, 0x31, 0x48, 0xB8, 0x9D, 0x3B, 0x1E, 0x35, + // 0xBE, 0x8A, 0x5F, 0x54, 0x8E, 0x25, 0xB9, 0xB8, 0xB8, 0xAA, 0x8D, 0xE4, 0x2A, 0xF7, 0x66, 0xE3, + // 0xC1, 0x8A, 0xA6, 0x89, 0x15, 0xFD, 0x88, 0xE3, 0xF1, 0x93, 0x60, 0x63, 0x84, 0x9D, 0x09, 0xD4, + // 0x75, 0xDD, 0x9C, 0x3F, 0xBC, 0x6F, 0x61, 0xC0, 0xB1, 0x82, 0x11, 0x8A, 0xE3, 0x56, 0x68, 0x9E, + // 0x2F, 0x2B, 0xD9, 0x07, 0xA1, 0x14, 0xB4, 0x6F, 0xF6, 0x4E, 0x85, 0x51, 0x5F, 0x64, 0x04, 0xF0, + // 0xF2, 0xDD, 0xE4, 0xBC, 0x09, 0x1E, 0xF2, 0xEC, 0xCB, 0x56, 0xD2, 0x66, 0x3E, 0x2F, 0xE4, 0x31, + // 0x3A, 0xD7, 0xF3, 0x2A, 0x20, 0xB1, 0x01, 0x7E, 0xB9, 0xA2, 0x1C, 0x5D, 0x3E, 0x1C, 0x77, 0x71, + // 0x05, 0x71, 0x9C, 0x14, 0xB0, 0xC7, 0x35, 0xBA, 0x25, 0x04, 0xA1, 0xF8, 0xA8, 0x23, 0x2B, 0x5F + //}; + + // sceKernelWaitSema + public static int sceMesgLed_driver_C79E3488(Span modData, int size, out int newSize) + { + return Decrypt(0x0B000000, key_0B000000, 0x4E, modData, size, out newSize, 0, null, 8, null, null); + } + + // sceKernelPollSema + static int sceMesgLed_driver_6BF453D3(Span modData, int size, out int newSize) => sceMesgLed_driver_C79E3488(modData, size, out newSize); + + private static readonly byte[] key_457B9AF0 = { 0x08, 0x57, 0xC2, 0x49, 0x15, 0xD6, 0x2C, 0xDB, 0x62, 0xBE, 0x86, 0x6C, 0x75, 0x19, 0xDC, 0x4D }; + private static readonly byte[] key_457B93F0 = { 0x88, 0xAF, 0x18, 0xE9, 0xC3, 0xAA, 0x6B, 0x56, 0xF7, 0xC5, 0xA8, 0xBF, 0x1A, 0x84, 0xE9, 0xF3 }; + private static readonly byte[] key_457B92F0 = { 0x92, 0x8C, 0xA4, 0x12, 0xD6, 0x5C, 0x55, 0x31, 0x5B, 0x94, 0x23, 0x9B, 0x62, 0xB3, 0xDB, 0x47 }; + private static readonly byte[] key_457B91F0 = { 0xC5, 0x9C, 0x77, 0x9C, 0x41, 0x01, 0xE4, 0x85, 0x79, 0xC8, 0x71, 0x63, 0xA5, 0x7D, 0x4F, 0xFB }; + private static readonly byte[] key_457B90F0 = { 0xBA, 0x76, 0x61, 0x47, 0x8B, 0x55, 0xA8, 0x72, 0x89, 0x15, 0x79, 0x6D, 0xD7, 0x2F, 0x78, 0x0E }; + private static readonly byte[] key_457B8AF0 = { 0x47, 0xEC, 0x60, 0x15, 0x12, 0x2C, 0xE3, 0xE0, 0x4A, 0x22, 0x6F, 0x31, 0x9F, 0xFA, 0x97, 0x3E }; + private static readonly byte[] key_457B80F0 = { 0xD4, 0x35, 0x18, 0x02, 0x29, 0x68, 0xFB, 0xA0, 0x6A, 0xA9, 0xA5, 0xED, 0x78, 0xFD, 0x2E, 0x9D }; + private static readonly byte[] key_457B10F0 = { 0x71, 0x10, 0xF0, 0xA4, 0x16, 0x14, 0xD5, 0x93, 0x12, 0xFF, 0x74, 0x96, 0xDF, 0x1F, 0xDA, 0x89 }; + private static readonly byte[] key_457B1EF0 = { 0xA3, 0x5D, 0x51, 0xE6, 0x56, 0xC8, 0x01, 0xCA, 0xE3, 0x77, 0xBF, 0xCD, 0xFF, 0x24, 0xDA, 0x4D }; + private static readonly byte[] key_457B28F0 = { 0xB1, 0xB3, 0x7F, 0x76, 0xC3, 0xFB, 0x88, 0xE6, 0xF8, 0x60, 0xD3, 0x35, 0x3C, 0xA3, 0x4E, 0xF3 }; + private static readonly byte[] key_457B0CF0 = { 0xAC, 0x34, 0xBA, 0xB1, 0x97, 0x8D, 0xAE, 0x6F, 0xBA, 0xE8, 0xB1, 0xD6, 0xDF, 0xDF, 0xF1, 0xA2 }; + private static readonly byte[] key_457B0BF0 = { 0x7B, 0x94, 0x72, 0x27, 0x4C, 0xCC, 0x54, 0x3B, 0xAE, 0xDF, 0x46, 0x37, 0xAC, 0x01, 0x4D, 0x87 }; + private static readonly byte[] key_457B0AF0 = { 0xE8, 0xBE, 0x2F, 0x06, 0xB1, 0x05, 0x2A, 0xB9, 0x18, 0x18, 0x03, 0xE3, 0xEB, 0x64, 0x7D, 0x26 }; + private static readonly byte[] key_457B08F0 = { 0xA4, 0x60, 0x8F, 0xAB, 0xAB, 0xDE, 0xA5, 0x65, 0x5D, 0x43, 0x3A, 0xD1, 0x5E, 0xC3, 0xFF, 0xEA }; + private static readonly byte[] key_457B06F0 = { 0x15, 0x07, 0x63, 0x26, 0xDB, 0xE2, 0x69, 0x34, 0x56, 0x08, 0x2A, 0x93, 0x4E, 0x4B, 0x8A, 0xB2 }; + private static readonly byte[] key_457B05F0 = { 0x40, 0x9B, 0xC6, 0x9B, 0xA9, 0xFB, 0x84, 0x7F, 0x72, 0x21, 0xD2, 0x36, 0x96, 0x55, 0x09, 0x74 }; + private static readonly byte[] key_76202403 = { 0xF3, 0xAC, 0x6E, 0x7C, 0x04, 0x0A, 0x23, 0xE7, 0x0D, 0x33, 0xD8, 0x24, 0x73, 0x39, 0x2B, 0x4A }; + private static readonly byte[] key_3ACE4DCE = + { + 0x2F, 0x66, 0xAE, 0x00, 0x01, 0x02, 0x66, 0xEE, 0xA7, 0xC6, 0x58, 0x0C, 0x01, 0x12, 0xB2, 0x88, + 0xE9, 0x9C, 0x04, 0x92, 0x4D, 0xD1, 0xB8, 0x3D, 0x7E, 0x4C, 0x73, 0xDD, 0xF9, 0x20, 0x1F, 0x05, + 0x67, 0x01, 0x54, 0x4F, 0xB1, 0x41, 0x34, 0xEF, 0xC2, 0x4D, 0x9A, 0x5A, 0x6B, 0x21, 0xAA, 0xF6, + 0x6E, 0x03, 0xD5, 0xDE, 0x3E, 0x24, 0xE4, 0x27, 0x1A, 0x19, 0x5E, 0x9C, 0x78, 0x1F, 0x5C, 0x88, + 0x27, 0x8F, 0x47, 0xCD, 0x95, 0x12, 0x88, 0x70, 0x32, 0xFB, 0x5F, 0x1F, 0x21, 0xB1, 0x39, 0x14, + 0x93, 0x46, 0x3D, 0x07, 0xB5, 0x83, 0xD2, 0x7D, 0xFF, 0x25, 0x75, 0xD2, 0x33, 0x4B, 0xFD, 0xDB, + 0x7C, 0x88, 0xFF, 0x89, 0xB3, 0x74, 0x01, 0x8D, 0xAA, 0xFC, 0xE4, 0x2E, 0x52, 0x6E, 0xB6, 0x1F, + 0x98, 0x74, 0x62, 0xD9, 0xB4, 0xFA, 0x81, 0x4F, 0xE9, 0xCC, 0xDB, 0xA7, 0x1C, 0x4E, 0x84, 0x6F, + 0xB0, 0x8B, 0xA4, 0x0E, 0x8B, 0x41, 0x72, 0x30, 0xDA, 0xBB, 0x4C, 0x03, 0x87, 0x83, 0x42, 0xD9 + }; + private static readonly byte[] key_03000000 = + { + 0x22, 0x4E, 0x3B, 0x01, 0xDE, 0xC8, 0x3F, 0x2C, 0x25, 0x00, 0x07, 0x9E, 0x16, 0x48, 0xBC, 0xCB, + 0xAE, 0x1D, 0x13, 0x4B, 0x34, 0xBB, 0x73, 0x83, 0xFA, 0x6A, 0xF9, 0xA1, 0xF3, 0xAE, 0x8E, 0xB7, + 0x6A, 0x25, 0x73, 0x6B, 0x0A, 0xB7, 0x7A, 0x1D, 0xCA, 0x3A, 0x75, 0x34, 0x46, 0x69, 0xD5, 0x52, + 0x6D, 0x62, 0xEC, 0x80, 0x5E, 0xB1, 0xD0, 0x64, 0xE4, 0x1C, 0x1E, 0x29, 0x65, 0x9F, 0x7D, 0xC5, + 0x4D, 0x71, 0x4F, 0xA9, 0xB8, 0xBE, 0xF9, 0xB4, 0x9E, 0xA8, 0x99, 0x63, 0x2C, 0x26, 0x8A, 0x68, + 0x77, 0xF5, 0x6B, 0x8E, 0xAC, 0xAC, 0x15, 0x8A, 0x1E, 0xD6, 0x40, 0xE0, 0x11, 0xE0, 0xB7, 0x8D, + 0x30, 0x04, 0x37, 0x73, 0x3A, 0x26, 0x5E, 0xCE, 0x66, 0x31, 0x73, 0x3C, 0xAB, 0x5A, 0xFF, 0xD0, + 0x87, 0xF8, 0x38, 0x4E, 0x88, 0x0F, 0x45, 0x65, 0x26, 0x5D, 0xDF, 0xD7, 0x49, 0x76, 0xCC, 0xB8, + 0xC5, 0x21, 0x9F, 0xCB, 0x85, 0x6F, 0x19, 0xAF, 0x39, 0x4F, 0x72, 0x9F, 0x79, 0x07, 0xFD, 0x07 + }; + + // sceKernelWaitSema + public static int sceMesgLed_driver_2CB700EC(Span modData, int size, out int newSize) + { + var ret = Decrypt(0x457B9AF0, key_457B9AF0, 0x5B, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B93F0, key_457B93F0, 0x5B, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B92F0, key_457B92F0, 0x5B, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B91F0, key_457B91F0, 0x5B, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B90F0, key_457B90F0, 0x5B, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B8AF0, key_457B8AF0, 0x5B, modData, size, out newSize, 0, null, 6, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B80F0, key_457B80F0, 0x5B, modData, size, out newSize, 0, null, 6, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B28F0, key_457B28F0, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B1EF0, key_457B1EF0, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B10F0, key_457B10F0, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B0CF0, key_457B0CF0, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B0BF0, key_457B0BF0, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B0AF0, key_457B0AF0, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B08F0, key_457B08F0, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B06F0, key_457B06F0, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x457B05F0, key_457B05F0, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x76202403, key_76202403, 0x5B, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x3ACE4DCE, key_3ACE4DCE, 0x5B, modData, size, out newSize, 0, null, 1, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x03000000, key_03000000, 0x46, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + return ret; + } + + // sceKernelPollSema + static int sceMesgLed_driver_308D37FF(Span modData, int size, out int newSize) => sceMesgLed_driver_2CB700EC(modData, size, out newSize); + + private static readonly byte[] key_B2B91F0 = { 0x46, 0x1D, 0xC9, 0xC2, 0x1D, 0x44, 0xA6, 0x68, 0xF2, 0x06, 0x37, 0xBF, 0x62, 0xCD, 0x11, 0x9E }; + + // sceKernelWaitSema + public static int sceResmgr_9DC14891(Span modData, int size, out int newSize) + { + return Decrypt(0xB2B91F0, key_B2B91F0, 0x5C, modData, size, out newSize, 0, null, 9, null, null); + } + + private static readonly byte[] key_D91605F0 = { 0xB8, 0x8C, 0x45, 0x8B, 0xB6, 0xE7, 0x6E, 0xB8, 0x51, 0x59, 0xA6, 0x53, 0x7C, 0x5E, 0x86, 0x31 }; + private static readonly byte[] key_D91606F0 = { 0xED, 0x10, 0xE0, 0x36, 0xC4, 0xFE, 0x83, 0xF3, 0x75, 0x70, 0x5E, 0xF6, 0xA4, 0x40, 0x05, 0xF7 }; + private static readonly byte[] key_D9160AF0 = { 0x10, 0xA9, 0xAC, 0x16, 0xAE, 0x19, 0xC0, 0x7E, 0x3B, 0x60, 0x77, 0x86, 0x01, 0x6F, 0xF2, 0x63 }; + private static readonly byte[] key_D9160BF0 = { 0x83, 0x83, 0xF1, 0x37, 0x53, 0xD0, 0xBE, 0xFC, 0x8D, 0xA7, 0x32, 0x52, 0x46, 0x0A, 0xC2, 0xC2 }; + private static readonly byte[] key_D91610F0 = { 0x89, 0x07, 0x73, 0xB4, 0x09, 0x08, 0x3F, 0x54, 0x31, 0x87, 0x00, 0xF3, 0x35, 0x14, 0x55, 0xCC }; + private static readonly byte[] key_D91611F0 = { 0x61, 0xB0, 0xC0, 0x58, 0x71, 0x57, 0xD9, 0xFA, 0x74, 0x67, 0x0E, 0x5C, 0x7E, 0x6E, 0x95, 0xB9 }; + private static readonly byte[] key_D91612F0 = { 0x9E, 0x20, 0xE1, 0xCD, 0xD7, 0x88, 0xDE, 0xC0, 0x31, 0x9B, 0x10, 0xAF, 0xC5, 0xB8, 0x73, 0x23 }; + private static readonly byte[] key_D91613F0 = { 0xEB, 0xFF, 0x40, 0xD8, 0xB4, 0x1A, 0xE1, 0x66, 0x91, 0x3B, 0x8F, 0x64, 0xB6, 0xFC, 0xB7, 0x12 }; + private static readonly byte[] key_D91614F0 = { 0xFD, 0xF7, 0xB7, 0x3C, 0x9F, 0xD1, 0x33, 0x95, 0x11, 0xB8, 0xB5, 0xBB, 0x54, 0x23, 0x73, 0x85 }; + private static readonly byte[] key_D91617F0 = { 0x02, 0xFA, 0x48, 0x73, 0x75, 0xAF, 0xAE, 0x0A, 0x67, 0x89, 0x2B, 0x95, 0x4B, 0x09, 0x87, 0xA3 }; + private static readonly byte[] key_D91618F0 = { 0x96, 0x96, 0x7C, 0xC3, 0xF7, 0x12, 0xDA, 0x62, 0x1B, 0xF6, 0x9A, 0x9A, 0x44, 0x44, 0xBC, 0x48 }; + private static readonly byte[] key_D9161AF0 = { 0x27, 0xE5, 0xA7, 0x49, 0x52, 0xE1, 0x94, 0x67, 0x35, 0x66, 0x91, 0x0C, 0xE8, 0x9A, 0x25, 0x24 }; + private static readonly byte[] key_D9161EF0 = { 0x5B, 0x4A, 0xD2, 0xF6, 0x49, 0xD4, 0xEB, 0x0D, 0xC0, 0x0F, 0xCB, 0xA8, 0x15, 0x2F, 0x55, 0x08 }; + private static readonly byte[] key_D91628F0 = { 0x49, 0xA4, 0xFC, 0x66, 0xDC, 0xE7, 0x62, 0x21, 0xDB, 0x18, 0xA7, 0x50, 0xD6, 0xA8, 0xC1, 0xB6 }; + private static readonly byte[] key_D91680F0 = { 0x2C, 0x22, 0x9B, 0x12, 0x36, 0x74, 0x11, 0x67, 0x49, 0xD1, 0xD1, 0x88, 0x92, 0xF6, 0xA1, 0xD8 }; + private static readonly byte[] key_D91681F0 = { 0x52, 0xB6, 0x36, 0x6C, 0x8C, 0x46, 0x7F, 0x7A, 0xCC, 0x11, 0x62, 0x99, 0xC1, 0x99, 0xBE, 0x98 }; + private static readonly byte[] key_D91690F0 = { 0x42, 0x61, 0xE2, 0x57, 0x94, 0x49, 0x42, 0xB5, 0xAA, 0x6D, 0x0D, 0x08, 0x3D, 0x24, 0xF7, 0x4B }; + private static readonly byte[] key_8004FD03 = { 0xF4, 0xAE, 0xF4, 0xE1, 0x86, 0xDD, 0xD2, 0x9C, 0x7C, 0xC5, 0x42, 0xA6, 0x95, 0xA0, 0x83, 0x88 }; + private static readonly byte[] key_C0CB167C = + { + 0x8F, 0xAA, 0x27, 0x39, 0x65, 0x94, 0xA2, 0x17, 0x41, 0xBF, 0x28, 0xF2, 0x58, 0xAA, 0x77, 0x0F, + 0xBA, 0xD2, 0x89, 0x91, 0xC3, 0xD4, 0x79, 0xB5, 0xD1, 0xA6, 0xB9, 0xFB, 0xD4, 0x40, 0x19, 0xA0, + 0x11, 0x1A, 0x11, 0x1E, 0x26, 0x8D, 0x20, 0x82, 0xEB, 0x31, 0x39, 0xA7, 0xE4, 0x69, 0xDC, 0xFF, + 0x4F, 0x04, 0xE2, 0xCF, 0x22, 0x41, 0x9A, 0xE4, 0x48, 0xDC, 0xE4, 0x81, 0x84, 0xAA, 0x20, 0x5D, + 0x55, 0x7B, 0xB3, 0xE3, 0xCE, 0xB4, 0xEB, 0x18, 0x73, 0x52, 0xF4, 0x4E, 0xD4, 0x52, 0x88, 0x24, + 0x37, 0x32, 0x1D, 0xFF, 0xCC, 0xCD, 0x41, 0x91, 0xC3, 0xF6, 0x72, 0x94, 0xFB, 0x25, 0x01, 0xEB, + 0x6A, 0x94, 0x98, 0x14, 0x3C, 0xC1, 0x46, 0xF2, 0xD2, 0xDC, 0xF6, 0xDF, 0x77, 0x6C, 0x8A, 0xBC, + 0xE2, 0xDD, 0xCC, 0xDA, 0xD4, 0x6F, 0xB4, 0x33, 0x0F, 0xE2, 0xDC, 0xA2, 0x69, 0x97, 0x33, 0x08, + 0x41, 0xFD, 0x86, 0x4E, 0xF6, 0x01, 0x81, 0x0B, 0x45, 0x38, 0x20, 0xB6, 0xC0, 0x36, 0x9B, 0xF3 + }; + private static readonly byte[] key_08000000 = + { + 0x5B, 0x16, 0x91, 0x75, 0x44, 0xC5, 0xC5, 0x02, 0x6C, 0x76, 0x94, 0xC0, 0x55, 0xF1, 0x12, 0x95, + 0x71, 0x93, 0x91, 0x7E, 0x81, 0xD2, 0x77, 0x31, 0xF1, 0x28, 0x30, 0x70, 0x9B, 0x73, 0x77, 0x4A, + 0x11, 0xD7, 0x56, 0xF1, 0x0E, 0x4C, 0xC1, 0xB0, 0x92, 0x04, 0xE4, 0xE2, 0xAC, 0xA8, 0x46, 0x07, + 0x85, 0xEA, 0x69, 0x9A, 0xD7, 0xD2, 0xEC, 0xF1, 0xA0, 0x43, 0x42, 0x1C, 0xC6, 0xAF, 0xC6, 0x21, + 0xE8, 0x24, 0x06, 0x29, 0x01, 0xBC, 0xB6, 0x59, 0x4D, 0x78, 0x0D, 0x00, 0xD3, 0xFD, 0x95, 0xE7, + 0x3F, 0xD1, 0x45, 0x16, 0xDB, 0x0A, 0x01, 0x18, 0x0B, 0x70, 0xE4, 0x8D, 0x8B, 0xF9, 0xC4, 0x5A, + 0x85, 0x10, 0xDB, 0x78, 0x05, 0xDF, 0xFB, 0x4A, 0xA9, 0xDD, 0x31, 0xC6, 0x6A, 0xC1, 0x98, 0xB4, + 0x65, 0x90, 0x58, 0xF5, 0x65, 0x3C, 0xCD, 0xB0, 0x6A, 0x2F, 0xF6, 0xFD, 0xA4, 0xC7, 0x86, 0x62, + 0xE6, 0xF8, 0x5E, 0xE8, 0x66, 0x16, 0x20, 0x92, 0xF0, 0x8E, 0xEA, 0x17, 0x9F, 0x1A, 0xF3, 0xDD + }; + + // sceKernelWaitSema + public static int sceMesgLed_driver_337D0DD3(Span modData, int size, out int newSize) + { + var ret = Decrypt(0xD91690F0, key_D91690F0, 0x5d, modData, size, out newSize, 0, null, 9, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91681F0, key_D91681F0, 0x5d, modData, size, out newSize, 0, null, 6, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91680F0, key_D91680F0, 0x5d, modData, size, out newSize, 0, null, 6, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD9161AF0, key_D9161AF0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91618F0, key_D91618F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91617F0, key_D91617F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91614F0, key_D91614F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91613F0, key_D91613F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91612F0, key_D91612F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91628F0, key_D91628F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD9161EF0, key_D9161EF0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91610F0, key_D91610F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91611F0, key_D91611F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD9160BF0, key_D9160BF0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD9160AF0, key_D9160AF0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91606F0, key_D91606F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD91605F0, key_D91605F0, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x8004FD03, key_8004FD03, 0x5d, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xC0CB167C, key_C0CB167C, 0x5d, modData, size, out newSize, 0, null, 1, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x08000000, key_08000000, 0x4B, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + } + return ret; + } + + // sceKernelPollSema + static int sceMesgLed_driver_792A6126(Span modData, int size, out int newSize) => sceMesgLed_driver_337D0DD3(modData, size, out newSize); + + private static readonly byte[] key_7B0528F0 = { 0xCE, 0x40, 0xE0, 0x47, 0xA9, 0x97, 0xCA, 0x68, 0xAD, 0x40, 0x1C, 0x68, 0xC3, 0xFF, 0xC2, 0x7A }; + private static readonly byte[] key_7B051EF0 = { 0xDC, 0xAE, 0xCE, 0xD7, 0x3C, 0xA4, 0x43, 0x44, 0xB6, 0x57, 0x70, 0x90, 0x00, 0x78, 0x56, 0xC4 }; + private static readonly byte[] key_7B0510F0 = { 0x1C, 0x0D, 0x41, 0x05, 0xE9, 0x4B, 0x1E, 0x31, 0x5C, 0xC8, 0xD7, 0x33, 0xE3, 0xC4, 0xC2, 0xBE }; + private static readonly byte[] key_7B0508F0 = { 0xC9, 0x7D, 0x3E, 0x0A, 0x54, 0x81, 0x6E, 0xC7, 0x13, 0x74, 0x99, 0x74, 0x62, 0x18, 0xE7, 0xDD }; + private static readonly byte[] key_7B0506F0 = { 0x78, 0x1A, 0xD2, 0x87, 0x24, 0xBD, 0xA2, 0x96, 0x18, 0x3F, 0x89, 0x36, 0x72, 0x90, 0x92, 0x85 }; + private static readonly byte[] key_7B0505F0 = { 0x2D, 0x86, 0x77, 0x3A, 0x56, 0xA4, 0x4F, 0xDD, 0x3C, 0x16, 0x71, 0x93, 0xAA, 0x8E, 0x11, 0x43 }; + private static readonly byte[] key_0A35EA03 = { 0xF9, 0x48, 0x38, 0x0C, 0x96, 0x88, 0xA7, 0x74, 0x4F, 0x65, 0xA0, 0x54, 0xC2, 0x76, 0xD9, 0xB8 }; + private static readonly byte[] key_BB67C59F = + { + 0x82, 0x78, 0x73, 0x69, 0x0D, 0x87, 0x1F, 0xE2, 0x49, 0xD6, 0x1F, 0xA0, 0x58, 0xDA, 0xF8, 0x47, + 0x7E, 0x67, 0x2A, 0xE9, 0xE1, 0x01, 0xBE, 0xC0, 0x6F, 0x54, 0x8D, 0x35, 0x9D, 0xA3, 0x73, 0x0E, + 0xF9, 0x20, 0x47, 0x84, 0xD3, 0xDA, 0xD3, 0x40, 0xF6, 0x1A, 0x37, 0x71, 0x9C, 0xB6, 0x71, 0xBD, + 0x38, 0x15, 0x8D, 0x19, 0x50, 0xE0, 0x35, 0x51, 0xC8, 0xDD, 0x4D, 0x3F, 0xFE, 0xFE, 0x48, 0xDF, + 0xB0, 0xC2, 0xCA, 0x0A, 0x47, 0x2A, 0x1D, 0x6E, 0xFA, 0x64, 0x14, 0x98, 0xED, 0x38, 0xDC, 0x4F, + 0x2D, 0x52, 0x60, 0x44, 0x79, 0x99, 0x79, 0x63, 0xB6, 0x76, 0x90, 0x69, 0x6A, 0x95, 0x42, 0x20, + 0x8B, 0x12, 0x7D, 0xBA, 0x32, 0xAB, 0x36, 0x81, 0x1F, 0x3F, 0xA3, 0xEB, 0x2F, 0x5A, 0x7A, 0x06, + 0x42, 0xC5, 0x24, 0x0B, 0x2C, 0xE2, 0x9D, 0x6F, 0x6B, 0x3B, 0x32, 0x59, 0x90, 0x42, 0x75, 0xE6, + 0xF9, 0x96, 0x16, 0x51, 0x68, 0x7A, 0x45, 0xDE, 0xE8, 0x41, 0x5A, 0xEC, 0x35, 0x7B, 0x6F, 0x05 + }; + private static readonly byte[] key_09000000 = + { + 0x88, 0xD7, 0x76, 0xF5, 0xB7, 0x6D, 0xEC, 0x0E, 0x00, 0x0B, 0x85, 0xE6, 0x4A, 0x37, 0x9B, 0xC9, + 0x00, 0x95, 0xEA, 0x7A, 0xAD, 0x66, 0x67, 0x09, 0x05, 0xF2, 0x09, 0x52, 0x47, 0x27, 0x8E, 0x4E, + 0x57, 0x83, 0xF7, 0x5D, 0x1F, 0x90, 0x11, 0x0E, 0xEF, 0xF6, 0x97, 0x39, 0x83, 0xA1, 0x65, 0xD0, + 0x11, 0xCA, 0x64, 0x96, 0x7A, 0xEF, 0xC9, 0x18, 0x6B, 0x7D, 0x97, 0x7A, 0xF0, 0x32, 0x28, 0xA6, + 0xE2, 0xE3, 0x86, 0x64, 0xDF, 0xAE, 0x03, 0xD2, 0xAD, 0x7E, 0x7F, 0x46, 0x2E, 0x5F, 0x02, 0x95, + 0xE9, 0x35, 0x5C, 0x17, 0xAD, 0x42, 0x54, 0x62, 0x84, 0x66, 0xC9, 0x35, 0x9C, 0x7B, 0x2A, 0xFA, + 0xD7, 0xDC, 0xD0, 0x2B, 0xDA, 0x89, 0x32, 0x7C, 0xD0, 0x77, 0xE6, 0xEE, 0xFE, 0xCD, 0x2B, 0xE0, + 0x87, 0xBB, 0xE1, 0xB5, 0xCE, 0x29, 0x11, 0x9A, 0x79, 0xF1, 0xA8, 0x71, 0xC0, 0x5E, 0x65, 0x39, + 0xFB, 0x6D, 0xB9, 0x37, 0xD4, 0x52, 0x74, 0x44, 0xE1, 0xC2, 0xBA, 0xBA, 0x3D, 0x2D, 0x4A, 0x35 + }; + + // sceKernelWaitSema + public static int sceMesgLed_driver_4EAB9850(Span modData, int size, out int newSize) + { + var ret = Decrypt(0x7B0528F0, key_7B0528F0, 0x5E, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x7B051EF0, key_7B051EF0, 0x5E, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x7B0510F0, key_7B0510F0, 0x5E, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x7B0508F0, key_7B0508F0, 0x5E, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x7B0506F0, key_7B0506F0, 0x5E, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x7B0505F0, key_7B0505F0, 0x5E, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x0A35EA03, key_0A35EA03, 0x5E, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xBB67C59F, key_BB67C59F, 0x5E, modData, size, out newSize, 0, null, 1, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x09000000, key_09000000, 0x4C, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + } + } + } + } + } + return ret; + } + + // sceKernelPollSema + static int sceMesgLed_driver_4BE02A12(Span modData, int size, out int newSize) => sceMesgLed_driver_4EAB9850(modData, size, out newSize); + + private static readonly byte[] key_ADF328F0 = { 0xE3, 0xB1, 0xC9, 0xB3, 0x4E, 0x07, 0x60, 0x49, 0xA7, 0x47, 0xFF, 0x7D, 0x19, 0xFA, 0x56, 0xA1 }; + private static readonly byte[] key_ADF31EF0 = { 0xF1, 0x5F, 0xE7, 0x23, 0xDB, 0x34, 0xE9, 0x65, 0xBC, 0x50, 0x93, 0x85, 0xDA, 0x7D, 0xC2, 0x1F }; + private static readonly byte[] key_ADF310F0 = { 0x23, 0x12, 0x46, 0x61, 0x9B, 0xE8, 0x3D, 0x3C, 0x4D, 0xD8, 0x58, 0xDE, 0xFA, 0x46, 0xC2, 0xDB }; + private static readonly byte[] key_ADF308F0 = { 0xF6, 0x62, 0x39, 0x6E, 0x26, 0x22, 0x4D, 0xCA, 0x02, 0x64, 0x16, 0x99, 0x7B, 0x9A, 0xE7, 0xB8 }; + private static readonly byte[] key_ADF306F0 = { 0x47, 0x05, 0xD5, 0xE3, 0x56, 0x1E, 0x81, 0x9B, 0x09, 0x2F, 0x06, 0xDB, 0x6B, 0x12, 0x92, 0xE0 }; + private static readonly byte[] key_ADF305F0 = { 0x12, 0x99, 0x70, 0x5E, 0x24, 0x07, 0x6C, 0xD0, 0x2D, 0x06, 0xFE, 0x7E, 0xB3, 0x0C, 0x11, 0x26 }; + private static readonly byte[] key_D67B3303 = { 0xC9, 0x03, 0x4F, 0x3C, 0xDD, 0x4F, 0xE8, 0xD0, 0x9A, 0xDD, 0xED, 0x74, 0x64, 0xDC, 0x5C, 0x35 }; + private static readonly byte[] key_7F24BDCD = + { + 0xB2, 0x97, 0x43, 0x75, 0x5E, 0x0C, 0xA7, 0x5E, 0x38, 0x53, 0x7D, 0x80, 0xD1, 0x6B, 0xEA, 0x68, + 0x86, 0x8D, 0x2B, 0xAD, 0x3B, 0x2D, 0x46, 0x47, 0x84, 0xF3, 0x1E, 0x56, 0xBB, 0xEE, 0x3A, 0x3C, + 0xE3, 0x7A, 0xF3, 0x6E, 0xAE, 0x77, 0xF6, 0x23, 0x16, 0xF3, 0x25, 0xCD, 0xFB, 0x8A, 0x01, 0x6F, + 0xB0, 0xDD, 0x3C, 0x44, 0x2F, 0x01, 0xE1, 0x00, 0xA1, 0x5F, 0xB8, 0x86, 0x86, 0x20, 0xB9, 0xD1, + 0xCE, 0x3D, 0xC5, 0x6E, 0x41, 0xB9, 0x11, 0x10, 0x57, 0x2F, 0x29, 0xE2, 0xC9, 0x4C, 0xFD, 0x08, + 0x3D, 0xC2, 0x18, 0x7E, 0x5C, 0xE2, 0xA2, 0x24, 0xD9, 0x01, 0x4B, 0xA1, 0x28, 0x4C, 0xC4, 0xAC, + 0xE2, 0x0E, 0x0A, 0xC4, 0xD2, 0x7F, 0xAA, 0x3A, 0x08, 0x62, 0x45, 0xF7, 0xCA, 0x8D, 0xC6, 0x18, + 0xF0, 0xDE, 0x12, 0x17, 0xAD, 0xCB, 0xB7, 0xF2, 0xCA, 0xC5, 0xA8, 0x56, 0xC1, 0xB3, 0x20, 0xB6, + 0x02, 0xE9, 0x31, 0x08, 0x3B, 0x44, 0x7C, 0xDE, 0x56, 0xB4, 0x3F, 0x12, 0x5D, 0xF0, 0x4F, 0x97 + }; + + private static readonly byte[] key_0C000000 = + { + 0x82, 0x4C, 0xA5, 0x18, 0xD3, 0xC8, 0x6E, 0xEA, 0x17, 0x41, 0x04, 0xDC, 0xEA, 0xC5, 0x01, 0xFC, + 0x97, 0xB1, 0x94, 0x54, 0x71, 0x19, 0x22, 0xEE, 0xE0, 0x2D, 0xE9, 0x83, 0x3D, 0x64, 0x30, 0xE6, + 0x42, 0x5C, 0x30, 0x5F, 0xEB, 0x41, 0xA0, 0xE0, 0x62, 0xC6, 0x63, 0xEE, 0x5D, 0xA5, 0x0D, 0x1E, + 0xC2, 0x10, 0x14, 0x49, 0x06, 0xC6, 0x93, 0x84, 0x71, 0xA5, 0x42, 0x63, 0x13, 0xF0, 0xB6, 0xD5, + 0x43, 0x51, 0x9E, 0xFA, 0x91, 0x0A, 0x7C, 0xE1, 0x58, 0x1B, 0x95, 0x25, 0x40, 0x11, 0xF1, 0x8D, + 0xB1, 0x01, 0x8D, 0x04, 0x09, 0x54, 0x5C, 0x54, 0xF5, 0x53, 0x08, 0xB0, 0x53, 0x85, 0xB4, 0xCE, + 0x0B, 0xF5, 0xC3, 0xFB, 0xC6, 0x55, 0x24, 0x0B, 0xF2, 0xC6, 0x2C, 0xE4, 0x0C, 0xF0, 0x05, 0x3C, + 0xD7, 0x6C, 0x39, 0xD5, 0x87, 0x22, 0x09, 0xF7, 0x3D, 0xC5, 0xA2, 0xFD, 0x55, 0x92, 0x3F, 0xB1, + 0xF6, 0xFE, 0xC8, 0x18, 0x1D, 0x6B, 0x04, 0x52, 0x5F, 0x8C, 0xE8, 0xE7, 0x26, 0x5A, 0x6E, 0x5A + }; + + // sceKernelWaitSema + public static int sceMesgLed_driver_21AFFAAC(Span modData, int size, out int newSize) + { + var ret = Decrypt(0xADF328F0, key_ADF328F0, 0x60, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xADF31EF0, key_ADF31EF0, 0x60, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xADF310F0, key_ADF310F0, 0x60, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xADF308F0, key_ADF308F0, 0x60, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xADF306F0, key_ADF306F0, 0x60, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xADF305F0, key_ADF305F0, 0x60, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD67B3303, key_D67B3303, 0x60, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x7F24BDCD, key_7F24BDCD, 0x60, modData, size, out newSize, 0, null, 1, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x0C000000, key_0C000000, 0x4F, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + } + } + } + } + } + + return ret; + } + + // sceKernelPollSema + static int sceMesgLed_driver_52B6E552(Span modData, int size, out int newSize) => sceMesgLed_driver_21AFFAAC(modData, size, out newSize); + + private static readonly byte[] key_279D28F0 = { 0xD2, 0xF4, 0x82, 0x58, 0xC3, 0x82, 0xDA, 0x73, 0xE9, 0xE2, 0x6F, 0x28, 0x81, 0x1F, 0xA6, 0xD3 }; + private static readonly byte[] key_279D1EF0 = { 0xC0, 0x1A, 0xAC, 0xC8, 0x56, 0xB1, 0x53, 0x5F, 0xF2, 0xF5, 0x03, 0xD0, 0x42, 0x98, 0x32, 0x6D }; + private static readonly byte[] key_279D10F0 = { 0x12, 0x57, 0x0D, 0x8A, 0x16, 0x6D, 0x87, 0x06, 0x03, 0x7D, 0xC8, 0x8B, 0x62, 0xA3, 0x32, 0xA9 }; + private static readonly byte[] key_279D08F0 = { 0xC7, 0x27, 0x72, 0x85, 0xAB, 0xA7, 0xF7, 0xF0, 0x4C, 0xC1, 0x86, 0xCC, 0xE3, 0x7F, 0x17, 0xCA }; + private static readonly byte[] key_279D06F0 = { 0x76, 0x40, 0x9E, 0x08, 0xDB, 0x9B, 0x3B, 0xA1, 0x47, 0x8A, 0x96, 0x8E, 0xF3, 0xF7, 0x62, 0x92 }; + private static readonly byte[] key_279D05F0 = { 0x23, 0xDC, 0x3B, 0xB5, 0xA9, 0x82, 0xD6, 0xEA, 0x63, 0xA3, 0x6E, 0x2B, 0x2B, 0xE9, 0xE1, 0x54 }; + private static readonly byte[] key_D66DF703 = { 0x22, 0x43, 0x57, 0x68, 0x2F, 0x41, 0xCE, 0x65, 0x4C, 0xA3, 0x7C, 0xC6, 0xC4, 0xAC, 0xF3, 0x60 }; + private static readonly byte[] key_1BC8D12B = + { + 0xAE, 0x11, 0x18, 0xE6, 0xED, 0xB2, 0xF1, 0xB2, 0xA8, 0xCC, 0xCA, 0xBF, 0xA9, 0x14, 0x80, 0x87, + 0x66, 0xF1, 0x2D, 0x87, 0xDA, 0xD3, 0x5E, 0xD3, 0xBF, 0xAF, 0xDF, 0xB5, 0x11, 0xC2, 0xDA, 0x4E, + 0x85, 0x09, 0x7C, 0x19, 0xA9, 0xB1, 0xF4, 0x80, 0x35, 0x10, 0xF7, 0x27, 0xA4, 0xB1, 0x52, 0x66, + 0x96, 0xA2, 0x68, 0x77, 0xDB, 0x9C, 0x76, 0x04, 0x09, 0x44, 0x87, 0x91, 0x06, 0xBA, 0xA4, 0x24, + 0x68, 0xA5, 0xBE, 0xD7, 0x4F, 0x73, 0x83, 0x7F, 0x0E, 0x60, 0x13, 0xAD, 0x60, 0xAA, 0xA5, 0xD7, + 0x80, 0x44, 0x3A, 0x3F, 0x5D, 0xE1, 0x6B, 0xD3, 0xD9, 0xE8, 0x4A, 0xF6, 0x57, 0xEB, 0x95, 0x5A, + 0x10, 0xA9, 0x9F, 0x4D, 0xDE, 0x2F, 0xE5, 0xCE, 0xE4, 0xDD, 0x82, 0x5D, 0x05, 0x57, 0x22, 0xCA, + 0xA6, 0xE3, 0xA2, 0xCD, 0x81, 0x16, 0xE4, 0x5A, 0x7D, 0x0E, 0x9B, 0xC0, 0x26, 0x0D, 0xC6, 0x82, + 0x02, 0xC0, 0x9B, 0x0C, 0xE3, 0x7C, 0xD3, 0xCD, 0xF4, 0x33, 0x72, 0x4D, 0xFC, 0x2C, 0xAA, 0x48 + }; + + private static readonly byte[] key_0D000000 = + { + 0xF5, 0x07, 0xA4, 0x47, 0xD7, 0xE0, 0x29, 0x25, 0xB2, 0xC8, 0xE3, 0x95, 0x3E, 0x5E, 0x69, 0xE0, + 0x6F, 0xA5, 0x0D, 0xCF, 0x2D, 0x6A, 0xB5, 0x6C, 0xF1, 0x7D, 0x65, 0xD2, 0x81, 0xD5, 0xBE, 0xDA, + 0xFC, 0xE9, 0xBE, 0x4F, 0xCE, 0x25, 0x95, 0x84, 0xE7, 0x94, 0x23, 0x4A, 0x99, 0x26, 0x48, 0x6F, + 0xFC, 0xCB, 0xC7, 0xDB, 0x99, 0x7A, 0x54, 0x5D, 0x06, 0x07, 0xDF, 0xD8, 0x50, 0x97, 0x48, 0x19, + 0x49, 0x72, 0x0F, 0x16, 0x0F, 0xE6, 0x94, 0x41, 0x70, 0x64, 0xC2, 0x69, 0xCD, 0x22, 0x97, 0xA4, + 0xB7, 0xBD, 0x3F, 0xF8, 0xD9, 0x60, 0xBA, 0x3E, 0x7C, 0xCE, 0x5E, 0x98, 0xBF, 0xAC, 0x5B, 0x91, + 0x01, 0xF3, 0xF1, 0x42, 0xB0, 0xE9, 0xDA, 0x2C, 0xE1, 0x62, 0x75, 0xA1, 0x35, 0x3C, 0xCE, 0x57, + 0x1D, 0xB0, 0xC1, 0x4A, 0xD5, 0xC2, 0xD0, 0x98, 0xAF, 0x64, 0x78, 0x84, 0xB7, 0xDF, 0x5B, 0x57, + 0x67, 0x74, 0xF1, 0x20, 0x57, 0x7A, 0x5F, 0xFF, 0x20, 0x8A, 0xE4, 0x29, 0xEB, 0x12, 0x5E, 0xF4 + }; + + // sceKernelWaitSema + public static int sceMesgLed_driver_C00DAD75(Span modData, int size, out int newSize) + { + var ret = Decrypt(0x279D28F0, key_279D28F0, 0x61, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x279D1EF0, key_279D1EF0, 0x61, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x279D10F0, key_279D10F0, 0x61, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x279D08F0, key_279D08F0, 0x61, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x279D06F0, key_279D06F0, 0x61, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x279D05F0, key_279D05F0, 0x61, modData, size, out newSize, 0, null, 4, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD66DF703, key_D66DF703, 0x61, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x1BC8D12B, key_1BC8D12B, 0x61, modData, size, out newSize, 0, null, 1, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x0D000000, key_0D000000, 0x50, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + } + } + } + } + } + return ret; + } + + // sceKernelPollSema + static int sceMesgLed_driver_F8485C9C(Span modData, int size, out int newSize) => sceMesgLed_driver_C00DAD75(modData, size, out newSize); + + private static readonly byte[] key_E92428F0 = { 0xAA, 0x14, 0x26, 0xB4, 0x4C, 0xB9, 0xF4, 0x58, 0xC1, 0x6F, 0xCD, 0x42, 0x70, 0x2E, 0x12, 0x6A }; + private static readonly byte[] key_E92428F0_xor = { 0xCB, 0x81, 0xEE, 0x3B, 0xDC, 0x87, 0x1E, 0xA1, 0xC8, 0x14, 0xB8, 0xFF, 0x92, 0x3F, 0xB7, 0xC0 }; + private static readonly byte[] key_E9241EF0 = { 0xEF, 0x4A, 0x8E, 0x6B, 0x24, 0x1A, 0xD5, 0xDC, 0xE0, 0xE5, 0x9D, 0xAD, 0xE6, 0x7F, 0xBD, 0x0E }; + private static readonly byte[] key_E9241EF0_xor = { 0x02, 0x99, 0xCE, 0xA6, 0x38, 0x38, 0x32, 0x84, 0x0E, 0xCF, 0x86, 0x6B, 0xB4, 0xEE, 0x3C, 0x77 }; + private static readonly byte[] key_E92410F0 = { 0x71, 0xBE, 0x93, 0xCD, 0x96, 0x65, 0xBC, 0x57, 0xF6, 0xE5, 0xE9, 0xD7, 0x1C, 0x6A, 0xD5, 0xAA }; + private static readonly byte[] key_E92410F0_xor = { 0x36, 0xEF, 0x82, 0x4E, 0x74, 0xFB, 0x17, 0x5B, 0x14, 0x14, 0x05, 0xF3, 0xB3, 0x8A, 0x76, 0x18 }; + private static readonly byte[] key_E92408F0 = { 0x24, 0x84, 0xBE, 0x35, 0xF0, 0xC5, 0x91, 0xA3, 0x3D, 0xA5, 0x94, 0x12, 0x8F, 0xD0, 0x4C, 0x01 }; + private static readonly byte[] key_E92408F0_xor = { 0x2A, 0x1B, 0xF2, 0xD5, 0x11, 0xF8, 0x93, 0x04, 0x9B, 0xF7, 0xB1, 0x7F, 0xC7, 0x8F, 0x6A, 0x11 }; + private static readonly byte[] key_89742B04 = { 0xD7, 0xEB, 0xC9, 0x24, 0x7E, 0x23, 0x3D, 0x89, 0x46, 0xE7, 0x2E, 0x47, 0xAD, 0xDB, 0x0D, 0x09 }; + private static readonly byte[] key_89742B04_xor = { 0xFF, 0x5E, 0xF1, 0xE9, 0xB1, 0xC9, 0x3E, 0xC5, 0xDB, 0xE0, 0x67, 0x82, 0x95, 0x3A, 0x8E, 0xA5 }; + + public static int sceMesgLed_driver_CED2C075(Span modData, int size, out int newSize, ReadOnlySpan versionKey) + { + var ret = Decrypt(0xE92428F0, key_E92428F0, 0x65, modData, size, out newSize, 0, null, 3, key_E92428F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0xE9241EF0, key_E9241EF0, 0x65, modData, size, out newSize, 0, null, 3, key_E9241EF0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0xE92410F0, key_E92410F0, 0x65, modData, size, out newSize, 0, null, 3, key_E92410F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0xE92408F0, key_E92408F0, 0x65, modData, size, out newSize, 0, null, 3, key_E92408F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x89742B04, key_89742B04, 0x65, modData, size, out newSize, 0, null, 3, key_89742B04_xor, versionKey); + } + } + } + } + return ret; + } + + private static readonly byte[] key_0DAA28F0 = { 0x81, 0x79, 0xDB, 0x6B, 0xC1, 0x04, 0x71, 0xE7, 0x64, 0x90, 0xAA, 0x71, 0xC2, 0x94, 0x92, 0x76 }; + private static readonly byte[] key_0DAA28F0_xor = { 0x3C, 0x8B, 0xAB, 0xB0, 0x07, 0x92, 0xAC, 0x1B, 0x2B, 0xCF, 0x10, 0xAA, 0xBD, 0x9F, 0x5B, 0xD8 }; + private static readonly byte[] key_0DAA1EF0 = { 0x1C, 0x3B, 0xD7, 0xA4, 0xA6, 0x41, 0x62, 0x98, 0xA7, 0xDF, 0x5B, 0x16, 0xDA, 0x53, 0x62, 0xF1 }; + private static readonly byte[] key_0DAA1EF0_xor = { 0x87, 0x66, 0x42, 0x6E, 0x6D, 0xB8, 0xC6, 0x28, 0x10, 0x7F, 0xFD, 0xBD, 0x10, 0x7E, 0x7E, 0x31 }; + private static readonly byte[] key_0DAA10F0 = { 0xA9, 0x81, 0x71, 0x9D, 0x92, 0x2D, 0xCC, 0xEE, 0x44, 0x1C, 0x0E, 0x37, 0x7A, 0xF6, 0xE2, 0x3E }; + private static readonly byte[] key_0DAA10F0_xor = { 0xD3, 0x43, 0xA8, 0x49, 0x79, 0x61, 0x82, 0x63, 0x40, 0xBF, 0xA3, 0xEF, 0xB0, 0x99, 0xED, 0x48 }; + private static readonly byte[] key_0DAA06F0 = { 0xCA, 0x26, 0x7D, 0xA2, 0xB9, 0xCE, 0x24, 0x6E, 0xFD, 0x32, 0xA8, 0x97, 0xF4, 0x7C, 0x19, 0x19 }; + private static readonly byte[] key_0DAA06F0_xor = { 0x77, 0x32, 0x20, 0x31, 0xDF, 0x7F, 0x4B, 0x1C, 0x8D, 0xD7, 0xD2, 0xC3, 0x23, 0xA9, 0xF8, 0xA9 }; + + public static int sceMesgLed_driver_EBB4613D(Span modData, int size, out int newSize, ReadOnlySpan versionKey) + { + var ret = Decrypt(0x0DAA28F0, key_0DAA28F0, 0x65, modData, size, out newSize, 0, null, 5, key_0DAA28F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x0DAA1EF0, key_0DAA1EF0, 0x65, modData, size, out newSize, 0, null, 5, key_0DAA1EF0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x0DAA10F0, key_0DAA10F0, 0x65, modData, size, out newSize, 0, null, 5, key_0DAA10F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x0DAA06F0, key_0DAA06F0, 0x65, modData, size, out newSize, 0, null, 5, key_0DAA06F0_xor, versionKey); + } + } + } + + return ret; + } + + private static readonly byte[] key_692828F0 = { 0x49, 0x85, 0x86, 0x1E, 0xB9, 0x99, 0xBD, 0xA5, 0x92, 0xE9, 0xF9, 0xD1, 0x26, 0x43, 0x7E, 0xB5 }; + private static readonly byte[] key_692828F0_xor = { 0x88, 0xD5, 0x04, 0xD5, 0xF8, 0x27, 0x24, 0x13, 0x62, 0x4B, 0xBB, 0x16, 0x44, 0x1E, 0x43, 0x50 }; + private static readonly byte[] key_69281EF0 = { 0x05, 0x94, 0x7F, 0xE2, 0x80, 0x5C, 0x7E, 0xAB, 0x03, 0x66, 0x40, 0x85, 0x3C, 0xD1, 0x2C, 0xFA }; + private static readonly byte[] key_69281EF0_xor = { 0x10, 0xCD, 0x0D, 0xD5, 0x25, 0xC6, 0x28, 0x87, 0x34, 0xC6, 0x0E, 0xBE, 0x6D, 0xE7, 0x19, 0x7D }; + private static readonly byte[] key_692810F0 = { 0xB8, 0xE7, 0xAC, 0xEE, 0x3F, 0x50, 0xB9, 0xA0, 0x66, 0xC8, 0xBD, 0x5E, 0x21, 0x53, 0xF1, 0xD5 }; + private static readonly byte[] key_692810F0_xor = { 0x21, 0x52, 0x5D, 0x76, 0xF6, 0x81, 0x0F, 0x15, 0x2F, 0x4A, 0x40, 0x89, 0x63, 0xA0, 0x10, 0x55 }; + private static readonly byte[] key_692808F0 = { 0x77, 0x66, 0xAD, 0xF8, 0x69, 0x1D, 0x04, 0x6A, 0x37, 0xFE, 0x46, 0x4C, 0xEB, 0xE2, 0x4C, 0xDC }; + private static readonly byte[] key_692808F0_xor = { 0xB6, 0x67, 0x15, 0x92, 0x49, 0x3D, 0x4D, 0x8A, 0x21, 0xE2, 0xF9, 0x0B, 0x7E, 0x24, 0x64, 0xF3 }; + private static readonly byte[] key_F5F12304 = { 0xC0, 0xF0, 0x2D, 0x65, 0xC6, 0xA6, 0x56, 0x9B, 0xB8, 0xE8, 0x0E, 0x82, 0x3B, 0x56, 0xE2, 0xA9 }; + private static readonly byte[] key_F5F12304_xor = { 0x21, 0xEA, 0xBE, 0x48, 0x63, 0xDE, 0x22, 0x4B, 0x3A, 0xDB, 0x81, 0x53, 0x30, 0x03, 0x54, 0x92 }; + + public static int sceMesgLed_driver_C7D1C16B(Span modData, int size, out int newSize, ReadOnlySpan versionKey) + { + var ret = Decrypt(0x692828F0, key_692828F0, 0x66, modData, size, out newSize, 0, null, 3, key_692828F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x69281EF0, key_69281EF0, 0x66, modData, size, out newSize, 0, null, 3, key_69281EF0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x692810F0, key_692810F0, 0x66, modData, size, out newSize, 0, null, 3, key_692810F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x692808F0, key_692808F0, 0x66, modData, size, out newSize, 0, null, 3, key_692808F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0xF5F12304, key_F5F12304, 0x66, modData, size, out newSize, 0, null, 3, key_F5F12304_xor, versionKey); + } + } + } + } + return ret; + } + + private static readonly byte[] key_E1ED28F0 = { 0xF2, 0x1A, 0x92, 0x0C, 0x33, 0xC2, 0x5C, 0xFF, 0x30, 0x73, 0xF6, 0x94, 0x50, 0xAD, 0x33, 0x34 }; + private static readonly byte[] key_E1ED28F0_xor = { 0xE2, 0x04, 0x7A, 0xE7, 0x51, 0x69, 0xE2, 0xF5, 0xC5, 0x94, 0xDF, 0xC9, 0x18, 0x90, 0x43, 0xC8 }; + private static readonly byte[] key_E1ED1EF0 = { 0x50, 0xAC, 0x80, 0x31, 0x36, 0x27, 0xCE, 0x39, 0x43, 0xDA, 0xC7, 0x77, 0x6A, 0x1F, 0x8B, 0x1D }; + private static readonly byte[] key_E1ED1EF0_xor = { 0x0A, 0x97, 0x2A, 0x3C, 0xAD, 0x20, 0x09, 0xAC, 0xB0, 0x72, 0xB8, 0xBF, 0x1A, 0x01, 0x42, 0x8B }; + private static readonly byte[] key_E1ED10F0 = { 0x2A, 0xCE, 0x63, 0xF9, 0xA7, 0x93, 0x2A, 0x6B, 0xF1, 0xDB, 0x41, 0x70, 0x21, 0xB7, 0x21, 0x77 }; + private static readonly byte[] key_E1ED10F0_xor = { 0xE8, 0x21, 0xA6, 0x81, 0xBC, 0xC8, 0x4A, 0x09, 0x88, 0x92, 0x78, 0x65, 0x3A, 0x3B, 0x3C, 0x4E }; + private static readonly byte[] key_E1ED06F0 = { 0x2D, 0xB6, 0x4D, 0x66, 0xCB, 0xA3, 0x8E, 0x4D, 0x13, 0x6F, 0xB1, 0x63, 0x4C, 0xCC, 0x21, 0xF2 }; + private static readonly byte[] key_E1ED06F0_xor = { 0xA5, 0xAC, 0x61, 0x8A, 0x6B, 0xD2, 0x4A, 0xC4, 0x96, 0x75, 0x3B, 0x5A, 0x8C, 0xF6, 0x46, 0x2F }; + + public static int sceMesgLed_driver_66B348B2(Span modData, int size, out int newSize, ReadOnlySpan versionKey) + { + var ret = Decrypt(0xE1ED28F0, key_E1ED28F0, 0x66, modData, size, out newSize, 0, null, 5, key_E1ED28F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0xE1ED1EF0, key_E1ED1EF0, 0x66, modData, size, out newSize, 0, null, 5, key_E1ED1EF0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0xE1ED10F0, key_E1ED10F0, 0x66, modData, size, out newSize, 0, null, 5, key_E1ED10F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0xE1ED06F0, key_E1ED06F0, 0x66, modData, size, out newSize, 0, null, 5, key_E1ED06F0_xor, versionKey); + } + } + } + + return ret; + } + + private static readonly byte[] key_3C2A28F0 = { 0x0B, 0xFD, 0xC8, 0x94, 0xB2, 0xF1, 0x3B, 0x8B, 0x82, 0x0D, 0x1A, 0x58, 0x55, 0x15, 0x31, 0x8A }; + private static readonly byte[] key_3C2A1EF0 = { 0x19, 0x13, 0xE6, 0x04, 0x27, 0xC2, 0xB2, 0xA7, 0x99, 0x1A, 0x76, 0xA0, 0x96, 0x92, 0xA5, 0x34 }; + private static readonly byte[] key_3C2A10F0 = { 0xCB, 0x5E, 0x47, 0x46, 0x67, 0x1E, 0x66, 0xFE, 0x68, 0x92, 0xBD, 0xFB, 0xB6, 0xA9, 0xA5, 0xF0 }; + private static readonly byte[] key_3C2A08F0 = { 0x1E, 0x2E, 0x38, 0x49, 0xDA, 0xD4, 0x16, 0x08, 0x27, 0x2E, 0xF3, 0xBC, 0x37, 0x75, 0x80, 0x93 }; + + public static int sceMesgLed_driver_B2D95FDF(Span modData, int size, out int newSize) + { + var ret = Decrypt(0x3C2A28F0, key_3C2A28F0, 0x67, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x3C2A1EF0, key_3C2A1EF0, 0x67, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x3C2A10F0, key_3C2A10F0, 0x67, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x3C2A08F0, key_3C2A08F0, 0x67, modData, size, out newSize, 0, null, 2, null, null); + } + } + } + + return ret; + } + + static int sceMesgLed_driver_C9ABD2F2(Span modData, int size, out int newSize) => sceMesgLed_driver_B2D95FDF(modData, size, out newSize); + + private static readonly byte[] key_407810F0 = { 0xAF, 0xAD, 0xCA, 0xF1, 0x95, 0x59, 0x91, 0xEC, 0x1B, 0x27, 0xD0, 0x4E, 0x8A, 0xF3, 0x3D, 0xE7 }; + private static readonly byte[] key_407810F0_xor = { 0x84, 0x7B, 0xF5, 0xFE, 0xE8, 0x4D, 0xAD, 0x7A, 0xB5, 0x06, 0x28, 0x0E, 0x09, 0xFA, 0x81, 0xE1 }; + + public static int sceMesgLed_driver_91E0A9AD(Span modData, int size, out int newSize, ReadOnlySpan versionKey) + { + return Decrypt(0x407810F0, key_407810F0, 0x6A, modData, size, out newSize, 0, null, 5, key_407810F0_xor, versionKey); + } + + #endregion + + #region sceResmap_driver + + private static readonly byte[] key_628928F0 = { 0x7F, 0x97, 0xE8, 0x15, 0xFB, 0x5D, 0x9A, 0xAE, 0x06, 0x2D, 0xB1, 0xD4, 0xF6, 0x28, 0x53, 0xBC }; + private static readonly byte[] key_62891EF0 = { 0x6D, 0x79, 0xC6, 0x85, 0x6E, 0x6E, 0x13, 0x82, 0x1D, 0x3A, 0xDD, 0x2C, 0x35, 0xAF, 0xC7, 0x02 }; + private static readonly byte[] key_628910F0 = { 0xBF, 0x34, 0x67, 0xC7, 0x2E, 0xB2, 0xC7, 0xDB, 0xEC, 0xB2, 0x16, 0x77, 0x15, 0x94, 0xC7, 0xC6 }; + + private static readonly byte[] key_04000000 = + { + 0xA3, 0xA0, 0x8E, 0x41, 0x82, 0xE0, 0xBC, 0xC9, 0x9D, 0x22, 0x7F, 0xC5, 0x9C, 0x3C, 0x9C, 0xBE, + 0xC3, 0x72, 0x4F, 0x60, 0x69, 0x48, 0xD2, 0x8C, 0xAD, 0x3A, 0x4A, 0xCF, 0xAF, 0x3F, 0x80, 0x31, + 0x3C, 0x8D, 0x55, 0x2A, 0xD4, 0xBF, 0x3B, 0x3A, 0x5C, 0x2F, 0xF3, 0x57, 0x20, 0xAD, 0xFF, 0x35, + 0x71, 0x4F, 0xD2, 0xD4, 0xE8, 0xC9, 0x52, 0x81, 0xA5, 0x14, 0x0E, 0xB9, 0x8E, 0xED, 0x07, 0x2F, + 0xAE, 0x7F, 0x6B, 0x54, 0x0C, 0xCD, 0x86, 0x91, 0x87, 0x84, 0x48, 0xDA, 0xED, 0xA4, 0x6B, 0xA5, + 0xB5, 0x44, 0x5A, 0x4E, 0xD5, 0x44, 0x00, 0x58, 0x4D, 0xE5, 0xE8, 0xC8, 0x62, 0x68, 0xD2, 0x39, + 0xC7, 0x25, 0x86, 0x81, 0x27, 0xFF, 0x4C, 0x5D, 0xFE, 0x50, 0xBB, 0x2D, 0xB7, 0x08, 0x5C, 0x4F, + 0x7E, 0x85, 0x14, 0x03, 0x91, 0x5A, 0x26, 0x84, 0xF5, 0xE3, 0xA2, 0x02, 0x44, 0x49, 0x96, 0x4F, + 0x66, 0x7E, 0xCF, 0xDB, 0xFE, 0xCB, 0xB4, 0x02, 0xED, 0x66, 0x6F, 0x92, 0x7F, 0x4E, 0x80, 0x63 + }; + + // sceKernelWaitSema + public static int sceResmap_driver_E5659590(Span modData, int size, out int newSize) + { + var ret = Decrypt(0x628928F0, key_628928F0, 0x47, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x62891EF0, key_62891EF0, 0x47, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x628910F0, key_628910F0, 0x47, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x04000000, key_04000000, 0x47, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + return ret; + } + + // sceKernelPollSema + static int sceResmap_driver_4434E59F(Span modData, int size, out int newSize) => sceResmap_driver_E5659590(modData, size, out newSize); + #endregion + + #region scePauth_driver + + private static readonly byte[] key_2FD312F0 = { 0xC5, 0xFB, 0x69, 0x03, 0x20, 0x7A, 0xCF, 0xBA, 0x2C, 0x90, 0xF8, 0xB8, 0x4D, 0xD2, 0xF1, 0xDE }; + private static readonly byte[] key_2FD312F0_xor = { 0xA9, 0x1E, 0xDD, 0x7B, 0x09, 0xBB, 0x22, 0xB5, 0x9D, 0xA3, 0x30, 0x69, 0x13, 0x6E, 0x0E, 0xD8 }; + private static readonly byte[] key_2FD311F0 = { 0x3A, 0x6B, 0x48, 0x96, 0x86, 0xA5, 0xC8, 0x80, 0x69, 0x6C, 0xE6, 0x4B, 0xF6, 0x04, 0x17, 0x44 }; + private static readonly byte[] key_2FD311F0_xor = { 0xA9, 0x1E, 0xDD, 0x7B, 0x09, 0xBB, 0x22, 0xB5, 0x9D, 0xA3, 0x30, 0x69, 0x13, 0x6E, 0x0E, 0xD8 }; + + public static int scePauth_driver_F7AA47F6(Span modData, int size, out int newSize, ReadOnlySpan versionKey) + { + var ret = Decrypt(0x2FD312F0, key_2FD312F0, 0x47, modData, size, out newSize, 0, null, 5, key_2FD312F0_xor, versionKey); + if (ret == -0x12d) + { + ret = Decrypt(0x2FD311F0, key_2FD311F0, 0x47, modData, size, out newSize, 0, null, 5, key_2FD311F0_xor, versionKey); + } + + return ret; + } + + private static readonly byte[] key_2FD313F0 = { 0xB0, 0x24, 0xC8, 0x16, 0x43, 0xE8, 0xF0, 0x1C, 0x8C, 0x30, 0x67, 0x73, 0x3E, 0x96, 0x35, 0xEF }; + private static readonly byte[] key_2FD313F0_xor = { 0xA9, 0x1E, 0xDD, 0x7B, 0x09, 0xBB, 0x22, 0xB5, 0x9D, 0xA3, 0x30, 0x69, 0x13, 0x6E, 0x0E, 0xD8 }; + + static int scePauth_driver_98B83B5D(Span modData, int size, out int newSize, ReadOnlySpan versionKey) + { + return Decrypt(0x2FD313F0, key_2FD313F0, 0x47, modData, size, out newSize, 0, null, 5, key_2FD313F0_xor, versionKey); + } + #endregion + + #region sceDbman_driver + + private static readonly byte[] key_8B9B28F0 = { 0x1A, 0x16, 0xB2, 0x22, 0x5A, 0xA4, 0xF9, 0xB3, 0x55, 0x09, 0xDA, 0xA9, 0xF0, 0x4C, 0x35, 0x28 }; + private static readonly byte[] key_8B9B1EF0 = { 0x08, 0xF8, 0x9C, 0xB2, 0xCF, 0x97, 0x70, 0x9F, 0x4E, 0x1E, 0xB6, 0x51, 0x33, 0xCB, 0xA1, 0x96 }; + private static readonly byte[] key_8B9B10F0 = { 0xDA, 0xB5, 0x3D, 0xF0, 0x8F, 0x4B, 0xA4, 0xC6, 0xBF, 0x96, 0x7D, 0x0A, 0x13, 0xF0, 0xA1, 0x52 }; + private static readonly byte[] key_05000000 = + { + 0xF3, 0x43, 0x5E, 0xDD, 0x3A, 0x41, 0xCA, 0x41, 0xC6, 0x22, 0xBD, 0x56, 0xA5, 0x42, 0x18, 0xB3, + 0x8C, 0xDB, 0xA7, 0x6F, 0x87, 0x41, 0xB2, 0x83, 0x84, 0xAB, 0x07, 0xB0, 0x3D, 0x40, 0x1B, 0xBE, + 0x06, 0x24, 0x6C, 0xD6, 0xAD, 0xEA, 0x47, 0xB6, 0xD0, 0xEE, 0x63, 0xD9, 0x75, 0x0E, 0x3A, 0x1E, + 0x80, 0xAA, 0x4E, 0x5A, 0xD9, 0xED, 0x40, 0x98, 0xD4, 0x14, 0x2D, 0xBF, 0xC5, 0xE7, 0x46, 0xA2, + 0xCD, 0xCD, 0xF2, 0x19, 0x38, 0xDC, 0x74, 0xE4, 0xC5, 0x5D, 0x51, 0xC9, 0xA5, 0xA6, 0x8C, 0x26, + 0x49, 0x36, 0x7A, 0x85, 0x05, 0x7D, 0x5B, 0x4C, 0x24, 0x9F, 0x84, 0x67, 0x23, 0x83, 0xC3, 0xEF, + 0x6C, 0x6A, 0x3C, 0xF9, 0x9F, 0x6E, 0xCA, 0x83, 0xAC, 0x98, 0xC1, 0xDD, 0xE6, 0xD4, 0x91, 0xEA, + 0x77, 0xAA, 0x95, 0xC9, 0x02, 0x03, 0x40, 0xDA, 0x40, 0xF9, 0xC7, 0x8B, 0x95, 0x60, 0xCA, 0x34, + 0x03, 0x17, 0x19, 0x0C, 0xFC, 0xE6, 0x51, 0x3D, 0xC5, 0xF3, 0xC3, 0x72, 0x2D, 0x6B, 0xC4, 0xD5 + }; + + // sceKernelWaitSema sceDbmanSelect + public static int sceDbman_driver_B2B8C3F9(Span modData, int size, out int newSize) + { + var ret = Decrypt(0x8B9B28F0, key_8B9B28F0, 0x48, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x8B9B1EF0, key_8B9B1EF0, 0x48, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x8B9B10F0, key_8B9B10F0, 0x48, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x05000000, key_05000000, 0x48, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + return ret; + } + + // sceKernelPollSema + static int sceDbman_driver_34B53D46(Span modData, int size, out int newSize) => sceDbman_driver_B2B8C3F9(modData, size, out newSize); + #endregion + + #region sceNwman_driver + + private static readonly byte[] key_5A5C28F0 = { 0x66, 0xC5, 0x9C, 0x07, 0x50, 0x73, 0xE4, 0x54, 0xE9, 0x13, 0x42, 0x47, 0xEF, 0xD1, 0x2D, 0x2A }; + private static readonly byte[] key_5A5C1EF0 = { 0x74, 0x2B, 0xB2, 0x97, 0xC5, 0x40, 0x6D, 0x78, 0xF2, 0x04, 0x2E, 0xBF, 0x2C, 0x56, 0xB9, 0x94 }; + private static readonly byte[] key_5A5C10F0 = { 0xA6, 0x66, 0x13, 0xD5, 0x85, 0x9C, 0xB9, 0x21, 0x03, 0x8C, 0xE5, 0xE4, 0x0C, 0x6D, 0xB9, 0x50 }; + private static readonly byte[] key_E42C2303 = { 0x6D, 0x79, 0xF2, 0xF6, 0x37, 0x3D, 0xB7, 0xBE, 0xA2, 0x73, 0xA1, 0xAE, 0x88, 0x70, 0xC9, 0xA3 }; + private static readonly byte[] key_06000000 = + { + 0x84, 0x15, 0x12, 0x8C, 0xA8, 0x83, 0xD7, 0x80, 0xEF, 0x1E, 0x88, 0xDB, 0xBC, 0x61, 0x96, 0x23, + 0x2B, 0xF3, 0x88, 0xFC, 0xE5, 0x3F, 0xB5, 0xDE, 0x98, 0x5A, 0xA0, 0x6B, 0xDE, 0x0A, 0x55, 0xDB, + 0xF2, 0xF8, 0x44, 0x36, 0xEB, 0xD1, 0x94, 0x55, 0x4A, 0x39, 0x3E, 0x93, 0x7C, 0x3D, 0xE3, 0x02, + 0x12, 0x88, 0xE7, 0xF5, 0xF8, 0xF0, 0xC1, 0xEB, 0x25, 0x1B, 0x8D, 0xC6, 0xB8, 0x1E, 0x2B, 0x44, + 0xA5, 0xB7, 0x6A, 0x7E, 0xD0, 0x39, 0x46, 0x92, 0x6D, 0x71, 0xDE, 0x07, 0x97, 0xB8, 0x2F, 0x10, + 0x18, 0xBA, 0xDD, 0x53, 0xC6, 0x07, 0x2B, 0x98, 0x24, 0x8A, 0x74, 0x0D, 0x5C, 0x64, 0x5D, 0xFE, + 0x8E, 0xE7, 0x67, 0x43, 0x93, 0x96, 0xB3, 0xA1, 0xA1, 0xA8, 0xEC, 0x12, 0xC4, 0xFB, 0x58, 0x44, + 0xFC, 0x55, 0x0A, 0x9C, 0x1E, 0x30, 0x37, 0xFA, 0x54, 0x24, 0xD3, 0x03, 0xE2, 0x92, 0xBD, 0x31, + 0x23, 0x03, 0xEA, 0xF7, 0xE7, 0x73, 0xDE, 0x09, 0x3B, 0xB3, 0x83, 0x79, 0xDA, 0x17, 0x85, 0x0E + }; + + // sceKernelWaitSema + public static int sceNwman_driver_9555D68D(Span modData, int size, out int newSize) + { + var ret = Decrypt(0x5A5C28F0, key_5A5C28F0, 0x49, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x5A5C1EF0, key_5A5C1EF0, 0x49, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x5A5C10F0, key_5A5C10F0, 0x49, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xE42C2303, key_E42C2303, 0x49, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x06000000, key_06000000, 0x49, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + } + return ret; + } + #endregion + + #region sceMesgd_driver + + private static readonly byte[] key_D82328F0 = { 0x5D, 0xAA, 0x72, 0xF2, 0x26, 0x60, 0x4D, 0x1C, 0xE7, 0x2D, 0xC8, 0xA3, 0x2F, 0x79, 0xC5, 0x54 }; + private static readonly byte[] key_D8231EF0 = { 0x4F, 0x44, 0x5C, 0x62, 0xB3, 0x53, 0xC4, 0x30, 0xFC, 0x3A, 0xA4, 0x5B, 0xEC, 0xFE, 0x51, 0xEA }; + private static readonly byte[] key_D82310F0 = { 0x9D, 0x09, 0xFD, 0x20, 0xF3, 0x8F, 0x10, 0x69, 0x0D, 0xB2, 0x6F, 0x00, 0xCC, 0xC5, 0x51, 0x2E }; + private static readonly byte[] key_63BAB403 = { 0x02, 0x2B, 0x67, 0x21, 0xE7, 0x86, 0xAD, 0x91, 0x73, 0xBC, 0xC9, 0xDE, 0xC5, 0x7A, 0x13, 0xA4 }; + private static readonly byte[] key_0E000000 = + { + 0xDE, 0x57, 0xB7, 0x77, 0x17, 0xDD, 0x62, 0xEE, 0x7B, 0x78, 0x03, 0x5D, 0x44, 0x86, 0xCA, 0x59, + 0x20, 0x8D, 0xF6, 0x93, 0x28, 0x93, 0x81, 0x21, 0x71, 0x4E, 0xA7, 0x86, 0xCA, 0x82, 0x24, 0x1B, + 0x58, 0xAE, 0x74, 0x5F, 0x6C, 0x01, 0x8D, 0x56, 0x32, 0x88, 0x4D, 0x9A, 0x72, 0x43, 0xA2, 0x2E, + 0x84, 0xF4, 0x0C, 0x82, 0xB9, 0x06, 0xFC, 0xFC, 0x6A, 0xFB, 0x5B, 0x8A, 0xD7, 0x9C, 0x9F, 0xBF, + 0x01, 0x0D, 0x85, 0x15, 0xBA, 0x5F, 0xED, 0x39, 0x93, 0x83, 0xC3, 0x4C, 0xAF, 0xDE, 0x3A, 0xED, + 0xBF, 0x68, 0xA7, 0x1A, 0x77, 0x8A, 0xBD, 0x89, 0x65, 0x41, 0x56, 0x46, 0xD9, 0xDB, 0x33, 0x73, + 0x81, 0x6C, 0xE8, 0x62, 0x96, 0x9B, 0x29, 0x03, 0x5A, 0xAE, 0xAF, 0x73, 0x20, 0x53, 0xA0, 0x40, + 0xE8, 0x4B, 0x66, 0x10, 0x99, 0x6A, 0xB7, 0xE5, 0x70, 0xDD, 0xE0, 0x29, 0x28, 0x24, 0x60, 0xEA, + 0x30, 0xAE, 0x42, 0x20, 0x32, 0x8D, 0x6F, 0x94, 0x71, 0x5F, 0x9E, 0xA2, 0xD5, 0x7F, 0x0C, 0x7C + }; + + // sceKernelWaitSema + public static int sceMesgd_driver_102DC8AF(Span modData, int size, out int newSize) + { + var ret = Decrypt(0xD82328F0, key_D82328F0, 0x51, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD8231EF0, key_D8231EF0, 0x51, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD82310F0, key_D82310F0, 0x51, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x63BAB403, key_63BAB403, 0x51, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x0E000000, key_0E000000, 0x51, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + } + return ret; + } + + // sceKernelPollSema + static int sceMesgd_driver_ADD0CB66(Span modData, int size, out int newSize) => sceMesgd_driver_102DC8AF(modData, size, out newSize); + #endregion + + #region sceWmd_driver + + private static readonly byte[] key_D13B28F0 = { 0x16, 0x6F, 0x8A, 0x89, 0x93, 0x67, 0xF2, 0x47, 0xEB, 0x3D, 0xE5, 0x05, 0xB9, 0x96, 0xEA, 0xEA }; + private static readonly byte[] key_D13B1EF0 = { 0x04, 0x81, 0xA4, 0x19, 0x06, 0x54, 0x7B, 0x6B, 0xF0, 0x2A, 0x89, 0xFD, 0x7A, 0x11, 0x7E, 0x54 }; + private static readonly byte[] key_D13B10F0 = { 0xD6, 0xCC, 0x05, 0x5B, 0x46, 0x88, 0xAF, 0x32, 0x01, 0xA2, 0x42, 0xA6, 0x5A, 0x2A, 0x7E, 0x90 }; + private static readonly byte[] key_D13B08F0 = { 0x03, 0xBC, 0x7A, 0x54, 0xFB, 0x42, 0xDF, 0xC4, 0x4E, 0x1E, 0x0C, 0xE1, 0xDB, 0xF6, 0x5B, 0xF3 }; + private static readonly byte[] key_D13B06F0 = { 0xB2, 0xDB, 0x96, 0xD9, 0x8B, 0x7E, 0x13, 0x95, 0x45, 0x55, 0x1C, 0xA3, 0xCB, 0x7E, 0x2E, 0xAB }; + private static readonly byte[] key_D13B05F0 = { 0xE7, 0x47, 0x33, 0x64, 0xF9, 0x67, 0xFE, 0xDE, 0x61, 0x7C, 0xE4, 0x06, 0x13, 0x60, 0xAD, 0x6D }; + private static readonly byte[] key_1B11FD03 = { 0x71, 0x39, 0xAD, 0x80, 0xA1, 0x07, 0xDC, 0xA1, 0xE4, 0xE5, 0x59, 0x97, 0xEB, 0xB3, 0xFF, 0x48 }; + private static readonly byte[] key_862648D1 = + { + 0x35, 0x2E, 0x7E, 0x08, 0xD6, 0x0A, 0xA8, 0xD0, 0xC2, 0xCF, 0xAB, 0x01, 0x46, 0x6C, 0x5C, 0x81, + 0xF9, 0xE5, 0xA0, 0xAD, 0x38, 0x06, 0x49, 0x19, 0x10, 0xAE, 0x2F, 0xDB, 0xE2, 0x3B, 0xAB, 0x97, + 0xD1, 0x56, 0x70, 0x9F, 0x54, 0x22, 0x07, 0x40, 0xAA, 0x37, 0x11, 0x1F, 0x78, 0x39, 0x42, 0xF2, + 0x1C, 0x44, 0xE5, 0x12, 0xA3, 0x31, 0xE9, 0xEB, 0xFC, 0xE8, 0x5E, 0x8A, 0xE7, 0xC3, 0x18, 0x7B, + 0xEC, 0xA0, 0xBB, 0xD8, 0x11, 0xFB, 0x9C, 0xFD, 0xB8, 0x17, 0xC0, 0xB6, 0x3B, 0x54, 0xAD, 0x2E, + 0xEC, 0xFA, 0x13, 0xF2, 0xFC, 0x8B, 0x91, 0x27, 0xB1, 0x43, 0x93, 0xF0, 0x72, 0x80, 0xE2, 0x50, + 0x18, 0x69, 0xF9, 0x23, 0x69, 0xDA, 0xF8, 0xC0, 0x54, 0xAA, 0x56, 0x80, 0xEE, 0x03, 0x41, 0xD0, + 0xE7, 0xF9, 0x9D, 0xDD, 0x76, 0x09, 0xBC, 0xBF, 0x96, 0xAC, 0x3E, 0x5E, 0x83, 0xA3, 0xEC, 0xCB, + 0x0F, 0xAB, 0x86, 0x9B, 0x02, 0xFC, 0x34, 0x1A, 0x06, 0x3F, 0xC8, 0xD9, 0xF0, 0x00, 0x4C, 0x17 + }; + private static readonly byte[] key_0F000000 = + { + 0x60, 0x73, 0xFD, 0xA2, 0x2D, 0xCE, 0xF1, 0x11, 0xE3, 0x82, 0x78, 0xF5, 0x34, 0xAA, 0x9D, 0xE6, + 0xD2, 0x2D, 0x34, 0xBE, 0x55, 0xBB, 0x57, 0x7F, 0x9B, 0x63, 0x70, 0x21, 0x94, 0x31, 0xEB, 0x4F, + 0xDB, 0x97, 0xB7, 0xA3, 0x1D, 0x64, 0xE4, 0x19, 0xF7, 0x13, 0x16, 0x5E, 0xB3, 0xC9, 0x0F, 0x3E, + 0xBE, 0xC6, 0x41, 0xB5, 0x13, 0xD0, 0x7F, 0xB4, 0x55, 0x16, 0x46, 0x95, 0x22, 0x9A, 0xE6, 0xF7, + 0xAA, 0x67, 0xCB, 0xC4, 0xDD, 0xB7, 0x70, 0x67, 0x52, 0x48, 0xB3, 0x26, 0x5E, 0xA9, 0x38, 0xFF, + 0x5F, 0x62, 0xEA, 0xD4, 0x47, 0xAC, 0xD0, 0x49, 0xAB, 0xC7, 0x7C, 0x48, 0x1B, 0x83, 0x02, 0x83, + 0x30, 0x1B, 0x33, 0xC1, 0x7D, 0x0B, 0x52, 0xD4, 0xBB, 0xEA, 0x10, 0x39, 0x59, 0x3D, 0xF6, 0x96, + 0x2F, 0xF0, 0x50, 0x42, 0xF4, 0x87, 0xC4, 0xEE, 0x29, 0x98, 0x4A, 0xA7, 0x77, 0x36, 0x11, 0xAF, + 0xE7, 0xF9, 0x9C, 0x42, 0xB6, 0x3A, 0x2A, 0x78, 0x0C, 0xFE, 0x8E, 0x55, 0x82, 0x66, 0x11, 0x4A + }; + + // sceKernelWaitSema + public static int sceWmd_driver_7A0E484C(Span modData, int size, out int newSize) + { + var ret = Decrypt(0xD13B28F0, key_D13B28F0, 0x52, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD13B1EF0, key_D13B1EF0, 0x52, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD13B10F0, key_D13B10F0, 0x52, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD13B08F0, key_D13B08F0, 0x52, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD13B06F0, key_D13B06F0, 0x52, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0xD13B05F0, key_D13B05F0, 0x52, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x1B11FD03, key_1B11FD03, 0x52, modData, size, out newSize, 0, null, 2, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x862648D1, key_862648D1, 0x52, modData, size, out newSize, 0, null, 1, null, null); + if (ret == -0x12d) + { + ret = Decrypt(0x0F000000, key_0F000000, 0x52, modData, size, out newSize, 0, null, 0, null, null); + } + } + } + } + } + } + } + } + return ret; + } + + // sceKernelPollSema + static int sceWmd_driver_B7CE9041(Span modData, int size, out int newSize) => sceWmd_driver_7A0E484C(modData, size, out newSize); + #endregion + + + static readonly Memory mem88134300 = new byte[0x1000]; + static readonly Memory mem881343a4 = mem88134300.Slice(0xa4, 0x78); + static readonly Memory mem881343b4 = mem88134300.Slice(0xb4, 0x30); + static readonly Memory mem881343e4 = mem88134300.Slice(0xe4); + static readonly Memory mem881343f4 = mem88134300.Slice(0xf4); + static readonly Memory mem881345c0 = mem88134300.Slice(0x2c0); + static readonly Memory mem881345c4 = mem88134300.Slice(0x2c4); + static readonly Memory mem881345d4 = mem88134300.Slice(0x2d4); + static readonly Memory mem881345d8 = mem88134300.Slice(0x2d8); + static readonly Memory mem881345fc = mem88134300.Slice(0x2fc); + static readonly Memory mem881345e8 = mem88134300.Slice(0x2e8); + static readonly Memory mem88134604 = mem88134300.Slice(0x304); + static readonly Memory mem88134740 = mem88134300.Slice(0x440); + static readonly byte[] key7D20 = { 0xAC, 0x84, 0x69, 0xEB, 0x19, 0x51, 0xEE, 0xE3, 0xFD, 0x5D, 0x93, 0xF8, 0xAB, 0x97, 0x47, 0x56 }; + static readonly byte[] key7D30 = { 0x00, 0x77, 0x3A, 0x39, 0x2A, 0xFC, 0x47, 0x6D, 0x16, 0x3D, 0x43, 0x7E, 0xEF, 0xEF, 0xC3, 0x50 }; + static readonly byte[] key7D40 = + { + 0x2E, 0x2C, 0x67, 0x81, 0xB2, 0x0D, 0xB6, 0x80, 0x7D, 0x99, 0xEB, 0x04, 0x5F, 0xC3, 0x37, 0xB0, + 0xF5, 0x44, 0x2F, 0xEF, 0xDE, 0x1F, 0x9A, 0x4A, 0x01, 0x8A, 0x2E, 0xBF, 0x82, 0x4A, 0x74, 0x95, + 0x71, 0x1F, 0xF2, 0x29, 0xCB, 0x23, 0xC2, 0x6D + }; + + public static int sceResmgr_8E6C62C8(ReadOnlySpan keyBuf) + { + keyBuf.CopyTo(mem881343a4.Span); + mem881343a4[..0x40].CopyTo(mem881345c4); + int len = 0x50; + MemoryMarshal.Write(mem881345c0.Span, ref len); + key7D20.CopyTo(mem88134604); + KIRKEngine.sceUtilsBufferCopyWithRange(mem881345c0.Span, 0x54, mem881345c0.Span, 0x54, KIRKEngine.KIRK_CMD_SHA1_HASH); + mem881345c0[..0x14].CopyTo(mem88134740); + mem881343e4[..0x10].CopyTo(mem881345d4); + Kirk7(mem881345c0.Span, 0x10, 0x56, 0); + if (!mem881345c0.Span.Slice(0, 0x10).SequenceEqual(mem88134740.Span.Slice(0, 0x10))) + { + return -0x12e; + } + key7D40.CopyTo(mem881345c0); + mem88134740[..0x14].CopyTo(mem881345e8); + mem881343f4[..0x28].CopyTo(mem881345fc); + var ret = KIRKEngine.sceUtilsBufferCopyWithRange(null, 0, mem881345c0.Span, 100, KIRKEngine.KIRK_CMD_ECDSA_VERIFY); + if (ret != 0) + { + return -0x12f; + } + mem881343b4.CopyTo(mem881345d4); + for (int i = 0; i < 0x30; i++) + { + mem881345d4.Span[i] ^= key7D30[i & 0xF]; + } + ret = Kirk7(mem881345c0.Span, 0x30, 0x56, 0); + if (ret != 0) + { + return -0x67; + } + for (int i = 0; i < 0x30; i++) + { + mem881345c0.Span[i] ^= key7D30[i & 0xF]; + } + int type = 2; + uint oldTag1, oldTag2, keyTag1, keyTag2; + Memory key1, key2; + if (mem881345c0.Span[0] == 0xF0 && mem881345c0.Span[1] > 0x8f) + { + // type4 + type = 4; + // D91690F0 + oldTag1 = 0xD91690F0; + keyTag1 = MemoryMarshal.Read(mem881345c0.Span); + key1 = mem881345c4.Slice(0, 0x10); + // 2E5E90F0 + oldTag2 = 0x2E5E90F0; + keyTag2 = MemoryMarshal.Read(mem881345d4.Span); + key2 = mem881345d8.Slice(0, 0x10); + } + else if (mem881345c0.Span[0] == 0xF0 && (sbyte)mem881345c0.Span[1] < 0) + { + // type3 + type = 3; + // D91680F0 + oldTag1 = 0xD91680F0; + keyTag1 = MemoryMarshal.Read(mem881345c0.Span); + key1 = mem881345c4.Slice(0, 0x10); + // 2E5E80F0 + oldTag2 = 0x2E5E80F0; + keyTag2 = MemoryMarshal.Read(mem881345d4.Span); + key2 = mem881345d8.Slice(0, 0x10); + } + else + { + // type1 + type = 1; + // D91611F0 + oldTag1 = 0xD91611F0; + keyTag1 = MemoryMarshal.Read(mem881345c0.Span); + key1 = mem881345c4.Slice(0, 0x10); + // 2E5E11F0 + oldTag2 = 0x2E5E11F0; + keyTag2 = MemoryMarshal.Read(mem881345d4.Span); + key2 = mem881345d8.Slice(0, 0x10); + } + Console.WriteLine($"Type: {type}"); + Console.WriteLine($"Old Tag: 0x{oldTag1:X8}, New Tag: 0x{keyTag1:X8}"); + Console.WriteLine($"Key {string.Join(", ", key1.ToArray().Select(b => $"0x{b:X2}"))}"); + Console.WriteLine($"Old Tag: 0x{oldTag2:X8}, New Tag: 0x{keyTag2:X8}"); + Console.WriteLine($"Key {string.Join(", ", key2.ToArray().Select(b => $"0x{b:X2}"))}"); + + return 0; + } + + static int Kirk4(Span data, int size, int seed, int use_polling) + { + KIRKEngine.KIRK_AES128CBC_HEADER hdr = new KIRKEngine.KIRK_AES128CBC_HEADER + { + mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC, + keyseed = seed, + data_size = size + }; + MemoryMarshal.Write(data, ref hdr); + //using (var ms = new MemoryStream(data)) + //{ + // ms.Seek(offset, SeekOrigin.Begin); + // using var bw = new BinaryWriter(ms); + // bw.Write(KIRKEngine.KIRK_MODE_DECRYPT_CBC); + // bw.Write(0); + // bw.Write(0); + // bw.Write(seed); + // bw.Write(size); + //} + if (use_polling == 0) + { + KIRKEngine.sceUtilsBufferCopyWithRange(data, size + 20, data, size + 20, + KIRKEngine.KIRK_CMD_ENCRYPT_IV_0); + } + return 0; + } + + static int Kirk7(Span data, int size, int seed, int use_polling) + { + KIRKEngine.KIRK_AES128CBC_HEADER hdr = new KIRKEngine.KIRK_AES128CBC_HEADER + { + mode = KIRKEngine.KIRK_MODE_DECRYPT_CBC, + keyseed = seed, + data_size = size + }; + MemoryMarshal.Write(data, ref hdr); + //using (var ms = new MemoryStream(data)) + //{ + // ms.Seek(offset, SeekOrigin.Begin); + // using var bw = new BinaryWriter(ms); + // bw.Write(KIRKEngine.KIRK_MODE_DECRYPT_CBC); + // bw.Write(0); + // bw.Write(0); + // bw.Write(seed); + // bw.Write(size); + //} + if (use_polling == 0) + { + KIRKEngine.sceUtilsBufferCopyWithRange(data, size + 20, data, size + 20, + KIRKEngine.KIRK_CMD_DECRYPT_IV_0); + } + return 0; + } + + + static int BuildKeyData(int keySeed, int use_polling, int type, ReadOnlySpan key, + ReadOnlySpan versionKey) + { + //Span kirkBuf = stackalloc byte[20 + 0x90]; + if ((1 < type && type < 8) || type == 9 || type == 10) + { + for (byte i = 0; i < 9; i++) + { + key[..16].CopyTo(KirkMemory[(20 + i * 16)..].Span); + KirkMemory.Span[20 + i * 16] = i; + } + } + else + { + key[..144].CopyTo(KirkMemory[20..].Span); + } + + var ret = Kirk7(KirkMemory.Span, 0x90, keySeed, use_polling); + if (ret != 0) + { + ret = -0x68; + return ret; + } + + if ((type == 3 || type == 5 || type == 7 || type == 10) && versionKey != null) + { + XorKey(KirkMemory.Span, 0x90, versionKey); + } + KirkMemory[..0x90].CopyTo(KeyData); + return ret; + } + static int CheckBlackList(PSPHeader2 prxHdr, ReadOnlySpan blacklist) + { + return 0; + } + static void XorKeyLarge(Span buffer, int size, ReadOnlySpan key) + { + for (int i = 0; i < size; i++) + { + buffer[i] ^= key[i]; + } + } + + static void XorKey(Span buffer, int size, ReadOnlySpan key) + { + XorKey(buffer, size, key, null); + } + + static void XorKey(Span buffer, int size, ReadOnlySpan key1, ReadOnlySpan key2) + { + for (int i = 0; i < size; i++) + { + if (key2 != null) + { + buffer[i] ^= key2[i & 0xf]; + } + + buffer[i] ^= key1[i & 0xf]; + } + } + + static void XorKeyInto(Span dst, int size, ReadOnlySpan src, ReadOnlySpan key) + { + for (int i = 0; i < size; i++) + { + dst[i] = (byte)(src[i] ^ key[i]); + } + } + + private static readonly Memory KirkMemory = new byte[0x150]; // DAT_00009dc0 + private static readonly Memory KeyData = new byte[0x90]; // DAT_00009d00 + private static readonly Memory KirkCmd1Memory = new byte[0xb4]; // DAT_00009f40 + private static readonly Memory KirkEcdsaMemory = new byte[0x48]; // DAT_0000a040 DAT_0000a060 + + private static readonly byte[] key7AE0 = + { + 0xE3, 0x5E, 0x4E, 0x7E, 0x2F, 0xA3, 0x20, 0x96, 0x75, 0x43, 0x94, 0xA9, 0x92, 0x01, 0x83, 0xA7, + 0x85, 0xBD, 0xF6, 0x19, 0x1F, 0x44, 0x8F, 0x95, 0xE0, 0x43, 0x35, 0xA3, 0xF5, 0xE5, 0x05, 0x65, + 0x5E, 0xD7, 0x59, 0x3F, 0xC6, 0xDB, 0xAF, 0x39 + }; + + private static readonly byte[] key7AB8 = + { + 0x25, 0xDC, 0xFD, 0xE2, 0x12, 0x79, 0x89, 0x54, 0x79, 0x37, 0x13, 0x24, 0xEC, 0x25, 0x08, 0x81, + 0x57, 0xAA, 0xF1, 0xD0, 0xA4, 0x64, 0x8C, 0x15, 0x42, 0x25, 0xF6, 0x90, 0x3F, 0x44, 0xE3, 0x6A, + 0xE6, 0x64, 0x12, 0xFC, 0x80, 0x68, 0xBD, 0xC1 + }; + + private static readonly byte[] key7A90 = + { + 0x77, 0x3F, 0x4B, 0xE1, 0x4C, 0x0A, 0xB4, 0x52, 0x67, 0x2B, 0x67, 0x56, 0x82, 0x4C, 0xCF, 0x42, + 0xAA, 0x37, 0xFF, 0xC0, 0x89, 0x41, 0xE5, 0x63, 0x5E, 0x84, 0xE9, 0xFB, 0x53, 0xDA, 0x94, 0x9E, + 0x9B, 0xB7, 0xC2, 0xA4, 0x22, 0x9F, 0xDF, 0x1F + }; + + public static int Decrypt(Span modData, out int newSize, int use_polling, ReadOnlySpan xorKey2) + { + var hdr = MemoryMarshal.AsRef(modData); + if (!Ciphers.ContainsKey(hdr.tag)) + { + newSize = 0; + return -1; + } + var cipher = Ciphers[hdr.tag]; + return Decrypt(hdr.tag, cipher.Key, cipher.Seed, modData, hdr.pspSize, out newSize, use_polling, null, cipher.Type, cipher.XorKey, xorKey2); + } + + static int Decrypt(uint tag, ReadOnlySpan key, int keySeed, Span modData, int size, out int newSize, int use_polling, ReadOnlySpan blacklist, int type, ReadOnlySpan xorKey, ReadOnlySpan xorKey2) + { + newSize = 0; + int ret = -201; + + PSPHeader2 hdr; // DAT_00009bb0 + // Span buf2 = stackalloc byte[0x150]; // DAT_00009dc0 + // Span buf3 = stackalloc byte[0x90]; // DAT_00009d00 + // Span buf4 = stackalloc byte[0xb4]; // DAT_00009f40 + // Span buf5 = stackalloc byte[0x20]; // DAT_0000a040 + // Span buf6 = stackalloc byte[0x28]; // DAT_0000a060 + KirkMemory.Span.Clear(); + KirkCmd1Memory.Span.Clear(); + KirkEcdsaMemory.Span.Clear(); + byte b_0xd4 = 0; + if (modData == null || size == 0) + { + return ret; // 0xffffff37 + } + ret = -202; // 0xffffff36 + if (size < 0x160) + { + return ret; + } + + hdr = MemoryMarshal.Read(modData); + // DAT_00009c80 + var encTag = hdr.tag; + if (tag != encTag) + { + ret = -0x12d; + return ret; + } + + if (type - 9 < 2 && size < 0x160) + { + ret = 0; + return ret; + } + + if ((type == 9 || type == 10) && size < 0x160) + { + ret = -0xca; + return ret; + } + + switch (type) + { + case 3: + { + for (int i = 0; i < 0x18; i++) + { + if (hdr.sCheck[i] != 0) + { + return -0x12e; + } + } + + break; + } + case 2: + { + for (int i = 0; i < 0x58; i++) + { + if (hdr.sCheck[i] != 0) + { + return -0x12e; + } + } + break; + } + case 5: + { + for (int i = 1; i < 0x58; i++) + { + if (hdr.sCheck[i] != 0) + { + return -0x12e; + } + } + + b_0xd4 = hdr.sCheck[0]; // DAT_00009c84 + break; + } + case 6: + { + for (int i = 0; i < 0x38; i++) + { + if (hdr.sCheck[i] != 0) + { + return -0x12e; + } + } + + break; + } + case 7: + { + for (int i = 1; i < 0x38; i++) + { + if (hdr.sCheck[i] != 0) + { + return -0x12e; + } + } + b_0xd4 = hdr.sCheck[0]; // DAT_00009c84 + break; + } + case 9: + { + for (int i = 0; i < 0x30; i++) + { + if (hdr.sCheck[i] != 0) + { + return -0x12e; + } + } + + break; + } + case 10: + { + for (int i = 1; i < 0x30; i++) + { + if (hdr.sCheck[i] != 0) + { + return -0x12e; + } + } + b_0xd4 = hdr.sCheck[0]; // DAT_00009c84 + + break; + } + } + + if (blacklist != null && blacklist.Length > 0) + { + ret = CheckBlackList(hdr, blacklist); + if (ret == 1) + { + ret = -0x131; + return ret; + } + } + + var elfSize = hdr.dataSize; // DAT_00009c60 - 9bb0 + newSize = elfSize; + if (size - 0x150 < elfSize) + { + ret = -0xce; + return ret; + } + + ret = BuildKeyData(keySeed, use_polling, type, key, xorKey2); + if (ret != 0) + { + return ret; + } + + + if (type == 9 || type == 10) + { + Span sigSpan = stackalloc byte[48]; + Span sha1Span = stackalloc byte[32]; + modData.Slice(0x104, 0x28).CopyTo(KirkEcdsaMemory.Slice(0x20).Span); // DAT_0000a060 + modData.Slice(0x104, 0x28).Clear(); + KirkEcdsaMemory.Slice(0x20).Span.CopyTo(sigSpan); + var tmpSize = size - 4; + MemoryMarshal.Write(modData, ref tmpSize); + if (use_polling == 0) + { + ret = KIRKEngine.sceUtilsBufferCopyWithRange(modData, size, modData, size, + KIRKEngine.KIRK_CMD_SHA1_HASH); + } + else + { + ret = -13; + return ret; + } + + if (ret != 0) + { + ret = -13; + return ret; + } + modData[..0x14].CopyTo(sha1Span); + hdr.RawHdr.Slice(0, 0x20).CopyTo(modData); + var tagBytes = BitConverter.GetBytes(hdr.tag); + ref var ecdsaHdr = ref MemoryMarshal.AsRef(KirkCmd1Memory.Span); + if (tagBytes[2] == 0x16) + { + // key7AE0.CopyTo(buf4); // DAT_00007ae0 DAT_00009f40 + key7AE0.CopyTo(ecdsaHdr.public_key.point); // DAT_00007ae0 DAT_00009f40 + } + else if (tagBytes[2] == 0x5e) + { + // key7AB8.CopyTo(buf4); // DAT_00007ab DAT_00009f40 + key7AB8.CopyTo(ecdsaHdr.public_key.point); // DAT_00007ab DAT_00009f40 + } + else + { + // key7A90.CopyTo(buf4); // DAT_00007a90 DAT_00009f40 + key7A90.CopyTo(ecdsaHdr.public_key.point); + } + // sha1Span.Slice(0, 0x14).CopyTo(buf4.Slice(0x28)); // DAT_00009f68 - 9f40 + sha1Span[..0x14].CopyTo(ecdsaHdr.message_hash); // DAT_00009f68 - 9f40 + // sigSpan.Slice(0, 0x28).CopyTo(buf4.Slice(0x3c)); // DAT_00009f7c - 9f40 + sigSpan[..0x28].CopyTo(ecdsaHdr.signature.sig); // DAT_00009f68 - 9f40 + if (use_polling == 0) + { + ret = KIRKEngine.sceUtilsBufferCopyWithRange(null, 0, KirkCmd1Memory.Span, 0x64, + KIRKEngine.KIRK_CMD_ECDSA_VERIFY); + } + else + { + ret = -13; + return ret; + } + + if (ret != 0) + { + ret = -0x132; + return ret; + } + } + + if (type == 3) + { + hdr.sCheck.Slice(24, 0x40).CopyTo(KirkMemory.Span); // DAT_00009c9c - 9bb0 buf1 0xec + KirkMemory.Slice(0x40, 0x50).Span.Clear(); // DAT_00009e00 - 9dc0 0x40 + KirkMemory.Span[0x60] = 3; // DAT_00009e20 - 9dc0 0x60 + KirkMemory.Span[0x70] = 0x50; // DAT_00009e30 - 9dc0 0x70 + hdr.keyData.CopyTo(KirkMemory.Slice(0x90).Span); // DAT_00009c30 - 9bb0 0x80 DAT_00009e50 - 9dc0 0x90 + hdr.cmacDataHash.CopyTo(KirkMemory.Slice(0xc0).Span); // DAT_00009c70 - 9bb0 0xc0 DAT_00009e80 - 9dc0 0xc0 + hdr.sha1Hash.Slice(0, 0x10).CopyTo(KirkMemory.Slice(0xd0).Span); // DAT_00009cdc - 9bb0 0x12c DAT_00009e90 - 9dc0 0xd0 + + XorKey(KirkMemory.Slice(0x90).Span, 0x50, xorKey, xorKey2); // DAT_00009e50 - 9dc0 0x90 + ret = KIRKEngine.sceUtilsBufferCopyWithRange(KirkCmd1Memory.Span, 0xb4, KirkMemory.Span, 0x150, KIRKEngine.KIRK_CMD_3); + if (ret != 0) + { + ret = -14; + return ret; + } + + MemoryMarshal.Write(KirkMemory.Span, ref hdr.tag); + KirkMemory.Slice(0x04, 0x58).Span.Clear(); + hdr.keyData4.CopyTo(KirkMemory.Slice(0x5c).Span); + hdr.sha1Hash.CopyTo(KirkMemory.Slice(0x6c).Span); + KirkCmd1Memory[..0x10].CopyTo(KirkMemory[0x6c..]); + KirkCmd1Memory[..0x30].CopyTo(KirkMemory[0x80..]); + KirkCmd1Memory.Slice(0x30, 0x10).CopyTo(KirkMemory.Slice(0xb0)); + hdr.sizeInfo.CopyTo(KirkMemory.Slice(0xc0).Span); + var hdrSpan = MemoryMarshal.CreateSpan(ref hdr, 1); + MemoryMarshal.AsBytes(hdrSpan).Slice(0, 0x80).CopyTo(KirkMemory.Slice(0xd0).Span); + } + else if (type == 5 || type == 7 || type == 10) + { + hdr.keyData.CopyTo(KirkMemory.Slice(0x14).Span); // DAT_00009c30 - 9bb0 0x80 DAT_00009dd4 - 9dc0 0x14 + hdr.cmacDataHash.CopyTo(KirkMemory.Slice(0x44).Span); // DAT_00009c70 - 9bb0 0xC0 DAT_00009e04 - 9dc0 0x44 + hdr.sha1Hash.Slice(0, 0x10).CopyTo(KirkMemory.Slice(0x54).Span); // DAT_00009cdc - 9bb0 0x12C DAT_00009e14 - 9dc0 0x54 + XorKey(KirkMemory.Slice(0x14).Span, 0x50, xorKey, xorKey2); + ret = Kirk7(KirkMemory.Span, 0x50, keySeed, use_polling); + if (ret != 0) + { + return -13; + } + KirkMemory[..0x50].CopyTo(KirkCmd1Memory); // DAT_00009dc0 DAT_00009f40 + MemoryMarshal.Write(KirkMemory.Span, ref hdr.tag); // DAT_00009c80 - 9bb0 DAT_00009dc0 + KirkMemory.Slice(4, 0x58).Span.Fill(0); // DAT_00009dc4 - 9dc0 4 + if (type == 7) + { + hdr.sCheck.Slice(56, 0x20).CopyTo(KirkMemory.Slice(0x3c).Span); // DAT_00009cbc - 9bb0 0x10c DAT_00009dfc - 9dc0 0x3C + } + + hdr.keyData4.CopyTo(KirkMemory.Slice(0x5c).Span); // DAT_00009cf0 - 9bb0 0x140 DAT_00009e1c - 9dc0 0x5c + hdr.sha1Hash.CopyTo(KirkMemory.Slice(0x6c).Span); // DAT_00009cdc - 9bb0 0x12C DAT_00009e2c - 9dc0 0x6c + KirkCmd1Memory.Slice(0x40, 0x10).CopyTo(KirkMemory.Slice(0x6c)); // DAT_00009f80 - 9f40 0x40 DAT_00009e2c - 9dc0 0x6c + KirkCmd1Memory[..0x30].CopyTo(KirkMemory[0x80..]); + KirkCmd1Memory.Slice(0x30, 0x10).CopyTo(KirkMemory.Slice(0xb0)); + hdr.sizeInfo.CopyTo(KirkMemory.Slice(0xc0).Span); // DAT_00009c60 - 9bb0 0xB0 DAT_00009e80 - 9dc0 0xC0 + hdr.RawHdr.CopyTo(KirkMemory.Slice(0xd0).Span); // DAT_00009bb0 DAT_00009e90 - 9dc0 0xd0 + + if (type == 10) + { + KirkMemory.Slice(0x34, 0x28).Span.Fill(0); + } + } + else if (type == 2 || type == 4 || type == 6 || type == 9) + { + MemoryMarshal.Write(KirkMemory.Span, ref hdr.tag); + hdr.keyData4.CopyTo(KirkMemory.Slice(0x5c).Span); + hdr.sha1Hash.CopyTo(KirkMemory.Slice(0x6c).Span); + hdr.keyData.CopyTo(KirkMemory.Slice(0x80).Span); + hdr.cmacDataHash.CopyTo(KirkMemory.Slice(0xb0).Span); + hdr.sizeInfo.CopyTo(KirkMemory.Slice(0xc0).Span); + hdr.RawHdr.CopyTo(KirkMemory.Slice(0xd0).Span); + if (type == 9) + { + KirkMemory.Slice(0x34, 0x28).Span.Fill(0); // DAT_00009df4 - 9dc0 + } + } + else + { + hdr.CheckData.CopyTo(KirkMemory.Span); // DAT_00009c80 DAT_00009dc0 + hdr.keyData50.CopyTo(KirkMemory.Slice(0x80).Span); // DAT_00009c30 DAT_00009e40 + hdr.RawHdr.CopyTo(KirkMemory.Slice(0xd0).Span); // DAT_00009bb0 DAT_00009e90 + } + + if (type == 1) + { + KirkMemory.Slice(0x10, 0xa0).CopyTo(KirkCmd1Memory.Slice(0x14)); // DAT_00009dd0 DAT_00009f54 + ret = Kirk7(KirkCmd1Memory.Span, 0xa0, keySeed, use_polling); + if (ret != 0) + { + return -15; + } + KirkCmd1Memory[..0xa0].CopyTo(KirkMemory[0x10..]); // DAT_00009f40 DAT_00009dd0 + } + else if ((1 < type && type < 8) || type == 9 || type == 10) + { + KirkMemory.Slice(0x5c, 0x60).CopyTo(KirkCmd1Memory[0x14..]); // DAT_00009e1c DAT_00009f54 + + if (type == 3 || type == 5 || type == 7 || type == 10) + { + XorKey(KirkCmd1Memory.Span[0x14..], 0x60, xorKey); // DAT_00009f54 + } + + unsafe + { + fixed (byte* ptr = &MemoryMarshal.GetReference(KirkCmd1Memory.Span)) + { + + } + } + + + ret = Kirk7(KirkCmd1Memory.Span, 0x60, keySeed, use_polling); + if (ret != 0) + { + return -5; + } + KirkCmd1Memory[..0x60].CopyTo(KirkMemory[0x5c..]); // DAT_00009f40 DAT_00009e1c + } + + if ((1 < type && type < 8) || type == 9 || type == 10) + { + KirkMemory.Slice(0x6c, 0x14).CopyTo(KirkCmd1Memory); + if (type == 4) + { + KirkMemory[..0x67].CopyTo(KirkMemory[0x18..]); // DAT_00009dc4 DAT_00009dd8 + } + else + { + KirkMemory.Slice(0x5c, 0x10).CopyTo(KirkMemory[0x70..]); // DAT_00009e1c DAT_00009e30 + if (type == 6 || type == 7) + { + KirkMemory.Slice(0x3c, 0x20).CopyTo(KirkEcdsaMemory); // DAT_00009dfc DAT_0000a040 + KirkEcdsaMemory[..0x20].CopyTo(KirkMemory[0x50..]); // DAT_0000a040 DAT_00009e10 + KirkMemory.Slice(0x18, 0x38).Span.Fill(0); // DAT_00009dd8 + } + else + { + KirkMemory.Span.Slice(0x18, 0x58).Fill(0); // DAT_00009dd8 + } + + if (b_0xd4 == 0x80) + { + KirkMemory.Span[0x18] = 0x80; // DAT_00009dd8 + } + + KirkMemory[..0x4].CopyTo(KirkMemory[0x04..]); // DAT_00009dc0 + var value = 0x14c; // sha1 hdr size 0x150 - 4 + MemoryMarshal.Write(KirkMemory.Span, ref value); + + KeyData[..0x10].CopyTo(KirkMemory[8..]); // DAT_00009d00 DAT_00009dc8 + } + } + else + { + KirkMemory.Slice(0x04, 0x14).CopyTo(KirkCmd1Memory); // DAT_00009dc4 DAT_00009f40 + var value = 0x14c; // sha1 hdr size 0x150 - 4 + MemoryMarshal.Write(KirkMemory.Span, ref value); + + KeyData[..0x14].CopyTo(KirkMemory[0x4..]); // DAT_00009d00 DAT_00009dc4 + } + + if (use_polling == 0) + { + ret = KIRKEngine.sceUtilsBufferCopyWithRange(KirkMemory.Span, 0x150, KirkMemory.Span, 0x150, + KIRKEngine.KIRK_CMD_SHA1_HASH); + } + else + { + ret = -1; + // ignore + } + if (ret != 0) + { + ret = -6; + return ret; + } + + if (!KirkMemory.Slice(0, 0x14).Span.SequenceEqual(KirkCmd1Memory.Slice(0, 0x14).Span)) + { + ret = 0x12e; + return ret; + } + + ref var cmd1Hdr = ref MemoryMarshal.AsRef(modData.Slice(0x40)); + ref var cmd1ecdsaHdr = ref MemoryMarshal.AsRef(modData.Slice(0x40)); + + if ((1 < type && type < 8) || type == 9 || type == 10) + { + XorKeyLarge(KirkMemory[0x80..].Span, 0x40, KeyData[16..].Span); // DAT_00009e40 - 9dc0 0x80 DAT_00009d10 - 9d00 0x10 + var k4 = KirkMemory.Slice(0x70, 0x10).Span; + unsafe + { + fixed (byte* ptr = &MemoryMarshal.GetReference(KirkMemory[0x6c..].Span)) + { + + } + } + ret = Kirk7(KirkMemory[0x6c..].Span, 0x40, keySeed, use_polling); + if (ret != 0) + { + ret = -7; + return ret; + } + unsafe + { + fixed (byte* ptr = &MemoryMarshal.GetReference(modData[0x40..])) + { + + } + } + XorKeyInto(modData[0x40..], 0x40, KirkMemory[108..].Span, KeyData[80..].Span); + + + if (type == 6 || type == 7) + { + KirkEcdsaMemory[..0x20].Span.CopyTo(modData[0x80..]); // DAT_0000a040 + modData.Slice(0xa0, 0x10).Clear(); + // modData[0xa4] = 1; // cmd1 ecdsa_hash + cmd1ecdsaHdr.ecdsa_hash = 1; + // modData[0xa0] = 1; // cmd1 mode + cmd1ecdsaHdr.mode = 1; + KirkMemory.Slice(0xc0, 0x10).Span.CopyTo(modData.Slice(0xb0)); + modData.Slice(0xc0, 0x10).Clear(); // DAT_00009e80 + KirkMemory.Slice(0xd0, 0x80).Span.CopyTo(modData.Slice(0xd0)); // DAT_00009e90 + } + else + { + // modData.Slice(0x80, 0x30).Fill(0); + cmd1Hdr.content.Slice(0, 0x30).Clear(); + // modData[0xa0] = 1; // cmd1 mode + cmd1Hdr.mode = 1; // cmd1 mode + // KirkMemory.Slice(0xc0, 0x10).Span.CopyTo(modData.Slice(0xb0)); // DAT_00009e80 + KirkMemory.Slice(0xc0, 0x10).Span.CopyTo(cmd1Hdr.off70); // DAT_00009e80 + modData.Slice(0xc0, 0x10).Clear(); + KirkMemory.Slice(0xd0, 0x80).Span.CopyTo(modData.Slice(0xd0)); // psp hdr size 0x80 + } + } + else + { + XorKeyLarge(KirkMemory.Slice(0x40).Span, 0x70, KeyData.Slice(0x14).Span); // DAT_00009e00 DAT_00009d14 + ret = Kirk7(KirkMemory.Slice(0x2c).Span, 0x70, keySeed, use_polling); // DAT_00009dec + if (ret != 0) + { + ret = -16; + return ret; + } + XorKeyInto(modData.Slice(0x40), 0x70, KirkMemory.Slice(0x2c).Span, KeyData.Slice(0x20).Span); // DAT_00009dec DAT_00009d20 + KirkMemory.Slice(0xb0, 0xa0).Span.CopyTo(modData.Slice(0xb0)); // DAT_00009e70 + if (type == 8 && cmd1ecdsaHdr.ecdsa_hash != 1) + { + ret = -0x12f; + return ret; + } + } + + if (b_0xd4 == 0x80) + { + if (modData[0x590] != 0) + { + ret = -0x12e; + return ret; + } + modData[0x590] = 0x80; + } + + if (use_polling == 0) + { + ret = KIRKEngine.sceUtilsBufferCopyWithRange(modData, size, modData[0x40..], + size - 0x40, KIRKEngine.KIRK_CMD_DECRYPT_PRIVATE); + } + else + { + //ignore + ret = -5; + return ret; + } + + return ret; + } + + + + private const int EI_NIDENT = 16; + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + unsafe struct Elf32_Ehdr + { + public fixed byte e_ident[EI_NIDENT]; + public Elf32_Half e_type; + public Elf32_Half e_machine; + public Elf32_Word e_version; + public Elf32_Addr e_entry; /* Entry point */ + public Elf32_Off e_phoff; + public Elf32_Off e_shoff; + public Elf32_Word e_flags; + public Elf32_Half e_ehsize; + public Elf32_Half e_phentsize; + public Elf32_Half e_phnum; + public Elf32_Half e_shentsize; + public Elf32_Half e_shnum; + public Elf32_Half e_shstrndx; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct Elf32_Shdr + { + public Elf32_Word sh_name; + public Elf32_Word sh_type; + public Elf32_Word sh_flags; + public Elf32_Addr sh_addr; + public Elf32_Off sh_offset; + public Elf32_Word sh_size; + public Elf32_Word sh_link; + public Elf32_Word sh_info; + public Elf32_Word sh_addralign; + public Elf32_Word sh_entsize; + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + struct Elf32_Phdr + { + public Elf32_Word p_type; + public Elf32_Off p_offset; + public Elf32_Addr p_vaddr; + public Elf32_Addr p_paddr; + public Elf32_Word p_filesz; + public Elf32_Word p_memsz; + public Elf32_Word p_flags; + public Elf32_Word p_align; + } + + private const int SCE_MODULE_NAME_LEN = 27; + private const int SCE_MODULE_MAX_SEGMENTS = 4; + private const uint PT_LOAD = 1; + + [StructLayout(LayoutKind.Sequential)] + unsafe struct SceModuleInfo + { + public ushort modattribute; + private fixed byte _modversion[2]; + + public Span modversion + { + get + { + fixed (byte* ptr = _modversion) + { + return new Span(ptr, 2); + } + } + } + private fixed byte _modname[SCE_MODULE_NAME_LEN]; + + public Span modname + { + get + { + fixed (byte* ptr = _modname) + { + return new Span(ptr, SCE_MODULE_NAME_LEN); + } + } + } + public byte terminal; + public uint gp_value; + public uint ent_top; + public uint ent_end; + public uint stub_top; + public uint stub_end; + } + + [StructLayout(LayoutKind.Sequential)] + struct PspModuleExport + { + public uint name; + public uint flags; + public byte entry_len; + public byte var_count; + public ushort func_count; + public uint exports; + } + + private static Elf32_Shdr? FindSection(ReadOnlySpan selections, ReadOnlySpan strTable, string name) + { + var nameSpan = Encoding.UTF8.GetBytes(name).AsSpan(); + foreach (var selection in selections) + { + if (nameSpan.SequenceEqual(strTable.Slice((int)selection.sh_name, name.Length))) + { + return selection; + } + } + + return null; + } + + private static void GenDecrypt(Span buff) + { + var hdr = MemoryMarshal.AsRef(buff); + switch (hdr.decryptMode) + { + case SceExecFileDecryptMode.DECRYPT_MODE_NO_EXEC: + break; + case SceExecFileDecryptMode.DECRYPT_MODE_DEMO_EXEC: + case SceExecFileDecryptMode.DECRYPT_MODE_KERNEL_MODULE: + break; + } + } + + struct Cipher + { + public byte[] Key; + public int Seed; + public int Type; + public byte[] XorKey; + } + + + private static readonly Dictionary Ciphers = new Dictionary + { + {0x00000000, new Cipher {Key = SceMemlmd.key_00000000, Seed = 0x42, Type = 0} }, + {0x01000000, new Cipher {Key = SceMemlmd.key_01000000, Seed = 0x43, Type = 0} }, + {0x4C940AF0, new Cipher {Key = SceMemlmd.key_4C940AF0, Seed = 0x43, Type = 2} }, + {0x4C940BF0, new Cipher {Key = SceMemlmd.key_4C940BF0, Seed = 0x43, Type = 2} }, + {0x4C9410F0, new Cipher {Key = SceMemlmd.key_4C9410F0, Seed = 0x43, Type = 2} }, + {0x4C9412F0, new Cipher {Key = SceMemlmd.key_4C9412F0, Seed = 0x43, Type = 2} }, + {0x4C9413F0, new Cipher {Key = SceMemlmd.key_4C9413F0, Seed = 0x43, Type = 2} }, + {0x4C9414F0, new Cipher {Key = SceMemlmd.key_4C9414F0, Seed = 0x43, Type = 2} }, + {0x4C9415F0, new Cipher {Key = SceMemlmd.key_4C9415F0, Seed = 0x43, Type = 2} }, + {0x4C9490F0, new Cipher {Key = SceMemlmd.key_4C9490F0, Seed = 0x43, Type = 9} }, + {0x4C9491F0, new Cipher {Key = SceMemlmd.key_4C9491F0, Seed = 0x43, Type = 9} }, + {0x4C9494F0, new Cipher {Key = SceMemlmd.key_4C9494F0, Seed = 0x43, Type = 9} }, + {0x4C949AF0, new Cipher {Key = SceMemlmd.key_4C949AF0, Seed = 0x43, Type = 9} }, + {0x4C949BF0, new Cipher {Key = SceMemlmd.key_4C949BF0, Seed = 0x43, Type = 9} }, + {0x4C949CF0, new Cipher {Key = SceMemlmd.key_4C949CF0, Seed = 0x43, Type = 9} }, + {0x4C949DF0, new Cipher {Key = SceMemlmd.key_4C949DF0, Seed = 0x43, Type = 9} }, + {0x4C949EF0, new Cipher {Key = SceMemlmd.key_4C949EF0, Seed = 0x43, Type = 9} }, + {0x4C949FF0, new Cipher {Key = SceMemlmd.key_4C949FF0, Seed = 0x43, Type = 9} }, + {0x4C94A0F0, new Cipher {Key = SceMemlmd.key_4C94A0F0, Seed = 0x43, Type = 9} }, + {0x4C94A1F0, new Cipher {Key = SceMemlmd.key_4C94A1F0, Seed = 0x43, Type = 9} }, + + {0x04000000, new Cipher {Key = key_04000000, Seed = 0x47, Type = 0} }, + {0x628910F0, new Cipher {Key = key_628910F0, Seed = 0x47, Type = 2} }, + {0x62891EF0, new Cipher {Key = key_62891EF0, Seed = 0x47, Type = 2} }, + {0x628928F0, new Cipher {Key = key_628928F0, Seed = 0x47, Type = 2} }, + + {0x2FD311F0, new Cipher {Key = key_2FD311F0, Seed = 0x47, Type = 5, XorKey = key_2FD311F0_xor} }, + {0x2FD312F0, new Cipher {Key = key_2FD312F0, Seed = 0x47, Type = 5, XorKey = key_2FD312F0_xor} }, + {0x2FD313F0, new Cipher {Key = key_2FD313F0, Seed = 0x47, Type = 5, XorKey = key_2FD313F0_xor} }, + + {0x05000000, new Cipher {Key = key_05000000, Seed = 0x48, Type = 0} }, + {0x8B9B10F0, new Cipher {Key = key_8B9B10F0, Seed = 0x48, Type = 2} }, + {0x8B9B1EF0, new Cipher {Key = key_8B9B1EF0, Seed = 0x48, Type = 2} }, + {0x8B9B28F0, new Cipher {Key = key_8B9B28F0, Seed = 0x48, Type = 2} }, + + {0x2E5E10F0, new Cipher {Key = key_2E5E10F0, Seed = 0x48, Type = 5, XorKey = key_2E5E10F0_xor} }, + {0x2E5E11F0, new Cipher {Key = key_2E5E11F0, Seed = 0x48, Type = 5, XorKey = key_2E5E11F0_xor} }, + {0x2E5E12F0, new Cipher {Key = key_2E5E12F0, Seed = 0x48, Type = 5, XorKey = key_2E5E11F0_xor} }, + {0x2E5E13F0, new Cipher {Key = key_2E5E13F0, Seed = 0x48, Type = 5, XorKey = key_2E5E11F0_xor} }, + {0x2E5E80F0, new Cipher {Key = key_2E5E80F0, Seed = 0x48, Type = 7, XorKey = key_2E5E80F0_xor} }, + {0x2E5E90F0, new Cipher {Key = key_2E5E90F0, Seed = 0x48, Type = 10, XorKey = key_2E5E90F0_xor} }, + + {0x06000000, new Cipher {Key = key_06000000, Seed = 0x49, Type = 0} }, + {0xE42C2303, new Cipher {Key = key_E42C2303, Seed = 0x49, Type = 2} }, + {0x5A5C10F0, new Cipher {Key = key_5A5C10F0, Seed = 0x49, Type = 2} }, + {0x5A5C1EF0, new Cipher {Key = key_5A5C1EF0, Seed = 0x49, Type = 2} }, + {0x5A5C28F0, new Cipher {Key = key_5A5C28F0, Seed = 0x49, Type = 2} }, + + {0x0A000000, new Cipher {Key = key_0A000000, Seed = 0x4D, Type = 0} }, + {0xEFD210F0, new Cipher {Key = key_EFD210F0, Seed = 0x4D, Type = 2} }, + {0xEFD21EF0, new Cipher {Key = key_EFD21EF0, Seed = 0x4D, Type = 2} }, + {0xEFD228F0, new Cipher {Key = key_EFD228F0, Seed = 0x4D, Type = 2} }, + + {0x0B000000, new Cipher {Key = key_0B000000, Seed = 0x4E, Type = 8} }, + + {0x0E000000, new Cipher {Key = key_0E000000, Seed = 0x51, Type = 0} }, + {0x63BAB403, new Cipher {Key = key_63BAB403, Seed = 0x51, Type = 2} }, + {0xD82310F0, new Cipher {Key = key_D82310F0, Seed = 0x51, Type = 2} }, + {0xD8231EF0, new Cipher {Key = key_D8231EF0, Seed = 0x51, Type = 2} }, + {0xD82328F0, new Cipher {Key = key_D82328F0, Seed = 0x51, Type = 2} }, + + {0x0F000000, new Cipher {Key = key_0F000000, Seed = 0x52, Type = 0} }, + {0x862648D1, new Cipher {Key = key_862648D1, Seed = 0x52, Type = 1} }, + {0x1B11FD03, new Cipher {Key = key_1B11FD03, Seed = 0x52, Type = 2} }, + {0xD13B05F0, new Cipher {Key = key_D13B05F0, Seed = 0x52, Type = 2} }, + {0xD13B06F0, new Cipher {Key = key_D13B06F0, Seed = 0x52, Type = 2} }, + {0xD13B08F0, new Cipher {Key = key_D13B08F0, Seed = 0x52, Type = 2} }, + {0xD13B10F0, new Cipher {Key = key_D13B10F0, Seed = 0x52, Type = 2} }, + {0xD13B1EF0, new Cipher {Key = key_D13B1EF0, Seed = 0x52, Type = 2} }, + {0xD13B28F0, new Cipher {Key = key_D13B28F0, Seed = 0x52, Type = 2} }, + + {0x4467415D, new Cipher {Key = SceMemlmd.key_4467415D, Seed = 0x59, Type = 1} }, + + {0x02000000, new Cipher {Key = key_02000000, Seed = 0x45, Type = 0} }, + {0x380290F0, new Cipher {Key = key_380290F0, Seed = 0x5A, Type = 9} }, + {0x380291F0, new Cipher {Key = key_380291F0, Seed = 0x5A, Type = 9} }, + {0x380292F0, new Cipher {Key = key_380292F0, Seed = 0x5A, Type = 9} }, + {0x380293F0, new Cipher {Key = key_380293F0, Seed = 0x5A, Type = 9} }, + {0x38029AF0, new Cipher {Key = key_38029AF0, Seed = 0x5A, Type = 9} }, + + {0x03000000, new Cipher {Key = key_03000000, Seed = 0x46, Type = 0} }, + {0x3ACE4DCE, new Cipher {Key = key_3ACE4DCE, Seed = 0x5B, Type = 1} }, + {0x76202403, new Cipher {Key = key_76202403, Seed = 0x5B, Type = 2} }, + {0x457B05F0, new Cipher {Key = key_457B05F0, Seed = 0x5B, Type = 2} }, + {0x457B06F0, new Cipher {Key = key_457B06F0, Seed = 0x5B, Type = 2} }, + {0x457B08F0, new Cipher {Key = key_457B08F0, Seed = 0x5B, Type = 2} }, + {0x457B0AF0, new Cipher {Key = key_457B0AF0, Seed = 0x5B, Type = 2} }, + {0x457B0BF0, new Cipher {Key = key_457B0BF0, Seed = 0x5B, Type = 2} }, + {0x457B0CF0, new Cipher {Key = key_457B0CF0, Seed = 0x5B, Type = 2} }, + {0x457B10F0, new Cipher {Key = key_457B10F0, Seed = 0x5B, Type = 2} }, + {0x457B1EF0, new Cipher {Key = key_457B1EF0, Seed = 0x5B, Type = 2} }, + {0x457B28F0, new Cipher {Key = key_457B28F0, Seed = 0x5B, Type = 2} }, + {0x457B80F0, new Cipher {Key = key_457B80F0, Seed = 0x5B, Type = 6} }, + {0x457B8AF0, new Cipher {Key = key_457B8AF0, Seed = 0x5B, Type = 6} }, + {0x457B90F0, new Cipher {Key = key_457B90F0, Seed = 0x5B, Type = 9} }, + {0x457B91F0, new Cipher {Key = key_457B91F0, Seed = 0x5B, Type = 9} }, + {0x457B92F0, new Cipher {Key = key_457B92F0, Seed = 0x5B, Type = 9} }, + {0x457B93F0, new Cipher {Key = key_457B93F0, Seed = 0x5B, Type = 9} }, + {0x457B9AF0, new Cipher {Key = key_457B9AF0, Seed = 0x5B, Type = 9} }, + + {0x08000000, new Cipher {Key = key_08000000, Seed = 0x4B, Type = 0} }, + {0xC0CB167C, new Cipher {Key = key_C0CB167C, Seed = 0x5D, Type = 1} }, + {0x8004FD03, new Cipher {Key = key_8004FD03, Seed = 0x5D, Type = 2} }, + {0xD91605F0, new Cipher {Key = key_D91605F0, Seed = 0x5D, Type = 2} }, + {0xD91606F0, new Cipher {Key = key_D91606F0, Seed = 0x5D, Type = 2} }, + {0xD9160AF0, new Cipher {Key = key_D9160AF0, Seed = 0x5D, Type = 2} }, + {0xD9160BF0, new Cipher {Key = key_D9160BF0, Seed = 0x5D, Type = 2} }, + {0xD91610F0, new Cipher {Key = key_D91610F0, Seed = 0x5D, Type = 2} }, + {0xD91611F0, new Cipher {Key = key_D91611F0, Seed = 0x5D, Type = 2} }, + {0xD91612F0, new Cipher {Key = key_D91612F0, Seed = 0x5D, Type = 2} }, + {0xD91613F0, new Cipher {Key = key_D91613F0, Seed = 0x5D, Type = 2} }, + {0xD91614F0, new Cipher {Key = key_D91614F0, Seed = 0x5D, Type = 2} }, + {0xD91617F0, new Cipher {Key = key_D91617F0, Seed = 0x5D, Type = 2} }, + {0xD91618F0, new Cipher {Key = key_D91618F0, Seed = 0x5D, Type = 2} }, + {0xD9161AF0, new Cipher {Key = key_D9161AF0, Seed = 0x5D, Type = 2} }, + {0xD9161EF0, new Cipher {Key = key_D9161EF0, Seed = 0x5D, Type = 2} }, + {0xD91628F0, new Cipher {Key = key_D91628F0, Seed = 0x5D, Type = 2} }, + {0xD91680F0, new Cipher {Key = key_D91680F0, Seed = 0x5D, Type = 6} }, + {0xD91681F0, new Cipher {Key = key_D91681F0, Seed = 0x5D, Type = 6} }, + {0xD91690F0, new Cipher {Key = key_D91690F0, Seed = 0x5D, Type = 9} }, + + {0x09000000, new Cipher {Key = key_09000000, Seed = 0x4C, Type = 0} }, + {0xBB67C59F, new Cipher {Key = key_BB67C59F, Seed = 0x5E, Type = 1} }, + {0x0A35EA03, new Cipher {Key = key_0A35EA03, Seed = 0x5E, Type = 2} }, + {0x7B0505F0, new Cipher {Key = key_7B0505F0, Seed = 0x5E, Type = 2} }, + {0x7B0506F0, new Cipher {Key = key_7B0506F0, Seed = 0x5E, Type = 2} }, + {0x7B0508F0, new Cipher {Key = key_7B0508F0, Seed = 0x5E, Type = 2} }, + {0x7B0510F0, new Cipher {Key = key_7B0510F0, Seed = 0x5E, Type = 2} }, + {0x7B051EF0, new Cipher {Key = key_7B051EF0, Seed = 0x5E, Type = 2} }, + {0x7B0528F0, new Cipher {Key = key_7B0528F0, Seed = 0x5E, Type = 2} }, + + {0x0C000000, new Cipher {Key = key_0C000000, Seed = 0x4F, Type = 0} }, + {0x7F24BDCD, new Cipher {Key = key_7F24BDCD, Seed = 0x60, Type = 1} }, + {0xD67B3303, new Cipher {Key = key_D67B3303, Seed = 0x60, Type = 2} }, + {0xADF305F0, new Cipher {Key = key_ADF305F0, Seed = 0x60, Type = 4} }, + {0xADF306F0, new Cipher {Key = key_ADF306F0, Seed = 0x60, Type = 4} }, + {0xADF308F0, new Cipher {Key = key_ADF308F0, Seed = 0x60, Type = 4} }, + {0xADF310F0, new Cipher {Key = key_ADF310F0, Seed = 0x60, Type = 4} }, + {0xADF31EF0, new Cipher {Key = key_ADF31EF0, Seed = 0x60, Type = 4} }, + {0xADF328F0, new Cipher {Key = key_ADF328F0, Seed = 0x60, Type = 4} }, + + {0x0D000000, new Cipher {Key = key_0D000000, Seed = 0x50, Type = 0} }, + {0x1BC8D12B, new Cipher {Key = key_1BC8D12B, Seed = 0x61, Type = 1} }, + {0xD66DF703, new Cipher {Key = key_D66DF703, Seed = 0x61, Type = 2} }, + {0x279D05F0, new Cipher {Key = key_279D05F0, Seed = 0x61, Type = 4} }, + {0x279D06F0, new Cipher {Key = key_279D06F0, Seed = 0x61, Type = 4} }, + {0x279D08F0, new Cipher {Key = key_279D08F0, Seed = 0x61, Type = 4} }, + {0x279D10F0, new Cipher {Key = key_279D10F0, Seed = 0x61, Type = 4} }, + {0x279D1EF0, new Cipher {Key = key_279D1EF0, Seed = 0x61, Type = 4} }, + {0x279D28F0, new Cipher {Key = key_279D28F0, Seed = 0x61, Type = 4} }, + + {0x16D59E03, new Cipher {Key = SceMemlmd.key_16D59E03, Seed = 0x62, Type = 2} }, + {0xCFEF05F0, new Cipher {Key = SceMemlmd.key_CFEF05F0, Seed = 0x62, Type = 2} }, + {0xCFEF06F0, new Cipher {Key = SceMemlmd.key_CFEF06F0, Seed = 0x62, Type = 2} }, + {0xCFEF08F0, new Cipher {Key = SceMemlmd.key_CFEF08F0, Seed = 0x62, Type = 2} }, + + {0x0DAA06F0, new Cipher {Key = key_0DAA06F0, Seed = 0x65, Type = 5, XorKey = key_0DAA06F0_xor} }, + {0x0DAA10F0, new Cipher {Key = key_0DAA10F0, Seed = 0x65, Type = 5, XorKey = key_0DAA10F0_xor} }, + {0x0DAA1EF0, new Cipher {Key = key_0DAA1EF0, Seed = 0x65, Type = 5, XorKey = key_0DAA1EF0_xor} }, + {0x0DAA28F0, new Cipher {Key = key_0DAA28F0, Seed = 0x65, Type = 5, XorKey = key_0DAA28F0_xor} }, + + {0x89742B04, new Cipher {Key = key_89742B04, Seed = 0x65, Type = 3, XorKey = key_89742B04_xor} }, + {0xE92408F0, new Cipher {Key = key_E92408F0, Seed = 0x65, Type = 3, XorKey = key_E92408F0_xor} }, + {0xE92410F0, new Cipher {Key = key_E92410F0, Seed = 0x65, Type = 3, XorKey = key_E92410F0_xor} }, + {0xE9241EF0, new Cipher {Key = key_E9241EF0, Seed = 0x65, Type = 3, XorKey = key_E9241EF0_xor} }, + {0xE92428F0, new Cipher {Key = key_E92428F0, Seed = 0x65, Type = 3, XorKey = key_E92428F0_xor} }, + + {0xE1ED06F0, new Cipher {Key = key_E1ED06F0, Seed = 0x66, Type = 5, XorKey = key_E1ED06F0_xor} }, + {0xE1ED10F0, new Cipher {Key = key_E1ED10F0, Seed = 0x66, Type = 5, XorKey = key_E1ED10F0_xor} }, + {0xE1ED1EF0, new Cipher {Key = key_E1ED1EF0, Seed = 0x66, Type = 5, XorKey = key_E1ED1EF0_xor} }, + {0xE1ED28F0, new Cipher {Key = key_E1ED28F0, Seed = 0x66, Type = 5, XorKey = key_E1ED28F0_xor} }, + + {0xF5F12304, new Cipher {Key = key_F5F12304, Seed = 0x66, Type = 3, XorKey = key_F5F12304_xor} }, + {0x692808F0, new Cipher {Key = key_692808F0, Seed = 0x66, Type = 3, XorKey = key_692808F0_xor} }, + {0x692810F0, new Cipher {Key = key_692810F0, Seed = 0x66, Type = 3, XorKey = key_692810F0_xor} }, + {0x69281EF0, new Cipher {Key = key_69281EF0, Seed = 0x66, Type = 3, XorKey = key_69281EF0_xor} }, + {0x692828F0, new Cipher {Key = key_692828F0, Seed = 0x66, Type = 3, XorKey = key_692828F0_xor} }, + + {0x3C2A08F0, new Cipher {Key = key_3C2A08F0, Seed = 0x67, Type = 2} }, + {0x3C2A10F0, new Cipher {Key = key_3C2A10F0, Seed = 0x67, Type = 2} }, + {0x3C2A1EF0, new Cipher {Key = key_3C2A1EF0, Seed = 0x67, Type = 2} }, + {0x3C2A28F0, new Cipher {Key = key_3C2A28F0, Seed = 0x67, Type = 2} }, + + {0x407810F0, new Cipher {Key = key_407810F0, Seed = 0x6A, Type = 5, XorKey = key_407810F0_xor} }, + }; + + public static int Encrypt(Span encData, ReadOnlySpan modData, uint tag, SceExecFileDecryptMode cryptType, ReadOnlySpan versionKey, string contentId) => Encrypt(encData, modData, tag, cryptType, versionKey, contentId, Array.Empty()); + + public static int Encrypt(Span encData, ReadOnlySpan modData, uint tag, SceExecFileDecryptMode cryptType, ReadOnlySpan versionKey, string contentId, ReadOnlySpan config) + { + int ret = -1; + if (!Ciphers.ContainsKey(tag)) + { + return ret; + } + var elfHdr = MemoryMarshal.AsRef(modData); + //var pspHdrSize = Marshal.SizeOf(); + const int pspHdrSize = 0xD0; + var selection = + MemoryMarshal.Cast(modData.Slice((int)elfHdr.e_shoff, + elfHdr.e_shentsize * elfHdr.e_shnum)); + var programHeader = + MemoryMarshal.Cast(modData.Slice((int)elfHdr.e_phoff, + elfHdr.e_phentsize * elfHdr.e_phnum)); + ref var pspHdr = ref MemoryMarshal.AsRef(encData); + pspHdr.magic = 0x5053507E; + //pspHdr.modAttribute = 0x0200; + pspHdr.moduleVerLo = 1; + pspHdr.moduleVerHi = 1; + pspHdr.modVersion = 1; + // pspHdr.nSegments = 2; + pspHdr.elfSize = modData.Length; + pspHdr.bootEntry = elfHdr.e_entry; + // pspHdr.modInfoOffset = 0x2940; + pspHdr.decryptMode = cryptType; + + Elf32_Shdr? sh = null; + SceModuleInfo modInfo; + if (selection.Length > 0) + { + var strSec = selection[elfHdr.e_shstrndx]; + var strTable = modData.Slice((int)strSec.sh_offset, (int)strSec.sh_size); + sh = FindSection(selection, strTable, ".rodata.sceModuleInfo"); + } + if (sh.HasValue) + { + pspHdr.modInfoOffset = (int)sh.Value.sh_offset; + modInfo = MemoryMarshal.Read(modData.Slice((int)sh.Value.sh_offset)); + } + else + { + var ph = programHeader[0]; + pspHdr.modInfoOffset = (int)ph.p_paddr; + modInfo = MemoryMarshal.Read(modData.Slice((int)ph.p_paddr)); + } + var ph0 = programHeader[0]; + var exports = MemoryMarshal.Read(modData.Slice((int)(ph0.p_offset + modInfo.ent_top - ph0.p_vaddr))); + if (exports.var_count > 0) + { + var total = exports.var_count + exports.func_count; + var expNids = MemoryMarshal.Cast(modData.Slice((int)(ph0.p_offset + exports.exports - ph0.p_vaddr), total * 4)); + var expPtrs = MemoryMarshal.Cast(modData.Slice((int)(ph0.p_offset + exports.exports - ph0.p_vaddr + total * 4), total * 4)); + for (var i = exports.func_count; i < total; i++) + { + if (expNids[i] == 0x11B97506) + { + var devkitVersion = MemoryMarshal.Read(modData.Slice((int)(ph0.p_offset + expPtrs[i] - ph0.p_vaddr))); + if (devkitVersion != 0x0c000031) + { + pspHdr.devkitVersion = devkitVersion; + } + break; + } + } + } + + pspHdr.modAttribute = modInfo.modattribute; + + byte j = 0; + + + for (var i = 0; i < elfHdr.e_phnum; i++) + { + if (programHeader[i].p_type == PT_LOAD) + { + if (j > SCE_MODULE_MAX_SEGMENTS) + { + throw new Exception("ERROR: Too many EBOOT PH segments!"); + } + pspHdr.segAlign[j] = (ushort)programHeader[i].p_align; + pspHdr.segAddress[j] = programHeader[i].p_vaddr; + pspHdr.segSize[j] = programHeader[i].p_memsz; + pspHdr.bssSize = programHeader[i].p_memsz - programHeader[i].p_filesz; + j++; + } + } + + pspHdr.nSegments = j; + + bool compress = false; + switch (cryptType) + { + case SceExecFileDecryptMode.DECRYPT_MODE_UMD_GAME_EXEC: + modInfo.modname.CopyTo(pspHdr.modName); + pspHdr.compAttribute = 0; + pspHdr.dataOffset = 0x80; + break; + case SceExecFileDecryptMode.DECRYPT_MODE_POPS_EXEC: + compress = true; + pspHdr.modName[0] = 0x20; + pspHdr.modAttribute = 0x0200; + pspHdr.compAttribute = 1; + pspHdr.dataOffset = 0x890; + break; + } + + + if (compress) + { + using var ms = new MemoryStream(); + using (var gs = new Ionic.Zlib.GZipStream(ms, Ionic.Zlib.CompressionMode.Compress, true)) + { + gs.LastModified = new System.DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + gs.Write(modData); + } + var tmp = ms.ToArray(); + tmp[9] = 3; + modData = tmp.AsSpan(); + } + modData.CopyTo(encData.Slice(pspHdrSize + pspHdr.dataOffset)); + var chk_size = (modData.Length + 15) / 16 * 16; + pspHdr.dataSize = modData.Length; + pspHdr.pspSize = pspHdrSize + pspHdr.dataOffset + chk_size; + + // MemoryMarshal.Write(encData, ref pspHdr); + + //var pspHdrSpan = MemoryMarshal.CreateReadOnlySpan(ref pspHdr, 1); + //var pspHdrBuffer = MemoryMarshal.AsBytes(pspHdrSpan); + Span randomSpan = null; + + if (cryptType == SceExecFileDecryptMode.DECRYPT_MODE_POPS_EXEC) + { + //randomSpan = new byte[] + //{ + //0x01, 0x94, 0xB1, 0x16, 0x3D, 0x01, 0x42, 0x0F, 0xC7, 0x03, 0xDE, 0x10, 0x1C, 0x50, 0x91, 0x52, + //0x73, 0x31, 0x52, 0xD7, 0xAF, 0xB4, 0x0A, 0xBF, 0x1C, 0xA5, 0x39, 0xCA, 0x1F, 0xBF, 0xA1, 0x0C, + //0xF6, 0x1A, 0x52, 0x53, 0x7E, 0x68, 0x2F, 0xDE, 0x66, 0xC3, 0x82, 0x31, 0x08, 0x1A, 0x18, 0xC3, + //0xFC, 0xE5, 0x83, 0xD5, 0xA6, 0x8F, 0xBC, 0x36, 0xD9, 0x17, 0x8E, 0x00, 0xEC, 0x5A, 0x49, 0x63, + //0xEF, 0xA2, 0x7E, 0x89, 0x35, 0x10, 0x0D, 0x28, 0xFE, 0x67, 0xF1, 0xD9, 0xA5, 0x73, 0x17, 0x73, + //0x86, 0x5B, 0xF1, 0x86, 0x06, 0x73, 0xCB, 0x18, 0xB8, 0xF9, 0x0B, 0x61, 0xDD, 0xB0, 0xC0, 0x56, + //0x3A, 0x96, 0xED, 0x1E, 0xE6, 0x8C, 0x37, 0xBE, 0xA2, 0xA1, 0x61, 0xDE, 0x7D, 0x48, 0xA3, 0x21, + //0x3C, 0x06, 0xBF, 0xB0, 0xB2, 0x40, 0x8C, 0x46, 0xED, 0x7B, 0x52, 0x01, 0x4F, 0xDD, 0x85, 0x43, + //0x09, 0x61, 0x65, 0x8F, 0xCC, 0x53, 0x29, 0xCC, 0xDC, 0x48, 0x99, 0x8E, 0x73, 0x09, 0x67, 0x68, + //0x93, 0x0A, 0xF5, 0x04, 0xFC, 0x3E, 0x51, 0xBB, 0x04, 0xD3, 0x70, 0xF9, 0xD0, 0xB0, 0xEA, 0x6D, + //0xC8, 0xD7, 0xDE, 0x29, 0x91, 0xB0, 0xC9, 0xA0, 0xE9, 0x25, 0x17, 0x2A, 0x61, 0xF4, 0xC0, 0x9B, + //0xEE, 0xD4, 0xCF, 0x55, 0x9F, 0x0C, 0xA9, 0x06, 0xE3, 0x79, 0xF7, 0x80, 0x85, 0xB5, 0x3C, 0x0E, + //0x6D, 0xC1, 0x5D, 0x38, 0x2F, 0x99, 0x74, 0xCA, 0xC0, 0xAC, 0x7F, 0x3C, 0x7C, 0xF5, 0xB5, 0xA9, + //0x85, 0x27, 0xC8, 0xF2, 0xEE, 0x7D, 0xCF, 0xE6, 0x53, 0x99, 0xF4, 0x14, 0x28, 0x20, 0x42, 0x3E, + //0x13, 0x7F, 0xB9, 0x60, 0x9E, 0xC4, 0xAC, 0xE5, 0x02, 0x8B, 0x36, 0x9A, 0xB3, 0x89, 0x46, 0x33, + //0xBC, 0x4D, 0x79, 0xD5, 0x1F, 0x8D, 0x36, 0xB5, 0xAB, 0x01, 0x0F, 0xFE, 0xC2, 0x09, 0xFD, 0xD8, + //0x4B, 0x12, 0x9F, 0x05, 0xF0, 0x6B, 0x3C, 0xBD, 0x3F, 0x65, 0xF5, 0x5B, 0x86, 0x19, 0x11, 0x20, + //0x53, 0xB1, 0x80, 0x32, 0x62, 0x3E, 0x00, 0x11, 0x9E, 0x8D, 0x4B, 0x57, 0x56, 0x48, 0xBC, 0x5B, + //0x32, 0x90, 0xA1, 0x9E, 0x05, 0xD1, 0xEB, 0x8C, 0x12, 0xA7, 0x4D, 0x33, 0x15, 0xCB, 0xC0, 0xD2, + //0x8F, 0x44, 0x98, 0x6F, 0x4B, 0x81, 0x44, 0x58, 0xD0, 0x5F, 0x88, 0xA5, 0x22, 0x06, 0x76, 0x9F, + //0x15, 0xB2, 0x40, 0xF2, 0x3C, 0x2B, 0x55, 0x6E, 0x1B, 0x9B, 0xCE, 0x11, 0x37, 0x75, 0xA6, 0xBE, + //0xE3, 0xA7, 0xFF, 0x30, 0xE8, 0x5C, 0x7B, 0xAF, 0x1C, 0xFC, 0x63, 0x4A, 0x5C, 0xE4, 0xCF, 0xBD, + //0x68, 0x89, 0xC8, 0xDF, 0x89, 0x63, 0x61, 0x71, 0x8C, 0xCD, 0xB2, 0x21, 0x23, 0x55, 0x5E, 0x8E, + //0x6F, 0x04, 0x89, 0xC0, 0x4E, 0xF6, 0x3A, 0x74, 0x89, 0xBA, 0xF4, 0x51, 0x9A, 0x5D, 0x09, 0x2C, + //0x76, 0x48, 0x9F, 0x86, 0xCD, 0xF3, 0x66, 0xA6, 0xBF, 0x78, 0xC7, 0xD0, 0x95, 0x48, 0x4E, 0x31, + //0x2B, 0xBC, 0x73, 0x70, 0x95, 0x7C, 0xE9, 0x1E, 0x8F, 0x55, 0x35, 0x83, 0x37, 0x65, 0x9C, 0x34, + //0x98, 0x15, 0x6F, 0x70, 0x31, 0x7C, 0xED, 0x46, 0x2A, 0xC4, 0x3E, 0xAE, 0x55, 0x49, 0x74, 0x37, + //0x81, 0x02, 0x5C, 0x73, 0x79, 0xA3, 0x6E, 0xCF, 0x37, 0xFB, 0x0F, 0x9F, 0xCC, 0x1E, 0xC1, 0x73, + //0xF5, 0xE1, 0x8B, 0x84, 0x52, 0xF2, 0x6B, 0xD3, 0xE7, 0x29, 0x69, 0xB1, 0xD9, 0xE9, 0xEB, 0x95, + //0x6D, 0xD3, 0x00, 0xC2, 0x7C, 0x5D, 0xDF, 0x84, 0xA6, 0x8D, 0xB0, 0x2E, 0xC2, 0x41, 0xC1, 0x49, + //0x2A, 0x61, 0xAA, 0x3B, 0x40, 0x98, 0x6F, 0x36, 0x3D, 0xAB, 0x31, 0x08, 0x50, 0xD1, 0x5C, 0xCC, + //0x0E, 0x6F, 0x7C, 0xD4, 0x98, 0x96, 0x73, 0x13, 0x9D, 0xD8, 0xF5, 0x4E, 0x81, 0x08, 0xEA, 0x2A, + //0x7E, 0x1A, 0x8A, 0x86, 0xC5, 0x70, 0x14, 0x15, 0xC6, 0x3F, 0x00, 0x86, 0x8C, 0x9F, 0x20, 0x29, + //0x7B, 0xF4, 0xD2, 0xDE, 0xB1, 0x8A, 0x3F, 0xE9, 0x58, 0x00, 0x6B, 0x19, 0x0C, 0xC1, 0xF9, 0x56, + //0x95, 0x29, 0xCC, 0x95, 0x29, 0x56, 0x16, 0xE3, 0xBA, 0xCE, 0xCA, 0x5C, 0xD5, 0x92, 0xB0, 0x49, + //0xBA, 0xBD, 0x9B, 0xE1, 0x0E, 0x22, 0x9A, 0x2B, 0x3A, 0x94, 0x1F, 0xB4, 0x74, 0x24, 0x1B, 0xBE, + //0xF3, 0x07, 0x7A, 0xD0, 0xCF, 0x6F, 0x3C, 0xC2, 0x5B, 0xA1, 0x08, 0x3A, 0xA6, 0x9F, 0xE5, 0xB1, + //0x95, 0x82, 0x9F, 0x1D, 0xC3, 0xAA, 0xEA, 0x8B, 0x75, 0x1C, 0x63, 0xA5, 0x3D, 0xAF, 0x6B, 0x70, + //0x21, 0x59, 0x4E, 0x50, 0x83, 0xF0, 0xCD, 0x7D, 0x83, 0x34, 0x68, 0x78, 0x71, 0xB1, 0x47, 0x2E, + //0x48, 0xB5, 0xCC, 0xE1, 0xAF, 0x8A, 0x04, 0xBD, 0xF4, 0x4B, 0xB7, 0x1F, 0xB5, 0x46, 0x47, 0x02, + //0xD7, 0xE8, 0x97, 0xDE, 0x53, 0x94, 0x41, 0xE3, 0x6F, 0x26, 0x9B, 0x4F, 0x97, 0x60, 0xAA, 0xA1, + //0x2F, 0x1C, 0xD3, 0x27, 0x99, 0x92, 0x21, 0x08, 0x32, 0x4F, 0x44, 0x4C, 0xA7, 0x1C, 0x73, 0xC6, + //0x7A, 0xCF, 0xC4, 0xBD, 0x7B, 0xAB, 0x17, 0x37, 0x33, 0xF2, 0x76, 0x2C, 0x65, 0xD6, 0x85, 0xB8, + //0x15, 0xBA, 0xC7, 0x82, 0x05, 0x65, 0x60, 0x0C, 0xC8, 0x1D, 0x32, 0xF6, 0xAB, 0x12, 0xD6, 0x59, + //0xA7, 0xC7, 0x06, 0x5C, 0x2F, 0x2D, 0xEC, 0xE7, 0x82, 0xF5, 0x40, 0x7D, 0x58, 0x80, 0xB2, 0x61, + //0xA8, 0x95, 0x58, 0xF5, 0xEA, 0x93, 0x5F, 0xEA, 0xAD, 0x77, 0x1C, 0x8E, 0xC3, 0x7A, 0x1B, 0xCF, + //0x31, 0x53, 0x8D, 0xA9, 0x86, 0xC6, 0x03, 0x09, 0x5D, 0x23, 0xF2, 0x25, 0xE4, 0x62, 0xAB, 0xC4, + //0x71, 0xA6, 0x3D, 0x0B, 0xDA, 0x3A, 0x24, 0xF6, 0x85, 0x3E, 0xD1, 0x13, 0x28, 0xEC, 0x58, 0xA3, + //0xC6, 0x18, 0x91, 0x75, 0x0E, 0xD6, 0x06, 0x6C, 0x89, 0xA1, 0xA7, 0xDE, 0xBF, 0x59, 0xD4, 0x2C, + //0xE4, 0x8C, 0x08, 0x15, 0x13, 0xF5, 0xD2, 0xD6, 0x6D, 0xC8, 0xEB, 0xE9, 0x9F, 0x9E, 0x19, 0x39, + //0x81, 0x7B, 0xE2, 0x89, 0x07, 0x1B, 0x98, 0xE3, 0x20, 0x18, 0xFE, 0xEF, 0xFD, 0x75, 0xED, 0x6D, + //0x9E, 0x1B, 0x21, 0x97, 0x17, 0x19, 0x59, 0xD5, 0xCA, 0xD1, 0xA0, 0xED, 0x7F, 0xAE, 0xEF, 0x8E, + //0x38, 0x8E, 0xAC, 0x1A, 0xC4, 0x0E, 0x4D, 0xDE, 0x03, 0xF5, 0x93, 0x95, 0x9C, 0xC9, 0x5E, 0x00, + //0x3A, 0x4E, 0x46, 0x8F, 0xA3, 0xAB, 0x6D, 0x61, 0x49, 0x6E, 0xCC, 0x0E, 0x8E, 0x4B, 0xC6, 0x91, + //0xB2, 0x08, 0x79, 0x57, 0x2F, 0x29, 0x76, 0xFA, 0x09, 0x8A, 0x59, 0xF9, 0x2B, 0x6E, 0x48, 0xF4, + //0x06, 0xF6, 0x82, 0xE8, 0xFC, 0x3F, 0xFC, 0x5C, 0xD8, 0x3B, 0x02, 0xD3, 0xC5, 0x20, 0x0C, 0xBB, + //0x53, 0x63, 0x06, 0xEF, 0x67, 0x85, 0xE4, 0xAB, 0x1E, 0x62, 0xEB, 0x20, 0x31, 0xE0, 0x47, 0xCD, + //0x2A, 0xAE, 0xF1, 0x85, 0x55, 0x69, 0xE5, 0xE2, 0xC1, 0x9A, 0x3D, 0xF3, 0x74, 0x75, 0x4F, 0xCD, + //0x94, 0x2C, 0xFB, 0x9B, 0x29, 0x81, 0x82, 0x07, 0x79, 0xC4, 0xB2, 0xD7, 0x62, 0x43, 0x78, 0x7C, + //0xF2, 0xC1, 0xAF, 0x37, 0xAA, 0x9A, 0xDA, 0xDB, 0x7D, 0xCC, 0xA2, 0xC7, 0x54, 0x56, 0xB6, 0x76, + //0xCF, 0x57, 0xE5, 0x0E, 0x2E, 0xB9, 0xF4, 0x68, 0x99, 0x36, 0xEE, 0xAF, 0x90, 0x21, 0x8F, 0xC8, + //0x5C, 0x92, 0x0A, 0x99, 0xDD, 0x6A, 0x2A, 0xB5, 0x74, 0xB9, 0xAC, 0xCC, 0x64, 0xA3, 0xA4, 0xC7, + //0xBD, 0xEE, 0x9A, 0xCD, 0x60, 0x3B, 0xBB, 0xB8, 0xE2, 0xAF, 0xE0, 0x91, 0x69, 0x37, 0x61, 0x63, + //0x04, 0x97, 0x52, 0x49, 0x86, 0x89, 0xC5, 0x80, 0xAA, 0x72, 0xA9, 0xEA, 0x87, 0xD0, 0x3A, 0xDA, + //0x7E, 0xAE, 0x4A, 0x84, 0x83, 0xC7, 0x1F, 0x12, 0xB9, 0x2E, 0x6B, 0x8E, 0xAC, 0x62, 0x5A, 0xF8 + //}; + //randomSpan.CopyTo(encData.Slice(0x150)); + config.CopyTo(encData[0x150..]); + //RandomNumberGenerator.Fill(encData.Slice(0x150, 0x410)); + + // JP9000-NPJI90001_00-0000000000000001 + // JP0082-NPJJ00294_00-0000000000000001 + Encoding.ASCII.GetBytes(contentId, encData[(0x150 + 0x410)..]); + int tmp = 0x01000000; + MemoryMarshal.Write(encData[0x590..], ref tmp); + } + else if (pspHdr.dataOffset > 0x80) + { + RandomNumberGenerator.Fill(encData.Slice(0x150, pspHdr.dataOffset - 0x80)); + } + + var cipher = Ciphers[tag]; + + var type = cipher.Type; + var keySeed = cipher.Seed; + var xorKey = cipher.XorKey.AsSpan(); + + // ENCRYPT DATA + Span aesKeys = stackalloc byte[32]; + using (var aes = AesHelper.CreateAes()) + { + // aes.Key = new byte[] { 0x37, 0x49, 0x0A, 0x87, 0xA3, 0xB3, 0x7B, 0x50, 0x47, 0x4E, 0x21, 0x2B, 0x4A, 0x0F, 0xA6, 0xC8 }; + aes.Key.AsSpan().CopyTo(aesKeys); + AesHelper.AesEncrypt(aes, encData.Slice(pspHdrSize + pspHdr.dataOffset), encData.Slice(pspHdrSize + pspHdr.dataOffset, modData.Length)); + } + + // CMAC HASHES + ref var cmd1Hdr = ref MemoryMarshal.AsRef(encData.Slice(0x40)); + using (var aesCmac = AesHelper.CreateAes()) + { + cmd1Hdr.mode = KIRKEngine.KIRK_MODE_CMD1; + if (type == 6 || type == 7) + { + cmd1Hdr.ecdsa_hash = 1; + } + encData[..0x80].CopyTo(encData.Slice(0x40 + 0x90)); + + // aesCmac.Key = new byte[] { 0xBE, 0xAF, 0x2D, 0x80, 0xBF, 0xC1, 0xCC, 0x53, 0x3B, 0xCF, 0x18, 0x7B, 0x95, 0xFA, 0xC7, 0xCC }; + aesCmac.Key.AsSpan().CopyTo(aesKeys.Slice(16)); + AesHelper.Cmac(aesCmac, cmd1Hdr.CMAC_header_hash, encData.Slice(0xA0, 0x30)); + AesHelper.Cmac(aesCmac, cmd1Hdr.CMAC_data_hash, encData.Slice(0xA0, 0x30 + pspHdr.dataOffset + chk_size)); + //cmac_header_hash.CopyTo(pspHdr.cmacHeaderHash); + //cmac_data_hash.CopyTo(pspHdr.cmacDataHash); + } + + // ENCRYPT KEYS + using (var kirkAes = AesHelper.CreateKirkAes()) + { + AesHelper.AesEncrypt(kirkAes, cmd1Hdr.AESKeys, aesKeys); + } + + ret = BuildKeyData(keySeed, 0, type, cipher.Key, versionKey); + if (ret != 0) + { + return ret; + } + + KirkMemory.Span.Fill(0); + KirkCmd1Memory.Span.Fill(0); + KirkEcdsaMemory.Span.Fill(0); + + if ((1 < type && type < 8) || type == 9 || type == 10) + { + if (type == 6 || type == 7) + { + // modData.Slice(0x80).CopyTo(buf5.Slice(0, 0x20)); + encData.Slice(0xb0, 0x10).CopyTo(KirkMemory.Slice(0xc0).Span); + encData.Slice(0xd0, 0x80).CopyTo(KirkMemory.Slice(0xd0).Span); + encData.Slice(0xd0, 0x80).Fill(0); + } + else + { + encData.Slice(0xb0, 0x10).CopyTo(KirkMemory.Slice(0xc0).Span); + encData.Slice(0xd0, 0x80).CopyTo(KirkMemory.Slice(0xd0).Span); + encData.Slice(0xd0, 0x80).Fill(0); + } + + unsafe + { + fixed (byte* ptr = encData.Slice(0x40)) + { + + } + + fixed (byte* ptr1 = KirkMemory.Slice(0x6c).Span) + { + + } + } + + XorKeyInto(KirkMemory.Slice(0x80).Span, 0x40, encData.Slice(0x40), KeyData.Slice(80).Span); + ret = Kirk4(KirkMemory.Slice(0x6c).Span, 0x40, keySeed, 0); + if (ret != 0) + { + ret = -7; + return ret; + } + XorKeyLarge(KirkMemory.Slice(0x80).Span, 0x40, KeyData.Slice(16).Span); + Span k4 = stackalloc byte[] { 0x4A, 0xB8, 0x0B, 0x41, 0xA1, 0xDF, 0x55, 0x0D, 0xFC, 0xD1, 0xA6, 0xA6, 0x84, 0xFE, 0xCE, 0x6A }; + //if (randomSpan != null) + //{ + // using (var aes = AesHelper.CreateAes()) + // { + // aes.Key = k4.ToArray(); + // AesHelper.AesDecrypt(aes, randomSpan, randomSpan, 0x410); + // } + //} + RandomNumberGenerator.Fill(k4); + k4.CopyTo(KirkMemory.Slice(0x70).Span); + if (type == 4) + { + + } + else + { + + + if (type == 6 || type == 7) + { + // TODO + } + else + { + KirkMemory.Span.Slice(0x18, 0x58).Fill(0); + } + // if (b_0xd4 == 0x80) + // KirkMemory.Span[0x18] = 0x80; + + var value = 0x14c; + MemoryMarshal.Write(KirkMemory.Span, ref value); + MemoryMarshal.Write(KirkMemory.Slice(4).Span, ref tag); + KeyData[..0x10].CopyTo(KirkMemory[8..]); + } + } + else + { + // TODO + } + + ret = KIRKEngine.sceUtilsBufferCopyWithRange(KirkCmd1Memory.Span, 0x150, KirkMemory.Span, 0x150, + KIRKEngine.KIRK_CMD_SHA1_HASH); + if (ret != 0) + { + ret = -6; + return ret; + } + + + if ((1 < type && type < 8) || type == 9 || type == 10) + { + if (type == 4) + { + KirkMemory.Slice(0x18, 0x67).CopyTo(KirkMemory); + } + else + { + KirkMemory.Slice(0x70, 0x10).CopyTo(KirkMemory.Slice(0x5c)); + if (type == 6 || type == 7) + { + KirkMemory.Slice(0x50, 0x20).CopyTo(KirkEcdsaMemory); + KirkEcdsaMemory[..0x20].CopyTo(KirkMemory[0x3c..]); + } + } + KirkCmd1Memory[..0x14].CopyTo(KirkMemory[0x6c..]); + } + else + { + // TODO + } + + if (type == 1) + { + // TODO + } + else + { + unsafe + { + fixed (byte* ptr = &MemoryMarshal.GetReference(KirkCmd1Memory.Slice(0x14).Span)) + { + + } + } + + KirkMemory.Slice(0x5c, 0x60).CopyTo(KirkCmd1Memory.Slice(0x14)); + ret = Kirk4(KirkCmd1Memory.Span, 0x60, keySeed, 0); + if (ret != 0) + { + return -5; + } + + if (type == 3 || type == 5 || type == 7 || type == 10) + { + XorKey(KirkCmd1Memory.Span.Slice(0x14), 0x60, xorKey); + } + + if ((1 < type && type < 8) || type == 9 || type == 10) + { + KirkCmd1Memory.Slice(0x14, 0x60).CopyTo(KirkMemory.Slice(0x5c)); + } + } + + if (type == 3) + { + // TODO + } + else if (type == 5 || type == 7 || type == 10) + { + KirkMemory.Slice(0xd0, 0x80).Span.CopyTo(pspHdr.RawHdr); + KirkMemory.Slice(0xc0, 0x10).Span.CopyTo(pspHdr.sizeInfo); + KirkMemory.Slice(0xb0, 0x10).CopyTo(KirkCmd1Memory.Slice(0x30)); + KirkMemory.Slice(0x80, 0x30).CopyTo(KirkCmd1Memory); + KirkMemory.Slice(0x6c, 0x10).CopyTo(KirkCmd1Memory.Slice(0x40)); + KirkMemory.Slice(0x6c, 0x14).Span.CopyTo(pspHdr.sha1Hash); + KirkMemory.Slice(0x5c, 0x10).Span.CopyTo(pspHdr.keyData4); + + if (type == 7) + { + KirkMemory.Slice(0x3c).Span.CopyTo(pspHdr.sCheck.Slice(56, 0x20)); + } + + KirkCmd1Memory[..0x50].CopyTo(KirkMemory[0x14..]); + ret = Kirk4(KirkMemory.Span, 0x50, keySeed, 0); + if (ret != 0) + { + return -13; + } + XorKey(KirkMemory.Slice(0x14).Span, 0x50, xorKey, versionKey); + KirkMemory.Slice(0x14, 0x30).Span.CopyTo(pspHdr.keyData); + KirkMemory.Slice(0x44, 0x10).Span.CopyTo(pspHdr.cmacDataHash); + KirkMemory.Slice(0x54, 0x10).Span.CopyTo(pspHdr.sha1Hash); + pspHdr.tag = tag; + } + else if (type == 2 || type == 4 || type == 6 || type == 9) + { + KirkMemory.Slice(0x5c, 0x10).Span.CopyTo(pspHdr.keyData4); + KirkMemory.Slice(0x6c, 0x14).Span.CopyTo(pspHdr.sha1Hash); + KirkMemory.Slice(0x80, 0x30).Span.CopyTo(pspHdr.keyData); + KirkMemory.Slice(0xb0, 0x10).Span.CopyTo(pspHdr.cmacDataHash); + KirkMemory.Slice(0xc0, 0x10).Span.CopyTo(pspHdr.sizeInfo); + KirkMemory.Slice(0xd0, 0x80).Span.CopyTo(pspHdr.RawHdr); + pspHdr.tag = tag; + pspHdr.sCheck.Clear(); + } + else + { + KirkMemory[..0x80].Span.CopyTo(pspHdr.CheckData); + KirkMemory.Slice(0x80, 0x30).Span.CopyTo(pspHdr.keyData); + KirkMemory.Slice(0xd0, 0x80).Span.CopyTo(pspHdr.RawHdr); + } + + return ret; + } + } +} diff --git a/PspCrypto/SceNpDrm.cs b/PspCrypto/SceNpDrm.cs new file mode 100644 index 0000000..020c90b --- /dev/null +++ b/PspCrypto/SceNpDrm.cs @@ -0,0 +1,1286 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using PspCrypto.Security.Cryptography; + +namespace PspCrypto +{ + public static class SceNpDrm + { + unsafe struct SceEbootPbp + { + public ulong Magic; + public int KeyType; + public int Type; + private fixed byte _contentId[0x30]; + + public Span ContentId + { + get + { + fixed (byte* ptr = _contentId) + { + return new Span(ptr, 0x30); + } + } + } + + public ulong Aid; + public ulong SecureTick; + public long PbpSize; + public int SwVer; + public int DiscCount; + private fixed long _discOffsets[6]; + + public Span DiscOffsets + { + get + { + fixed (long* ptr = _discOffsets) + { + return new Span(ptr, 6); + } + } + } + private fixed byte _padding[0xC8]; + private fixed byte _pbpHdrSig[0x38]; + + public Span PbpHdrSig + { + get + { + fixed (byte* ptr = _pbpHdrSig) + { + return new Span(ptr, 0x38); + } + } + } + private fixed byte _npUmdImgSig[0x38]; + + public Span NpUmdImgSig + { + get + { + fixed (byte* ptr = _npUmdImgSig) + { + return new Span(ptr, 0x38); + } + } + } + private fixed byte _sig[0x38]; + + public Span Sig + { + get + { + fixed (byte* ptr = _sig) + { + return new Span(ptr, 0x38); + } + } + } + } + unsafe struct SceEbootPbp100 + { + public ulong Magic; + public int KeyType; + public int Type; + private fixed byte _contentId[0x30]; + + public Span ContentId + { + get + { + fixed (byte* ptr = _contentId) + { + return new Span(ptr, 0x30); + } + } + } + private fixed byte _padding[0x18]; + private fixed byte _pbpHdrSig[0x38]; + + public Span PbpHdrSig + { + get + { + fixed (byte* ptr = _pbpHdrSig) + { + return new Span(ptr, 0x38); + } + } + } + private fixed byte _npUmdImgSig[0x38]; + + public Span NpUmdImgSig + { + get + { + fixed (byte* ptr = _npUmdImgSig) + { + return new Span(ptr, 0x38); + } + } + } + private fixed byte _sig[0x38]; + + public Span Sig + { + get + { + fixed (byte* ptr = _sig) + { + return new Span(ptr, 0x38); + } + } + } + } + + unsafe struct sceDiscInfo + { + private fixed byte _id[0x30]; + + public Span Id + { + get + { + fixed (byte* ptr = _id) + { + return new Span(ptr, 0x30); + } + } + } + + public int Version; + public int DiscCount; + public long FileSize; + private fixed long _diskOffsets[6]; + + public Span Offsets + { + get + { + fixed (long* ptr = _diskOffsets) + { + return new Span(ptr, 6); + } + } + } + + private fixed byte _pad[0x20]; + private fixed byte _discsSig[0x38]; + + public Span DiscsSig + { + get + { + fixed (byte* ptr = _discsSig) + { + return new Span(ptr, 0x38); + } + } + } + private fixed byte _sig[0x38]; + + public Span Sig + { + get + { + fixed (byte* ptr = _sig) + { + return new Span(ptr, 0x38); + } + } + } + + } + + public static ulong Aid { get; set; } + + public static byte[] SceDiskInfo => Resource.__sce_discinfo; + + private const int BLK_SIZE = 0x7C0; + + private static readonly Memory _memory = new byte[0x1000]; + + private static byte[] _psId; + + private static long _fuseId; + + public static void SetPSID(byte[] psid) + { + _psId = psid; + } + + public static void SetFuseId(long fuseId) + { + _fuseId = fuseId; + } + + public static void SetFuseId(Span fuseId) + { + SetFuseId(MemoryMarshal.Read(fuseId)); + } + + public unsafe struct SceNpDrmKey + { + private fixed byte _KeyData[16]; + public Span KeyData + { + get + { + fixed (byte* ptr = _KeyData) + { + return new Span(ptr, 16); + } + } + } + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct Rif + { + private short _version; + private short _versionFlag; + private int _drmType; + private fixed byte _accountId[8]; + private fixed byte _contentId[0x30]; + private fixed byte _encKey1[0x10]; + private fixed byte _encKey2[0x10]; + private long _startTime; + private long _endTime; + private fixed byte _ecdsaSig[0x28]; + public short Version => _version; + public short VersionFlag => _versionFlag; + public int DrmType => _drmType; + public Span AccountId + { + get + { + fixed (byte* ptr = _accountId) + { + return new Span(ptr, 8); + } + } + } + public Span ContentId + { + get + { + fixed (byte* ptr = _contentId) + { + return new Span(ptr, 0x30); + } + } + } + public Span EncKey1 + { + get + { + fixed (byte* ptr = _encKey1) + { + return new Span(ptr, 0x10); + } + } + } + public Span EncKey2 + { + get + { + fixed (byte* ptr = _encKey2) + { + return new Span(ptr, 0x10); + } + } + } + public long StartTime => _startTime; + public long EndTime => _endTime; + + public Span EcdsaSig + { + get + { + fixed (byte* ptr = _ecdsaSig) + { + return new Span(ptr, 0x28); + } + } + } + } + + [StructLayout(LayoutKind.Sequential)] + public unsafe struct Act + { + private short _actType; + private short _versionFlag; + private int _version; + private fixed byte _accountId[8]; + private fixed byte _primKeyTable[0x800]; + private fixed byte _unk1[0x40]; + private fixed byte _openPsId[0x10]; + private fixed byte _unk2[0x10]; + private fixed byte _unk4[0x10]; + private fixed byte _secondTable[0x650]; + private fixed byte _rsaSig[0x100]; + private fixed byte _unkSig[0x40]; + private fixed byte _ecdsaSig[0x28]; + public short ActType => _actType; + public short VersionFlag => _versionFlag; + public int Version => _version; + public Span AccountId + { + get + { + fixed (byte* ptr = _accountId) + { + return new Span(ptr, 8); + } + } + } + public Span PrimKeyTable + { + get + { + fixed (byte* ptr = _primKeyTable) + { + return new Span(ptr, 0x800); + } + } + } + public Span Unk1 + { + get + { + fixed (byte* ptr = _unk1) + { + return new Span(ptr, 0x40); + } + } + } + public Span OpenPsId + { + get + { + fixed (byte* ptr = _openPsId) + { + return new Span(ptr, 0x10); + } + } + } + public Span Unk2 + { + get + { + fixed (byte* ptr = _unk2) + { + return new Span(ptr, 0x10); + } + } + } + public Span Unk4 + { + get + { + fixed (byte* ptr = _unk4) + { + return new Span(ptr, 0x10); + } + } + } + public Span SecondTable + { + get + { + fixed (byte* ptr = _secondTable) + { + return new Span(ptr, 0x650); + } + } + } + public Span RsaSig + { + get + { + fixed (byte* ptr = _rsaSig) + { + return new Span(ptr, 0x100); + } + } + } + public Span UnkSig + { + get + { + fixed (byte* ptr = _unkSig) + { + return new Span(ptr, 0x40); + } + } + } + public Span EcdsaSig + { + get + { + fixed (byte* ptr = _ecdsaSig) + { + return new Span(ptr, 0x28); + } + } + } + + } + + public static int sceNpDrmGetIDps(Span idps) + { + if (_psId != null && idps != null) + { + _psId.CopyTo(idps); + return 0; + } + return -0x7faaf6ff; + } + + private static int GetActKey(Span key, ReadOnlySpan keyTable, int count) + { + Span idps = stackalloc byte[16]; + Span decKey = stackalloc byte[16]; + int ret = sceNpDrmGetIDps(idps); + if (ret > -1) + { + ret = AesHelper.AesEncrypt(KeyVault.drmActdatKey, decKey, idps); + if (ret == 16) + { + for (int i = 0; i < count; i++) + { + ret = AesHelper.AesDecrypt(keyTable[(i * 0x10)..], key[(i * 0x10)..], decKey); + if (ret != 16) + { + ret = unchecked((int)0x80550902); + break; + } + } + ret = 0; + } + } + idps.Clear(); + decKey.Clear(); + return ret; + } + + private static int GetFreeVersionKey(Span versionKey, ReadOnlySpan rifBuf) + { + return -1; + } + + private static int GetLicenseVersionKey(Span versionKey, ReadOnlySpan actBuf, ReadOnlySpan rifBuf) + { + Span buf1 = stackalloc byte[16]; + Span buf2 = stackalloc byte[16]; + int ret = sceNpDrmVerifyAct(actBuf); + if (ret > -1) + { + ret = sceNpDrmVerifyRif(rifBuf); + if (ret > -1) + { + var act = MemoryMarshal.AsRef(actBuf); + var rif = MemoryMarshal.AsRef(rifBuf); + if (!act.AccountId.SequenceEqual(rif.AccountId)) + { + return -0x7faaf6fb; + } + if (((act.ActType & 0xff) << 8 | act.ActType >> 8) < ((rif.Version & 0xff) << 8 | rif.Version >> 9)) + { + return -0x7faaf6ef; + } + AesHelper.AesDecrypt(rif.EncKey1, buf1, KeyVault.drmRifKey); + int keyIdx = MemoryMarshal.Read(buf1[12..]); + keyIdx >>= 0x18; + if (keyIdx >= 0x80) + { + return -0x7faaf6fe; + } + ret = GetActKey(buf2, act.PrimKeyTable[(keyIdx * 0x10)..], 1); + if (ret > -1) + { + ret = AesHelper.AesDecrypt(rif.EncKey2, versionKey, buf2); + if (ret != 16) + { + ret = -0x7faaf6fe; + } + } + } + } + buf1.Clear(); + buf2.Clear(); + return ret; + } + + private static int CheckRifExpress(ReadOnlySpan rifBuf) + { + // TODO + return 0; + } + + private static int GenVersionKey(Span versionKey, int type) + { + type &= 0xffffff; + int ret = 0; + if (type != 0) + { + ret = unchecked((int)0x80550901); + if (type < 4) + { + ret = AesHelper.AesEncrypt(versionKey, versionKey, KeyVault.drmVersionKeyKey.AsSpan().Slice(type * 0x10, 0x10)); + ret = ret == 16 ? 0 : unchecked((int)0x80550902); + } + } + return ret; + } + + public static int sceNpDrmGetVersionKey(Span versionKey, ReadOnlySpan actBuf, ReadOnlySpan rifBuf, int type) + { + int ret = -0x7faaf6ff; + if (versionKey != null && rifBuf != null) + { + var rif = MemoryMarshal.AsRef(rifBuf); + if (rif.DrmType == 0x300000) + { + ret = GetFreeVersionKey(versionKey, rifBuf); + } + else + { + if (actBuf == null) + { + return ret; + } + ret = GetLicenseVersionKey(versionKey, actBuf, rifBuf); + } + if (ret > -1) + { + ret = CheckRifExpress(rifBuf); + if (ret > -1) + { + ret = GenVersionKey(versionKey, type); + } + } + } + return ret; + } + + public static int sceNpDrmGetFixedKey(Span fixedKey, ReadOnlySpan contentId, int type) + { + Span buf = stackalloc byte[0x30]; + Span mkey = stackalloc byte[48]; + int ret = -0x7faaf6ff; + if ((type & 0x1000000) != 0) + { + contentId[..0x30].CopyTo(buf); + ret = AMCTRL.sceDrmBBMacInit(mkey, 1); + if (ret == 0) + { + ret = AMCTRL.sceDrmBBMacUpdate(mkey, buf, 0x30); + if (ret == 0) + { + ret = AMCTRL.sceDrmBBMacFinal(mkey, fixedKey, KeyVault.DrmFixedKey); + if (ret == 0) + { + ret = GenVersionKey(fixedKey, (int)(type & 0xfeffffff)); + } + else + { + ret = -0x7faaf6fe; + } + } + } + } + return ret; + } + + public static int sceNpDrmGetContentKey(Span contentKey, ReadOnlySpan actBuf, ReadOnlySpan rifBuf) => sceNpDrmGetVersionKey(contentKey, actBuf, rifBuf, 0); + + public static int sceNpDrmVerifyAct(ReadOnlySpan actBuf) + { + int ret = -0x7faaf6ff; + if (actBuf != null) + { + ret = -0x7faaf6fa; + var act = MemoryMarshal.AsRef(actBuf); + if (((act.VersionFlag & 0xFF) << 8 | act.VersionFlag >> 8) < 2) + { + if (((act.ActType & 0xFF) << 8 | act.ActType >> 8) < 2) + { + ret = VerifySig(act.EcdsaSig, KeyVault.drmActRifSig, actBuf, 0x1010); + } + } + } + return ret; + } + + private static int VerifySig(ReadOnlySpan sig, ReadOnlySpan pubKey, ReadOnlySpan data, int dataSize) + { + int ret; + Span digest = stackalloc byte[32]; + if (dataSize < 0x801) + { + ret = SceDdrdb.sceDdrdbHash(data, dataSize, digest); + } + else + { + ret = (SHA1.HashData(data[..dataSize], digest) > 0 ? 0 : -1); + } + if (ret > -1) + { + ret = SceDdrdb.sceDdrdbSigvry(pubKey, digest, sig); + } + return ret; + } + + private static int FillFuseId(Span fuseId) + { + if (_fuseId != 0) + { + uint heigh = (uint)((_fuseId >> 32) & 0xffffffff); + uint low = (uint)(_fuseId & 0xffffffff); + fuseId[0] = BinaryPrimitives.ReverseEndianness(heigh); + fuseId[1] = BinaryPrimitives.ReverseEndianness(low); + } + return -1; + } + + private static int VerifyRif(ReadOnlySpan rifBuf) + { + int ret = -0x7faaf6ff; + if (rifBuf != null) + { + ret = -0x7faaf6fa; + var rif = MemoryMarshal.AsRef(rifBuf); + if (((rif.VersionFlag & 0xff) << 8 | rif.VersionFlag >> 8) < 3) + { + if (((rif.Version & 0xff) << 8 | rif.Version >> 8) < 2) + { + int ret2; + if ((rif.DrmType & 0x10000) == 0) + { + ret = VerifySig(rif.EcdsaSig, KeyVault.drmActRifSig, rifBuf, 0x70); + ret2 = -0x7faaf6fc; + } + else + { + Span buffer = stackalloc byte[0x90]; + rifBuf[..0x70].CopyTo(buffer); + if (_psId == null) + { + return ret; + } + ret = sceNpDrmGetIDps(buffer[0x70..]); + if (ret < 0) + { + return ret; + } + Span fuseId = MemoryMarshal.Cast(buffer[0x80..]); + ret = FillFuseId(fuseId); + if (ret < 0) + { + return ret; + } + ret = VerifySig(rif.EcdsaSig, KeyVault.drmActRifSig, buffer, 0x90); + ret2 = -0x7faaf6e8; + } + if (ret != 0) + { + ret = ret2; + } + } + } + } + return ret; + } + + public static int sceNpDrmVerifyRif(ReadOnlySpan rifBuf) + { + int ret = VerifyRif(rifBuf); + if (ret > -1) + { + var rif = MemoryMarshal.AsRef(rifBuf); + if (rif.DrmType == 0x3000000) + { + ret = -1; // TODO + } + } + return ret; + } + + public static int KsceNpDrmEbootSigGenMultiDisc(string fileName, ReadOnlySpan sceDiscInfo, + Span ebootSig, int swVer) + { + return SceNpDrmEbootSigGenMultiDisc(fileName, sceDiscInfo, ebootSig, swVer, _memory.Span, BLK_SIZE); + } + + public static int KsceNpDrmEbootSigGenPs1(string fileName, Span ebootSig, int swVer) + { + return SceNpDrmEbootSigGen(fileName, 3, ebootSig, swVer, _memory.Span, BLK_SIZE); + } + + public static int KsceNpDrmEbootSigGenPsp(string fileName, Span ebootSig, int swVer) + { + return SceNpDrmEbootSigGen(fileName, 2, ebootSig, swVer, _memory.Span, BLK_SIZE); + } + + public static int KsceNpDrmPspEbootSigGen(string fileName, Span ebootSig) + { + return SceNpDrmPspEbootSigGen(fileName, ebootSig, _memory.Span, BLK_SIZE); + } + + private static int SceNpDrmEbootSigGenMultiDisc(string fileName, ReadOnlySpan sceDiskInfo, + Span ebootSig, int swVer, Span buffer, int blockSize) + { + Span pbpHdrDigest = stackalloc byte[32]; + Span discsDigest = stackalloc byte[32]; + Span ebootSigtmp = stackalloc byte[512]; + ref var sceEbootPbp = ref Utils.AsRef(ebootSigtmp); + if ((blockSize & 0x3F) != 0) + { + blockSize &= unchecked((int)0xFFFFFFC0); + } + + if (sceDiskInfo == null || string.IsNullOrWhiteSpace(fileName) || buffer == null || ebootSig == null || + blockSize < 0x400) + { + return unchecked((int)0x80870001); + } + + Span secureTick = stackalloc byte[8] { 0xD4, 0x7A, 0x2C, 0x13, 0x64, 0x59, 0xE2, 0x00 }; + //RandomNumberGenerator.Fill(secureTick); + + ebootSig.Fill(0); + sceEbootPbp.SwVer = swVer; + sceEbootPbp.Aid = Aid; + sceEbootPbp.SecureTick = Utils.AsRef(secureTick); + + var sha224 = SHA224.Create(); + var hash = sha224.ComputeHash(sceDiskInfo[..200].ToArray()); + var ret = SceSblGcAuthMgrDrmBBForDriver_4B506BE7(hash, sceDiskInfo[200..], 1000); + if (ret != 0) + { + return ret; + } + + var discInfo = Utils.AsRef(sceDiskInfo); + ret = unchecked((int)0x80870005); + if (discInfo.DiscCount > 6) + { + return ret; + } + var fi = new FileInfo(fileName); + if (!fi.Exists) + { + return -1; + } + + if (fi.Length != discInfo.FileSize) + { + return ret; + } + + ret = unchecked((int)0x80870001); + if (discInfo.DiscCount < 7) + { + if (fi.Length < discInfo.Offsets[0]) + { + return ret; + } + if (discInfo.DiscCount > 1 && fi.Length < discInfo.Offsets[1]) + { + return ret; + } + if (discInfo.DiscCount > 2 && fi.Length < discInfo.Offsets[2]) + { + return ret; + } + if (discInfo.DiscCount > 3 && fi.Length < discInfo.Offsets[3]) + { + return ret; + } + if (discInfo.DiscCount > 4 && fi.Length < discInfo.Offsets[4]) + { + return ret; + } + if (discInfo.DiscCount > 5 && fi.Length < discInfo.Offsets[5]) + { + return ret; + } + + using var stream = fi.OpenRead(); + ret = SceMultiDiscDigest(stream, fi.Length, discInfo.DiscCount, discInfo.Offsets, pbpHdrDigest, discsDigest, buffer, blockSize); + if (discInfo.DiscCount >= 0) + { + stream.Close(); + ret = SceSblGcAuthMgrDrmBBForDriver_4B506BE7(discsDigest, discInfo.DiscsSig, 1000); + if (ret != 0) + { + return ret; + } + + sceEbootPbp.PbpSize = fi.Length; + sceEbootPbp.Magic = 0x47495349544C554D; + sceEbootPbp.KeyType = 1; + sceEbootPbp.Type = 4; + discInfo.Id.CopyTo(sceEbootPbp.ContentId); + sceEbootPbp.DiscCount = discInfo.DiscCount; + discInfo.Offsets.CopyTo(sceEbootPbp.DiscOffsets); + ret = SceSblGcAuthMgrDrmBBForDriver_050DC6DF(pbpHdrDigest, sceEbootPbp.PbpHdrSig, 1); + if (ret != 0) + { + return ret; + } + discInfo.DiscsSig.CopyTo(sceEbootPbp.NpUmdImgSig); + var ebootsigDigst = sha224.ComputeHash(ebootSigtmp.Slice(0, 0x1C8).ToArray()); + ret = SceSblGcAuthMgrDrmBBForDriver_050DC6DF(ebootsigDigst, sceEbootPbp.Sig, 1); + if (ret != 0) + { + return ret; + } + ebootSigtmp.CopyTo(ebootSig); + ret = 0; + } + } + return ret; + } + + private static int SceMultiDiscDigest(Stream stream, long fileSize, int diskCount, + ReadOnlySpan diskOffsets, Span pbpHdrDigest, Span dataDigest, Span buffer, + int blockSize) + { + var sha224 = SHA224.Create(); + var ret = unchecked((int)0x80870005); + stream.Seek(0, SeekOrigin.Begin); + buffer = buffer.Slice(0, blockSize); + var readSize = stream.Read(buffer); + if (readSize < 0x28) + { + return ret; + } + var pbpHeader = Utils.AsRef(buffer); + if (pbpHeader.Sig != 0x50425000) + { + return ret; + } + + var tmp = readSize; + if (pbpHeader.DataPsarOff < readSize) + { + tmp = pbpHeader.DataPsarOff; + } + + var paramReadSize = pbpHeader.Icon0Off; + if (paramReadSize > 0x400) + { + paramReadSize = 0x400; + } + + if (tmp < paramReadSize) + { + return ret; + } + + sha224.TransformFinalBlock(buffer.ToArray(), 0, paramReadSize); + sha224.Hash.CopyTo(pbpHdrDigest); + + sha224.Initialize(); + sha224.TransformBlock(buffer.ToArray(), 0, paramReadSize, null, 0); + + long end = pbpHeader.DataPsarOff + 0xC0000; + if (diskOffsets[0] >= pbpHeader.DataPsarOff && diskOffsets[0] < end) + { + end = diskOffsets[0]; + } + + long start = pbpHeader.DataPsarOff; + stream.Seek(start, SeekOrigin.Begin); + for (; start < end; start += readSize) + { + var toRead = (int)(end - start); + if (toRead > blockSize) + { + toRead = blockSize; + } + + readSize = stream.Read(buffer.Slice(0, toRead)); + if (readSize == 0) + { + return -1; + } + sha224.TransformBlock(buffer.ToArray(), 0, readSize, null, 0); + } + + if (diskCount != 0) + { + var start1 = diskOffsets[0]; + end = diskOffsets[0] + 0xC0000; + if (end >= 0) + { + var discNo = 0; + var idx = 5; + while (true) + { + if (fileSize < end) + { + end = fileSize; + } + + if (++discNo < diskCount) + { + start = diskOffsets[idx - 4]; + if (start >= start1 && start < end) + { + end = start; + } + } + + if (start1 < end) + { + stream.Seek(start1, SeekOrigin.Begin); + while (true) + { + var toRead = (int)(end - start1); + if (toRead > blockSize) + { + toRead = blockSize; + } + + readSize = stream.Read(buffer.Slice(0, toRead)); + if (readSize == 0) + { + return -1; + } + sha224.TransformBlock(buffer.ToArray(), 0, readSize, null, 0); + start1 += toRead; + if (start1 >= end) + { + break; + } + } + } + + if (discNo == diskCount) + { + break; + } + start1 = diskOffsets[idx - 4]; + idx++; + end = start1 + 0xC0000; + + } + } + } + sha224.TransformFinalBlock(buffer.ToArray(), 0, 0); + sha224.Hash.CopyTo(dataDigest); + + stream.Seek(pbpHeader.DataPsarOff, SeekOrigin.Begin); + readSize = stream.Read(buffer.Slice(0, 0x100)); + if (readSize == 0x100) + { + ret = 0; + } + else + { + ret = unchecked((int)0x80870005); + } + + return ret; + } + + private static int SceNpDrmEbootSigGen(string fileName, int type, Span ebootSig, int swVer, Span buffer, + int blockSize) + { + Span pbpHdrDigest = stackalloc byte[32]; + Span npUmdImgDigest = stackalloc byte[32]; + Span ebootSigtmp = stackalloc byte[512]; + ref var sceEbootPbp = ref Utils.AsRef(ebootSigtmp); + if ((blockSize & 0x3F) != 0) + { + blockSize &= unchecked((int)0xFFFFFFC0); + } + + if (string.IsNullOrWhiteSpace(fileName) || buffer == null || ebootSig == null || blockSize < 0x400 || + type > 3) + { + return -0x7f78ffff; + } + + Span secureTick = stackalloc byte[8] { 0xD4, 0x7A, 0x2C, 0x13, 0x64, 0x59, 0xE2, 0x00 }; + //RandomNumberGenerator.Fill(secureTick); + + ebootSig.Fill(0); + sceEbootPbp.SwVer = swVer; + sceEbootPbp.Aid = Aid; + sceEbootPbp.SecureTick = Utils.AsRef(secureTick); + var fi = new FileInfo(fileName); + if (!fi.Exists) + { + return -1; + } + + using var stream = fi.OpenRead(); + var ret = SceEbootPbpDigest(stream, fi.Length, pbpHdrDigest, npUmdImgDigest, buffer, blockSize); + if (ret < 0) + { + return ret; + } + stream.Close(); + sceEbootPbp.KeyType = 1; + sceEbootPbp.PbpSize = fi.Length; + sceEbootPbp.Type = type; + var psarSig = Encoding.ASCII.GetString(buffer.Slice(0, 8)); + if (type == 3) + { + if (psarSig != "PSISOIMG") + { + return -0x7f78fffb; + } + sceEbootPbp.Magic = 0x474953315350504E; + } + else + { + if (psarSig != "NPUMDIMG") + { + return -0x7f78fffb; + } + + sceEbootPbp.Magic = 0x474953444D55504E; + buffer.Slice(0x10, 0x30).CopyTo(sceEbootPbp.ContentId); + } + + ret = SceSblGcAuthMgrDrmBBForDriver_050DC6DF(pbpHdrDigest, sceEbootPbp.PbpHdrSig, 1); + if (ret < 0) + { + return ret; + } + + ret = SceSblGcAuthMgrDrmBBForDriver_050DC6DF(npUmdImgDigest, sceEbootPbp.NpUmdImgSig, 1); + if (ret < 0) + { + return ret; + } + + var sha224 = SHA224.Create(); + var ebootsigDigst = sha224.ComputeHash(ebootSigtmp.Slice(0, 0x1C8).ToArray()); + ret = SceSblGcAuthMgrDrmBBForDriver_050DC6DF(ebootsigDigst, sceEbootPbp.Sig, 1); + if (ret < 0) + { + return ret; + } + ebootSigtmp.CopyTo(ebootSig); + ret = 0; + return ret; + } + + private static int SceEbootPbpDigest(Stream stream, long fileSize, Span pbpHdrDigest, + Span npUmdImgDigest, Span buffer, int blockSize) + { + var sha224 = SHA224.Create(); + var ret = unchecked((int)0x80870005); + stream.Seek(0, SeekOrigin.Begin); + buffer = buffer.Slice(0, blockSize); + var readSize = stream.Read(buffer); + if (readSize < 0x28) + { + return ret; + } + + var pbpHeader = Utils.AsRef(buffer); + if (fileSize < pbpHeader.DataPsarOff + 0xFF || pbpHeader.Sig != 0x50425000) + { + return ret; + } + + var tmp = readSize; + if (pbpHeader.DataPsarOff < readSize) + { + tmp = pbpHeader.DataPsarOff; + } + + var paramReadSize = pbpHeader.Icon0Off; + if (paramReadSize > 0x400) + { + paramReadSize = 0x400; + } + + if (tmp < paramReadSize) + { + return ret; + } + + sha224.TransformFinalBlock(buffer.ToArray(), 0, paramReadSize); + sha224.Hash.CopyTo(pbpHdrDigest); + + sha224.Initialize(); + + var alignedFileSize = (pbpHeader.DataPsarOff + 0x1C0000 + 64 - 1) & ~(64 - 1); + if (alignedFileSize < fileSize) + { + fileSize = alignedFileSize; + } + + var fixsize2 = fileSize; + if (pbpHeader.DataPsarOff + 0x1C0000 < fileSize) + { + fixsize2 = pbpHeader.DataPsarOff + 0x1C0000; + } + + if (pbpHeader.DataPsarOff < fixsize2) + { + var offset = pbpHeader.DataPsarOff; + stream.Seek(offset, SeekOrigin.Begin); + while (true) + { + readSize = stream.Read(buffer); + var bsize = readSize; + if (readSize <= 0) + { + ret = unchecked((int)0x80870002); + return ret; + } + + if (fixsize2 < offset + readSize) + { + bsize = (int)(fixsize2 - offset); + } + sha224.TransformBlock(buffer.ToArray(), 0, bsize, null, 0); + + offset += readSize; + if (offset >= fixsize2) + { + break; + } + } + sha224.TransformFinalBlock(buffer.ToArray(), 0, 0); + sha224.Hash.CopyTo(npUmdImgDigest); + stream.Seek(pbpHeader.DataPsarOff, SeekOrigin.Begin); + readSize = stream.Read(buffer.Slice(0, 0x100)); + if (readSize == 0x100) + { + ret = 0; + } + else + { + ret = unchecked((int)0x80870005); + } + } + + + return ret; + } + + private static int SceNpDrmPspEbootSigGen(string fileName, Span ebootSig, Span buffer, + int blockSize) + { + Span pbpHdrDigest = stackalloc byte[32]; + Span npUmdImgDigest = stackalloc byte[32]; + Span ebootSigtmp = stackalloc byte[0x100]; + ref var sceEbootPbp = ref Utils.AsRef(ebootSigtmp); + if ((blockSize & 0x3F) != 0) + { + blockSize &= unchecked((int)0xFFFFFFC0); + } + + if (string.IsNullOrWhiteSpace(fileName) || buffer == null || ebootSig == null || blockSize < 0x400) + { + return -0x7f78ffff; + } + var fi = new FileInfo(fileName); + if (!fi.Exists) + { + return -1; + } + + using var stream = fi.OpenRead(); + var ret = SceEbootPbpDigest(stream, fi.Length, pbpHdrDigest, npUmdImgDigest, buffer, blockSize); + if (ret < 0) + { + return ret; + } + stream.Close(); + + if (Encoding.ASCII.GetString(buffer.Slice(0, 8)) != "NPUMDIMG") + { + return -0x7f78fffb; + } + buffer.Slice(0, 0x40).CopyTo(ebootSigtmp); + sceEbootPbp.Type = 0; + sceEbootPbp.Magic = 0x474953444D55504E; + + ret = SceSblGcAuthMgrDrmBBForDriver_050DC6DF(pbpHdrDigest, sceEbootPbp.PbpHdrSig, 1); + if (ret < 0) + { + return ret; + } + + ret = SceSblGcAuthMgrDrmBBForDriver_050DC6DF(npUmdImgDigest, sceEbootPbp.NpUmdImgSig, 1); + if (ret < 0) + { + return ret; + } + + var sha224 = SHA224.Create(); + var ebootsigDigst = sha224.ComputeHash(ebootSigtmp.Slice(0, 0xC8).ToArray()); + ret = SceSblGcAuthMgrDrmBBForDriver_050DC6DF(ebootsigDigst, sceEbootPbp.Sig, 1); + if (ret < 0) + { + return ret; + } + ebootSigtmp.CopyTo(ebootSig); + ret = 0; + + return ret; + } + + private static int SceSblGcAuthMgrDrmBBForDriver_050DC6DF(ReadOnlySpan digest, Span sig, int type) + { + var curve = ECDsaHelper.SetCurve(KeyVault.Eboot_p, KeyVault.Eboot_a, KeyVault.Eboot_b, KeyVault.Eboot_N, KeyVault.Eboot_Gx, + KeyVault.Eboot_Gy); + using var ecdsa = ECDsaHelper.Create(curve, + KeyVault.Eboot_priv[type], + KeyVault.Eboot_pubx[type], + KeyVault.Eboot_puby[type], true); + var signature = ecdsa.SignHash(digest.ToArray()); + signature.CopyTo(sig); + return 0; + } + + private static int SceSblGcAuthMgrDrmBBForDriver_4B506BE7(ReadOnlySpan digest, ReadOnlySpan sig, int keyType) + { + byte[] pubx; + byte[] puby; + switch (keyType) + { + case 1: + pubx = KeyVault.VitaKirk18PubKey1x; + puby = KeyVault.VitaKirk18PubKey1y; + break; + case 0: + pubx = KeyVault.VitaKirk18PubKey0x; + puby = KeyVault.VitaKirk18PubKey0y; + break; + case 1000: + pubx = KeyVault.VitaKirk18PubKey1000x; + puby = KeyVault.VitaKirk18PubKey1000y; + break; + default: + return unchecked((int)0x808a040a); + } + var curve = ECDsaHelper.SetCurve(KeyVault.Eboot_p, KeyVault.Eboot_a, KeyVault.Eboot_b, KeyVault.Eboot_N, KeyVault.Eboot_Gx, + KeyVault.Eboot_Gy); + using var ecdsa = ECDsaHelper.Create(curve, pubx, puby); + var verify = ecdsa.VerifyHash(digest.ToArray(), sig.ToArray()); + + return verify ? 0 : -1; + } + } +} diff --git a/PspCrypto/Security/Cryptography/Asn1Reader/AsnValueReader.cs b/PspCrypto/Security/Cryptography/Asn1Reader/AsnValueReader.cs new file mode 100644 index 0000000..85a6c9b --- /dev/null +++ b/PspCrypto/Security/Cryptography/Asn1Reader/AsnValueReader.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace System.Formats.Asn1 +{ + internal ref struct AsnValueReader + { + private static readonly byte[] s_singleByte = new byte[1]; + + private ReadOnlySpan _span; + private readonly AsnEncodingRules _ruleSet; + + internal AsnValueReader(ReadOnlySpan span, AsnEncodingRules ruleSet) + { + _span = span; + _ruleSet = ruleSet; + } + + internal bool HasData => !_span.IsEmpty; + + internal void ThrowIfNotEmpty() + { + if (!_span.IsEmpty) + { + new AsnReader(s_singleByte, _ruleSet).ThrowIfNotEmpty(); + } + } + + internal Asn1Tag PeekTag() + { + return Asn1Tag.Decode(_span, out _); + } + + internal ReadOnlySpan PeekContentBytes() + { + AsnDecoder.ReadEncodedValue( + _span, + _ruleSet, + out int contentOffset, + out int contentLength, + out _); + + return _span.Slice(contentOffset, contentLength); + } + + internal ReadOnlySpan PeekEncodedValue() + { + AsnDecoder.ReadEncodedValue(_span, _ruleSet, out _, out _, out int consumed); + return _span.Slice(0, consumed); + } + + internal ReadOnlySpan ReadEncodedValue() + { + ReadOnlySpan value = PeekEncodedValue(); + _span = _span.Slice(value.Length); + return value; + } + + internal bool ReadBoolean(Asn1Tag? expectedTag = default) + { + bool ret = AsnDecoder.ReadBoolean(_span, _ruleSet, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } + + internal BigInteger ReadInteger(Asn1Tag? expectedTag = default) + { + BigInteger ret = AsnDecoder.ReadInteger(_span, _ruleSet, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } + + internal bool TryReadInt32(out int value, Asn1Tag? expectedTag = default) + { + bool ret = AsnDecoder.TryReadInt32(_span, _ruleSet, out value, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } + + internal ReadOnlySpan ReadIntegerBytes(Asn1Tag? expectedTag = default) + { + ReadOnlySpan ret = AsnDecoder.ReadIntegerBytes(_span, _ruleSet, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } + + internal bool TryReadPrimitiveBitString( + out int unusedBitCount, + out ReadOnlySpan value, + Asn1Tag? expectedTag = default) + { + bool ret = AsnDecoder.TryReadPrimitiveBitString( + _span, + _ruleSet, + out unusedBitCount, + out value, + out int consumed, + expectedTag); + + _span = _span.Slice(consumed); + return ret; + } + + internal byte[] ReadBitString(out int unusedBitCount, Asn1Tag? expectedTag = default) + { + byte[] ret = AsnDecoder.ReadBitString( + _span, + _ruleSet, + out unusedBitCount, + out int consumed, + expectedTag); + + _span = _span.Slice(consumed); + return ret; + } + + internal TFlagsEnum ReadNamedBitListValue(Asn1Tag? expectedTag = default) where TFlagsEnum : Enum + { + TFlagsEnum ret = AsnDecoder.ReadNamedBitListValue(_span, _ruleSet, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } + + internal bool TryReadPrimitiveOctetString( + out ReadOnlySpan value, + Asn1Tag? expectedTag = default) + { + bool ret = AsnDecoder.TryReadPrimitiveOctetString( + _span, + _ruleSet, + out value, + out int consumed, + expectedTag); + + _span = _span.Slice(consumed); + return ret; + } + + internal byte[] ReadOctetString(Asn1Tag? expectedTag = default) + { + byte[] ret = AsnDecoder.ReadOctetString( + _span, + _ruleSet, + out int consumed, + expectedTag); + + _span = _span.Slice(consumed); + return ret; + } + + internal string ReadObjectIdentifier(Asn1Tag? expectedTag = default) + { + string ret = AsnDecoder.ReadObjectIdentifier(_span, _ruleSet, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } + + internal AsnValueReader ReadSequence(Asn1Tag? expectedTag = default) + { + AsnDecoder.ReadSequence( + _span, + _ruleSet, + out int contentOffset, + out int contentLength, + out int bytesConsumed, + expectedTag); + + ReadOnlySpan content = _span.Slice(contentOffset, contentLength); + _span = _span.Slice(bytesConsumed); + return new AsnValueReader(content, _ruleSet); + } + + internal AsnValueReader ReadSetOf(Asn1Tag? expectedTag = default, bool skipSortOrderValidation = false) + { + AsnDecoder.ReadSetOf( + _span, + _ruleSet, + out int contentOffset, + out int contentLength, + out int bytesConsumed, + skipSortOrderValidation: skipSortOrderValidation, + expectedTag: expectedTag); + + ReadOnlySpan content = _span.Slice(contentOffset, contentLength); + _span = _span.Slice(bytesConsumed); + return new AsnValueReader(content, _ruleSet); + } + + internal DateTimeOffset ReadUtcTime(Asn1Tag? expectedTag = default) + { + DateTimeOffset ret = AsnDecoder.ReadUtcTime(_span, _ruleSet, out int consumed, expectedTag: expectedTag); + _span = _span.Slice(consumed); + return ret; + } + + internal DateTimeOffset ReadGeneralizedTime(Asn1Tag? expectedTag = default) + { + DateTimeOffset ret = AsnDecoder.ReadGeneralizedTime(_span, _ruleSet, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } + + internal string ReadCharacterString(UniversalTagNumber encodingType, Asn1Tag? expectedTag = default) + { + string ret = AsnDecoder.ReadCharacterString(_span, _ruleSet, encodingType, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } + + internal TEnum ReadEnumeratedValue(Asn1Tag? expectedTag = null) where TEnum : Enum + { + TEnum ret = AsnDecoder.ReadEnumeratedValue(_span, _ruleSet, out int consumed, expectedTag); + _span = _span.Slice(consumed); + return ret; + } + } +} diff --git a/PspCrypto/Security/Cryptography/AsymmetricAlgorithmHelpers.cs b/PspCrypto/Security/Cryptography/AsymmetricAlgorithmHelpers.cs new file mode 100644 index 0000000..71a782f --- /dev/null +++ b/PspCrypto/Security/Cryptography/AsymmetricAlgorithmHelpers.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Formats.Asn1; + +namespace PspCrypto.Security.Cryptography +{ + // + // Common infrastructure for AsymmetricAlgorithm-derived classes that layer on OpenSSL. + // + internal static partial class AsymmetricAlgorithmHelpers + { + + // private static readonly Func, ReadOnlyMemory[]> ReaderAsn; + + // static AsymmetricAlgorithmHelpers() + // { + // var assembly = typeof(Aes).Assembly; + // var r = assembly.GetType("System.Security.Cryptography.Asn1.AsnReader"); + // var par1Type = typeof(ReadOnlyMemory); + // var par2Type = assembly.GetType("System.Security.Cryptography.Asn1.AsnEncodingRules"); + // var asn1TagType = assembly.GetType("System.Security.Cryptography.Asn1.Asn1Tag"); + // var constructor = r.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, CallingConventions.HasThis, + // new[] { par1Type, par2Type }, new ParameterModifier[0]); + // if (constructor == null) + // { + // throw new NotImplementedException(); + // } + // var readSequence = r.GetMethod("ReadSequence"); + // if (readSequence == null) + // { + // throw new NotImplementedException(); + // } + // var throwIfNotEmpty = r.GetMethod("ThrowIfNotEmpty"); + // if (throwIfNotEmpty == null) + // { + // throw new NotImplementedException(); + // } + // var readIntegerBytes = r.GetMethod("ReadIntegerBytes", BindingFlags.Instance | BindingFlags.Public, null, CallingConventions.HasThis, new Type[0], new ParameterModifier[0]); + // if (readIntegerBytes == null) + // { + // throw new NotImplementedException(); + // } + // var par1 = Expression.Parameter(par1Type, "data"); + // var derField = par2Type.GetField("DER"); + // var readerVar = Expression.Variable(r, "reader"); + // var sequenceReaderVar = Expression.Variable(r, "sequenceReader"); + // var rDerVar = Expression.Variable(typeof(ReadOnlyMemory), "rDer"); + // var sDerVar = Expression.Variable(typeof(ReadOnlyMemory), "sDer"); + // var sequenceField = asn1TagType.GetField("Sequence"); + // var expBlock = Expression.Block(new[] { readerVar, sequenceReaderVar, rDerVar, sDerVar }, + // Expression.Assign(readerVar, Expression.New(constructor, par1, Expression.Field(null, derField))), + // Expression.Assign(sequenceReaderVar, Expression.Call(readerVar, readSequence, Expression.Field(null, sequenceField))), + // Expression.Call(readerVar, throwIfNotEmpty), + // Expression.Assign(rDerVar, Expression.Call(sequenceReaderVar, readIntegerBytes)), + // Expression.Assign(sDerVar, Expression.Call(sequenceReaderVar, readIntegerBytes)), + // Expression.Call(sequenceReaderVar, throwIfNotEmpty), + // Expression.NewArrayInit(typeof(ReadOnlyMemory), rDerVar, sDerVar)); + // ReaderAsn = Expression.Lambda, ReadOnlyMemory[]>>(expBlock, par1).Compile(); + + // } + + /// + /// Convert Der format of (r, s) to Ieee1363 format + /// + public static byte[] ConvertDerToIeee1363(ReadOnlySpan input, int fieldSizeBits) + { + int fieldSizeBytes = BitsToBytes(fieldSizeBits); + int encodedSize = 2 * fieldSizeBytes; + byte[] response = new byte[encodedSize]; + + ConvertDerToIeee1363(input, fieldSizeBits, response); + return response; + } + + internal static int ConvertDerToIeee1363(ReadOnlySpan input, int fieldSizeBits, Span destination) + { + int fieldSizeBytes = BitsToBytes(fieldSizeBits); + int encodedSize = 2 * fieldSizeBytes; + + Debug.Assert(destination.Length >= encodedSize); + + try + { + AsnValueReader reader = new AsnValueReader(input, AsnEncodingRules.DER); + AsnValueReader sequenceReader = reader.ReadSequence(); + reader.ThrowIfNotEmpty(); + ReadOnlySpan rDer = sequenceReader.ReadIntegerBytes(); + ReadOnlySpan sDer = sequenceReader.ReadIntegerBytes(); + sequenceReader.ThrowIfNotEmpty(); + + CopySignatureField(rDer, destination.Slice(0, fieldSizeBytes)); + CopySignatureField(sDer, destination.Slice(fieldSizeBytes, fieldSizeBytes)); + return encodedSize; + } + catch (AsnContentException e) + { + throw new CryptographicException("ASN1 corrupted data.", e); + } + } + + public static int BitsToBytes(int bitLength) + { + int byteLength = (bitLength + 7) / 8; + return byteLength; + } + + private static void CopySignatureField(ReadOnlySpan signatureField, Span response) + { + if (signatureField.Length > response.Length) + { + if (signatureField.Length != response.Length + 1 || + signatureField[0] != 0 || + signatureField[1] <= 0x7F) + { + // The only way this should be true is if the value required a zero-byte-pad. + Debug.Fail($"A signature field was longer ({signatureField.Length}) than expected ({response.Length})"); + throw new CryptographicException(); + } + + signatureField = signatureField.Slice(1); + } + + // If the field is too short then it needs to be prepended + // with zeroes in the response. Since the array was already + // zeroed out, just figure out where we need to start copying. + int writeOffset = response.Length - signatureField.Length; + response.Slice(0, writeOffset).Clear(); + signatureField.CopyTo(response.Slice(writeOffset)); + } + } +} diff --git a/PspCrypto/Security/Cryptography/ECDsaManaged.cs b/PspCrypto/Security/Cryptography/ECDsaManaged.cs new file mode 100644 index 0000000..ae01643 --- /dev/null +++ b/PspCrypto/Security/Cryptography/ECDsaManaged.cs @@ -0,0 +1,171 @@ +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Generators; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Math.EC; +using Org.BouncyCastle.Security; +using System; + +namespace PspCrypto.Security.Cryptography +{ + internal class ECDsaManaged : System.Security.Cryptography.ECDsa + { + private ECKeyParameters _ecKeyParameters; + private readonly bool _ebootPbp; + private readonly int _type; + + public ECDsaManaged() + { + + } + + public ECDsaManaged(System.Security.Cryptography.ECParameters parameters, bool ebootPbp, int type) + { + _ebootPbp = ebootPbp; + _type = type; + var gx = new BigInteger(1, parameters.Curve.G.X); + var gy = new BigInteger(1, parameters.Curve.G.Y); + var curve = ConvertECCurve(parameters.Curve); + var g = curve.CreatePoint(gx, gy); + var domainParameters = new ECDomainParameters(curve, g, curve.Order); + if (parameters.D != null) + { + var privateKey = new BigInteger(1, parameters.D); + _ecKeyParameters = new ECPrivateKeyParameters(privateKey, domainParameters); + } + else if (parameters.Q.X != null && parameters.Q.Y != null) + { + var publicKey = curve.CreatePoint(new BigInteger(1, parameters.Q.X), new BigInteger(1, parameters.Q.Y)); + _ecKeyParameters = new ECPublicKeyParameters(publicKey, domainParameters); + } + else + { + throw new ArgumentException("invalid parameters", nameof(parameters)); + } + } + + public override byte[] SignHash(byte[] hash) + { + if (_ecKeyParameters is not ECPrivateKeyParameters) + { + throw new ArgumentException("key is not private Key"); + } + var signer = CreateSigner(); + signer.Init(true, _ecKeyParameters); + signer.BlockUpdate(hash); + return signer.GenerateSignature(); + } + + public override bool VerifyHash(byte[] hash, byte[] signature) + { + var signer = CreateSigner(); + if (_ecKeyParameters is ECPrivateKeyParameters ecPrivateKeyParameters) + { + var publicKey = new ECPublicKeyParameters( + ecPrivateKeyParameters.Parameters.G.Multiply(ecPrivateKeyParameters.D), + ecPrivateKeyParameters.Parameters); + signer.Init(false, publicKey); + } + else + { + signer.Init(false, _ecKeyParameters); + } + signer.BlockUpdate(hash); + return signer.VerifySignature(signature); + } + + protected override byte[] HashData(byte[] data, int offset, int count, System.Security.Cryptography.HashAlgorithmName hashAlgorithm) + { + var dataSpan = data.AsSpan().Slice(offset, count); + if (hashAlgorithm == System.Security.Cryptography.HashAlgorithmName.SHA256) + { + return System.Security.Cryptography.SHA256.HashData(dataSpan); + } + else if (hashAlgorithm == System.Security.Cryptography.HashAlgorithmName.SHA1) + { + return System.Security.Cryptography.SHA1.HashData(dataSpan); + } + else + { + throw new NotSupportedException($"{hashAlgorithm} not supported"); + } + } + + private ISigner CreateSigner() + { + IDigest digest = DigestUtilities.GetDigest("NONE"); + IDsa dsa = _ebootPbp ? new ECDsaSigner(new EbootPbpKCalculator(_type)) : new ECDsaSigner(); + var signer = new DsaDigestSigner(dsa, digest, PlainDsaEncoding.Instance); + return signer; + } + + private FpCurve _fpCurve; + + public override void GenerateKey(System.Security.Cryptography.ECCurve curve) + { + _fpCurve = ConvertECCurve(curve); + var gx = new BigInteger(1, curve.G.X); + var gy = new BigInteger(1, curve.G.Y); + var g = _fpCurve.CreatePoint(gx, gy); + var domainParameters = new ECDomainParameters(_fpCurve, g, _fpCurve.Order); + var gen = new ECKeyPairGenerator(); + gen.Init(new ECKeyGenerationParameters(domainParameters, new SecureRandom())); + var keyPair = gen.GenerateKeyPair(); + _ecKeyParameters = (ECKeyParameters)keyPair.Private; + } + + public override System.Security.Cryptography.ECParameters ExportExplicitParameters(bool includePrivateParameters) + { + var normalG = _ecKeyParameters.Parameters.G; + var curve = new System.Security.Cryptography.ECCurve + { + A = _fpCurve.A.ToBigInteger().ToByteArrayUnsigned(), + B = _fpCurve.B.ToBigInteger().ToByteArrayUnsigned(), + Prime = _fpCurve.Q.ToByteArrayUnsigned(), + Order = _fpCurve.Order.ToByteArrayUnsigned(), + Cofactor = _fpCurve.Cofactor.ToByteArrayUnsigned(), + G = new System.Security.Cryptography.ECPoint + { + X = normalG.XCoord.ToBigInteger().ToByteArrayUnsigned(), + Y = normalG.YCoord.ToBigInteger().ToByteArrayUnsigned() + } + }; + var parameters = new System.Security.Cryptography.ECParameters + { + Curve = curve + }; + if (includePrivateParameters && _ecKeyParameters is ECPrivateKeyParameters privateKeyParameters) + { + parameters.D = privateKeyParameters.D.ToByteArrayUnsigned(); + Console.WriteLine(privateKeyParameters.D.ToString(16).ToUpper()); + var publicKey = privateKeyParameters.Parameters.G.Multiply(privateKeyParameters.D).Normalize(); + parameters.Q = new System.Security.Cryptography.ECPoint + { + X = publicKey.XCoord.ToBigInteger().ToByteArrayUnsigned(), + Y = publicKey.YCoord.ToBigInteger().ToByteArrayUnsigned() + }; + } + else if (_ecKeyParameters is ECPublicKeyParameters publicKeyParameters) + { + var publicKey = publicKeyParameters.Q; + parameters.Q = new System.Security.Cryptography.ECPoint + { + X = publicKey.XCoord.ToBigInteger().ToByteArrayUnsigned(), + Y = publicKey.YCoord.ToBigInteger().ToByteArrayUnsigned() + }; + } + return parameters; + } + + private static FpCurve ConvertECCurve(System.Security.Cryptography.ECCurve curve) + { + var p = new BigInteger(1, curve.Prime); + var a = new BigInteger(1, curve.A); + var b = new BigInteger(1, curve.B); + var n = new BigInteger(1, curve.Order); + var fpCurve = new FpCurve(p, a, b, n, BigInteger.One); + return fpCurve; + } + } +} diff --git a/PspCrypto/Security/Cryptography/EbootPbpKCalculator.cs b/PspCrypto/Security/Cryptography/EbootPbpKCalculator.cs new file mode 100644 index 0000000..e7918b3 --- /dev/null +++ b/PspCrypto/Security/Cryptography/EbootPbpKCalculator.cs @@ -0,0 +1,103 @@ +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Macs; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; +using Org.BouncyCastle.Math; +using Org.BouncyCastle.Security; +using System; + +namespace PspCrypto.Security.Cryptography +{ + internal class EbootPbpKCalculator : IDsaKCalculator + { + private int _type; + + private BigInteger _n; + + public EbootPbpKCalculator(int type) + { + _type = type; + } + + public bool IsDeterministic => true; + + private readonly Memory _hash = new byte[0x40]; + + public void Init(BigInteger n, SecureRandom random) + { + throw new NotImplementedException(); + } + public void Init(BigInteger n, BigInteger d, byte[] message) + { + _n = n; + Span hmacIn = stackalloc byte[0x38]; + message[..0x1C].CopyTo(hmacIn); + KeyVault.Eboot_priv[_type].CopyTo(hmacIn[0x1C..]); + + var hmac = new HMac(new Sha256Digest()); + hmac.Init(new KeyParameter(KeyVault.Eboot_hmacKey)); + hmac.BlockUpdate(hmacIn); + var hmac_hash_iv = new byte[hmac.GetMacSize()]; + hmac.DoFinal(hmac_hash_iv); + + int ret; + do + { + ret = can_be_reversed_80C17A(message, 0x1c, hmac_hash_iv, _hash.Span); + if (ret != 0 || (ret = can_be_reversed_80C17A(message, 0x1c, hmac_hash_iv, _hash.Span[0x20..])) != 0) + { + throw new Exception(); + } + + } while (ret != 0); + } + + public BigInteger NextK() + { + var bn = new BigInteger(1, _hash.Span[..0x3c]); + var ret = bn.Mod(_n); + return ret; + } + + + private static int can_be_reversed_80C17A(Span src, int some_size, Span iv, + Span src_xored_digest) + { + Span src_xored = stackalloc byte[0x20]; + iv.CopyTo(src_xored); + + if (some_size > 0x20) + { + return 0x12; + } + + for (int i = 0; i < some_size; i++) + { + src_xored[i] ^= src[i]; + } + + using var sha256 = System.Security.Cryptography.SHA256.Create(); + var hash = sha256.ComputeHash(src_xored.ToArray()); + hash.CopyTo(src_xored_digest); + + for (int i = 0; i < 0x20; i++) + { + iv[i] ^= src_xored_digest[i]; + } + + for (int i = 0; i < 0x20; i++) + { + if (iv[i] != 0xFF) + { + iv[i] += 1; + break; + } + + iv[i] = 0; + } + + return 0; + } + + } +} diff --git a/PspCrypto/Security/Cryptography/HMACCommon.cs b/PspCrypto/Security/Cryptography/HMACCommon.cs new file mode 100644 index 0000000..6fb21e2 --- /dev/null +++ b/PspCrypto/Security/Cryptography/HMACCommon.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace PspCrypto.Security.Cryptography +{ + // + // This class provides the common functionality for HMACSHA1, HMACSHA256, HMACMD5, etc. + // Ideally, this would be encapsulated in a common base class but the preexisting contract + // locks these public classes into deriving directly from HMAC so we have to use encapsulation + // and delegation to HMACCommon instead. + // + // This wrapper adds the ability to change the Key on the fly for compat with the desktop. + // + internal sealed class HMACCommon + { + public HMACCommon(string hashAlgorithmId, byte[] key, int blockSize) : + this(hashAlgorithmId, (ReadOnlySpan)key, blockSize) + { + // If the key is smaller than the block size, the delegated ctor won't have initialized ActualKey, + // so set it here as would ChangeKey. + ActualKey ??= key; + } + + internal HMACCommon(string hashAlgorithmId, ReadOnlySpan key, int blockSize) + { + Debug.Assert(!string.IsNullOrEmpty(hashAlgorithmId)); + Debug.Assert(blockSize > 0 || blockSize == -1); + + _hashAlgorithmId = hashAlgorithmId; + _blockSize = blockSize; + + // note: will not set ActualKey if key size is smaller or equal than blockSize + // this is to avoid extra allocation. ActualKey can still be used if key is generated. + // Otherwise the ReadOnlySpan overload would actually be slower than byte array overload. + ActualKey = ChangeKeyImpl(key); + } + + public int HashSizeInBits => _hMacProvider.HashSizeInBytes * 8; + public int HashSizeInBytes => _hMacProvider.HashSizeInBytes; + + public void ChangeKey(byte[] key) + { + ActualKey = ChangeKeyImpl(key) ?? key; + } + + [MemberNotNull(nameof(_hMacProvider))] + private byte[] ChangeKeyImpl(ReadOnlySpan key) + { + byte[] modifiedKey = null; + + // If _blockSize is -1 the key isn't going to be extractable by the object holder, + // so there's no point in recalculating it in managed code. + if (key.Length > _blockSize && _blockSize > 0) + { + // Perform RFC 2104, section 2 key adjustment. + modifiedKey = _hashAlgorithmId switch + { + "SHA224" => SHA224.HashData(key), + _ => throw new CryptographicException(string.Format("'{0}' is not a known hash algorithm.", _hashAlgorithmId)), + }; + } + + HashProvider oldHashProvider = _hMacProvider; + _hMacProvider = null!; + oldHashProvider?.Dispose(true); + _hMacProvider = HashProviderDispenser.CreateMacProvider(_hashAlgorithmId, key); + + return modifiedKey; + } + + // The actual key used for hashing. This will not be the same as the original key passed to ChangeKey() if the original key exceeded the + // hash algorithm's block size. (See RFC 2104, section 2) + public byte[] ActualKey { get; private set; } + + // Adds new data to be hashed. This can be called repeatedly in order to hash data from noncontiguous sources. + public void AppendHashData(byte[] data, int offset, int count) => + _hMacProvider.AppendHashData(data, offset, count); + + public void AppendHashData(ReadOnlySpan source) => + _hMacProvider.AppendHashData(source); + + // Compute the hash based on the appended data and resets the HashProvider for more hashing. + public byte[] FinalizeHashAndReset() => + _hMacProvider.FinalizeHashAndReset(); + + public int FinalizeHashAndReset(Span destination) => + _hMacProvider.FinalizeHashAndReset(destination); + + public bool TryFinalizeHashAndReset(Span destination, out int bytesWritten) => + _hMacProvider.TryFinalizeHashAndReset(destination, out bytesWritten); + + public int GetCurrentHash(Span destination) => + _hMacProvider.GetCurrentHash(destination); + + public void Reset() => _hMacProvider.Reset(); + + public void Dispose(bool disposing) + { + if (disposing) + { + _hMacProvider?.Dispose(true); + _hMacProvider = null!; + } + } + + private readonly string _hashAlgorithmId; + private HashProvider _hMacProvider; + private readonly int _blockSize; + } +} diff --git a/PspCrypto/Security/Cryptography/HMACManagedHashProvider.cs b/PspCrypto/Security/Cryptography/HMACManagedHashProvider.cs new file mode 100644 index 0000000..79ed4bf --- /dev/null +++ b/PspCrypto/Security/Cryptography/HMACManagedHashProvider.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace PspCrypto.Security.Cryptography +{ + internal sealed class HMACManagedHashProvider : HashProvider + { + private bool _hashing; + private readonly int _blockSizeValue; + private readonly int _hashSizeValue; + + private readonly byte[] _key; + private readonly HashProvider _hash1; + private readonly HashProvider _hash2; + + public HMACManagedHashProvider(string hashAlgorithmId, ReadOnlySpan key) + { + _hash1 = HashProviderDispenser.CreateHashProvider(hashAlgorithmId); + _hash2 = HashProviderDispenser.CreateHashProvider(hashAlgorithmId); + + _blockSizeValue = 64; + _hashSizeValue = 224 / 8; + + _key = InitializeKey(key); + } + + private byte[] InitializeKey(ReadOnlySpan key) + { + if (key.Length > _blockSizeValue) + { + byte[] result = new byte[_hashSizeValue]; + _hash1.AppendHashData(key); + int written = _hash1.FinalizeHashAndReset(result); + Debug.Assert(written == result.Length); + + return result; + } + + return key.ToArray(); + } + + public override void AppendHashData(ReadOnlySpan data) + { + if (!_hashing) + { + AppendInnerBuffer(); + _hashing = true; + } + + _hash1.AppendHashData(data); + } + + public override int FinalizeHashAndReset(Span destination) + { + int written = GetCurrentHash(destination); + Reset(); + return written; + } + + public override int GetCurrentHash(Span destination) + { + if (!_hashing) + { + AppendInnerBuffer(); + _hashing = true; + } + + // finalize the original hash + Span hashValue1 = stackalloc byte[_hashSizeValue]; + int hash1Written = _hash1.GetCurrentHash(hashValue1); + Debug.Assert(hash1Written == hashValue1.Length); + + // write the outer array + AppendOuterBuffer(); + // write the inner hash and finalize the hash + _hash2.AppendHashData(hashValue1); + return _hash2.FinalizeHashAndReset(destination); + } + + private void AppendInnerBuffer() => AppendPaddingBuffer(0x36, _hash1); + private void AppendOuterBuffer() => AppendPaddingBuffer(0x5C, _hash2); + + private void AppendPaddingBuffer(byte paddingConstant, HashProvider hash) + { + Span paddingBuffer = stackalloc byte[_blockSizeValue]; + paddingBuffer.Fill(paddingConstant); + + for (int i = 0; i < _key.Length; i++) + { + paddingBuffer[i] ^= _key[i]; + } + + hash.AppendHashData(paddingBuffer); + CryptographicOperations.ZeroMemory(paddingBuffer); + } + + public override int HashSizeInBytes => _hashSizeValue; + + public override void Dispose(bool disposing) + { + if (disposing) + { + _hash1.Dispose(); + _hash2.Dispose(); + + CryptographicOperations.ZeroMemory(_key); + } + } + + public override void Reset() + { + if (_hashing) + { + _hash1.Reset(); + _hash2.Reset(); + _hashing = false; + } + } + } +} diff --git a/PspCrypto/Security/Cryptography/HMACSHA224.cs b/PspCrypto/Security/Cryptography/HMACSHA224.cs new file mode 100644 index 0000000..9db364e --- /dev/null +++ b/PspCrypto/Security/Cryptography/HMACSHA224.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace PspCrypto.Security.Cryptography +{ + public class HMACSHA224 : HMAC + { + /// + /// The hash size produced by the HMAC SHA224 algorithm, in bits. + /// + public const int HashSizeInBits = 224; + + /// + /// The hash size produced by the HMAC SHA24 algorithm, in bytes. + /// + public const int HashSizeInBytes = HashSizeInBits / 8; + + public HMACSHA224() + : this(RandomNumberGenerator.GetBytes(BlockSize)) + { } + + public HMACSHA224(byte[] key) + { + if (key is null) + { + throw new ArgumentNullException(nameof(key)); + } + + HashName = HashAlgorithmNames.SHA224; + _hMacCommon = new HMACCommon(HashAlgorithmNames.SHA224, key, BlockSize); + base.Key = _hMacCommon.ActualKey!; + // this not really needed as it'll initialize BlockSizeValue with same value it has which is 64. + // we just want to be explicit in all HMAC extended classes + BlockSizeValue = BlockSize; + HashSizeValue = _hMacCommon.HashSizeInBits; + Debug.Assert(HashSizeValue == HashSizeInBits); + } + + public override byte[] Key + { + get + { + return base.Key; + } + set + { + ArgumentNullException.ThrowIfNull(value); + _hMacCommon.ChangeKey(value); + base.Key = _hMacCommon.ActualKey!; + } + } + + protected override void HashCore(byte[] rgb, int ib, int cb) => + _hMacCommon.AppendHashData(rgb, ib, cb); + + protected override void HashCore(ReadOnlySpan source) => + _hMacCommon.AppendHashData(source); + + protected override byte[] HashFinal() => + _hMacCommon.FinalizeHashAndReset(); + + protected override bool TryHashFinal(Span destination, out int bytesWritten) => + _hMacCommon.TryFinalizeHashAndReset(destination, out bytesWritten); + + public override void Initialize() => _hMacCommon.Reset(); + + /// + /// Computes the HMAC of data using the SHA224 algorithm. + /// + /// The HMAC key. + /// The data to HMAC. + /// The HMAC of the data. + /// + /// or is . + /// + public static byte[] HashData(byte[] key, byte[] source) + { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(source); + + return HashData(new ReadOnlySpan(key), new ReadOnlySpan(source)); + } + + /// + /// Computes the HMAC of data using the SHA224 algorithm. + /// + /// The HMAC key. + /// The data to HMAC. + /// The HMAC of the data. + public static byte[] HashData(ReadOnlySpan key, ReadOnlySpan source) + { + byte[] buffer = new byte[HashSizeInBytes]; + + int written = HashData(key, source, buffer.AsSpan()); + Debug.Assert(written == buffer.Length); + + return buffer; + } + + /// + /// Computes the HMAC of data using the SHA224 algorithm. + /// + /// The HMAC key. + /// The data to HMAC. + /// The buffer to receive the HMAC value. + /// The total number of bytes written to . + /// + /// The buffer in is too small to hold the calculated hash + /// size. The SHA224 algorithm always produces a 224-bit HMAC, or 28 bytes. + /// + public static int HashData(ReadOnlySpan key, ReadOnlySpan source, Span destination) + { + if (!TryHashData(key, source, destination, out int bytesWritten)) + { + throw new ArgumentException("Destination is too short.", nameof(destination)); + } + + return bytesWritten; + } + + /// + /// Attempts to compute the HMAC of data using the SHA224 algorithm. + /// + /// The HMAC key. + /// The data to HMAC. + /// The buffer to receive the HMAC value. + /// + /// When this method returns, the total number of bytes written into . + /// + /// + /// if is too small to hold the + /// calculated hash, otherwise. + /// + public static bool TryHashData(ReadOnlySpan key, ReadOnlySpan source, Span destination, out int bytesWritten) + { + if (destination.Length < HashSizeInBytes) + { + bytesWritten = 0; + return false; + } + + bytesWritten = HashProviderDispenser.OneShotHashProvider.MacData(HashAlgorithmNames.SHA224, key, source, destination); + Debug.Assert(bytesWritten == HashSizeInBytes); + + return true; + } + + private HMACCommon _hMacCommon; + private const int BlockSize = 64; + } +} diff --git a/PspCrypto/Security/Cryptography/HashAlgorithmNames.cs b/PspCrypto/Security/Cryptography/HashAlgorithmNames.cs new file mode 100644 index 0000000..5bd4b4a --- /dev/null +++ b/PspCrypto/Security/Cryptography/HashAlgorithmNames.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PspCrypto.Security.Cryptography +{ + internal static class HashAlgorithmNames + { + public const string SHA224 = "SHA224"; + } +} diff --git a/PspCrypto/Security/Cryptography/HashProvider.cs b/PspCrypto/Security/Cryptography/HashProvider.cs new file mode 100644 index 0000000..8b862b7 --- /dev/null +++ b/PspCrypto/Security/Cryptography/HashProvider.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PspCrypto.Security.Cryptography +{ + // + // This abstract class represents a reusable hash object and can wrap a CNG or WinRT hash object. + // + internal abstract class HashProvider : IDisposable + { + // Adds new data to be hashed. This can be called repeatedly in order to hash data from noncontiguous sources. + public void AppendHashData(byte[] data, int offset, int count) + { + ArgumentNullException.ThrowIfNull(data); + + // AppendHashData can be called via exposed APIs (e.g. a type that derives from + // HMACSHA1 and calls HashCore) and could be passed bad data from there. It could + // also receive a bad count from HashAlgorithm reading from a Stream that returns + // an invalid number of bytes read. Since our implementations of AppendHashDataCore + // end up using unsafe code, we want to be sure the arguments are valid. + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset), "Non-negative number required."); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count), "Non-negative number required."); + if (data.Length - offset < count) + throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + + AppendHashData(new ReadOnlySpan(data, offset, count)); + } + + public abstract void AppendHashData(ReadOnlySpan data); + + // Compute the hash based on the appended data and resets the HashProvider for more hashing. + public abstract int FinalizeHashAndReset(Span destination); + + public abstract int GetCurrentHash(Span destination); + + public byte[] FinalizeHashAndReset() + { + byte[] ret = new byte[HashSizeInBytes]; + + int written = FinalizeHashAndReset(ret); + Debug.Assert(written == HashSizeInBytes); + + return ret; + } + + public bool TryFinalizeHashAndReset(Span destination, out int bytesWritten) + { + if (destination.Length < HashSizeInBytes) + { + bytesWritten = 0; + return false; + } + + bytesWritten = FinalizeHashAndReset(destination); + return true; + } + + // Returns the length of the byte array returned by FinalizeHashAndReset. + public abstract int HashSizeInBytes { get; } + + // Releases any native resources and keys used by the HashProvider. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + // Releases any native resources and keys used by the HashProvider. + public abstract void Dispose(bool disposing); + + public abstract void Reset(); + } +} diff --git a/PspCrypto/Security/Cryptography/HashProviderDispenser.cs b/PspCrypto/Security/Cryptography/HashProviderDispenser.cs new file mode 100644 index 0000000..56413e4 --- /dev/null +++ b/PspCrypto/Security/Cryptography/HashProviderDispenser.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace PspCrypto.Security.Cryptography +{ + internal class HashProviderDispenser + { + public static HashProvider CreateHashProvider(string hashAlgorithmId) + { + switch (hashAlgorithmId) + { + case HashAlgorithmNames.SHA224: + return new SHAManagedHashProvider(hashAlgorithmId); + } + throw new CryptographicException(string.Format("'{0}' is not a known hash algorithm.", hashAlgorithmId)); + } + + public static class OneShotHashProvider + { + public static unsafe int MacData( + string hashAlgorithmId, + ReadOnlySpan key, + ReadOnlySpan source, + Span destination) + { + using HashProvider provider = CreateMacProvider(hashAlgorithmId, key); + provider.AppendHashData(source); + return provider.FinalizeHashAndReset(destination); + } + + public static int HashData(string hashAlgorithmId, ReadOnlySpan source, Span destination) + { + HashProvider provider = CreateHashProvider(hashAlgorithmId); + provider.AppendHashData(source); + return provider.FinalizeHashAndReset(destination); + } + } + + public static unsafe HashProvider CreateMacProvider(string hashAlgorithmId, ReadOnlySpan key) + { + switch (hashAlgorithmId) + { + case HashAlgorithmNames.SHA224: + return new HMACManagedHashProvider(hashAlgorithmId, key); + } + throw new CryptographicException(string.Format("'{0}' is not a known hash algorithm.", hashAlgorithmId)); + } + } +} diff --git a/PspCrypto/Security/Cryptography/SHA224.cs b/PspCrypto/Security/Cryptography/SHA224.cs new file mode 100644 index 0000000..5a719ed --- /dev/null +++ b/PspCrypto/Security/Cryptography/SHA224.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Security.Cryptography; +using System.Diagnostics; + +namespace PspCrypto.Security.Cryptography +{ + public abstract class SHA224 : HashAlgorithm + { + /// + /// The hash size produced by the SHA224 algorithm, in bits. + /// + public const int HashSizeInBits = 224; + + /// + /// The hash size produced by the SHA224 algorithm, in bytes. + /// + public const int HashSizeInBytes = HashSizeInBits / 8; + + public SHA224() + { + // SHA-224 hash length are 224 bits long + HashSizeValue = HashSizeInBits; + } + + public static new SHA224 Create() => new Implementation(); + + public new static SHA224 Create(string hashName) + { + var o = (SHA224)CryptoConfig.CreateFromName(hashName); + // in case machine.config isn't configured to use any SHA224 implementation + if (o == null) + { + o = new Implementation(); + } + return o; + } + + /// + /// Computes the hash of data using the SHA224 algorithm. + /// + /// The data to hash. + /// The hash of the data. + /// + /// is . + /// + public static byte[] HashData(byte[] source) + { + ArgumentNullException.ThrowIfNull(source); + + return HashData(new ReadOnlySpan(source)); + } + + /// + /// Computes the hash of data using the SHA224 algorithm. + /// + /// The data to hash. + /// The hash of the data. + public static byte[] HashData(ReadOnlySpan source) + { + byte[] buffer = GC.AllocateUninitializedArray(HashSizeInBytes); + + int written = HashData(source, buffer.AsSpan()); + Debug.Assert(written == buffer.Length); + + return buffer; + } + + /// + /// Computes the hash of data using the SHA224 algorithm. + /// + /// The data to hash. + /// The buffer to receive the hash value. + /// The total number of bytes written to . + /// + /// The buffer in is too small to hold the calculated hash + /// size. The SHA224 algorithm always produces a 224-bit hash, or 28 bytes. + /// + public static int HashData(ReadOnlySpan source, Span destination) + { + if (!TryHashData(source, destination, out int bytesWritten)) + throw new ArgumentException("Destination is too short.", nameof(destination)); + + return bytesWritten; + } + + + /// + /// Attempts to compute the hash of data using the SHA224 algorithm. + /// + /// The data to hash. + /// The buffer to receive the hash value. + /// + /// When this method returns, the total number of bytes written into . + /// + /// + /// if is too small to hold the + /// calculated hash, otherwise. + /// + public static bool TryHashData(ReadOnlySpan source, Span destination, out int bytesWritten) + { + if (destination.Length < HashSizeInBytes) + { + bytesWritten = 0; + return false; + } + + bytesWritten = HashProviderDispenser.OneShotHashProvider.HashData(HashAlgorithmNames.SHA224, source, destination); + Debug.Assert(bytesWritten == HashSizeInBytes); + + return true; + } + + private sealed class Implementation : SHA224 + { + private readonly HashProvider _hashProvider; + + public Implementation() + { + _hashProvider = HashProviderDispenser.CreateHashProvider(HashAlgorithmNames.SHA224); + HashSizeValue = _hashProvider.HashSizeInBytes * 8; + } + + protected sealed override void HashCore(byte[] array, int ibStart, int cbSize) => + _hashProvider.AppendHashData(array, ibStart, cbSize); + + protected sealed override void HashCore(ReadOnlySpan source) => + _hashProvider.AppendHashData(source); + + protected sealed override byte[] HashFinal() => + _hashProvider.FinalizeHashAndReset(); + + protected sealed override bool TryHashFinal(Span destination, out int bytesWritten) => + _hashProvider.TryFinalizeHashAndReset(destination, out bytesWritten); + + public sealed override void Initialize() => _hashProvider.Reset(); + + protected sealed override void Dispose(bool disposing) + { + _hashProvider.Dispose(disposing); + base.Dispose(disposing); + } + } + } +} diff --git a/PspCrypto/Security/Cryptography/SHAManagedHashProvider.cs b/PspCrypto/Security/Cryptography/SHAManagedHashProvider.cs new file mode 100644 index 0000000..bb93dab --- /dev/null +++ b/PspCrypto/Security/Cryptography/SHAManagedHashProvider.cs @@ -0,0 +1,376 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +using static System.Numerics.BitOperations; + +namespace PspCrypto.Security.Cryptography +{ + internal sealed class SHAManagedHashProvider : HashProvider + { + private int hashSizeInBytes; + private SHAManagedImplementationBase impl; + private MemoryStream buffer; + + public SHAManagedHashProvider(string hashAlgorithmId) + { + switch (hashAlgorithmId) + { + case HashAlgorithmNames.SHA224: + impl = new SHA224ManagedImplementation(); + hashSizeInBytes = 28; + break; + default: + throw new CryptographicException(string.Format("'{0}' is not a known hash algorithm.", hashAlgorithmId)); + } + } + + public override void AppendHashData(ReadOnlySpan data) + { + buffer ??= new MemoryStream(1000); + + buffer.Write(data); + } + + public override int FinalizeHashAndReset(Span destination) + { + GetCurrentHash(destination); + buffer = null; + + return hashSizeInBytes; + } + + public override int GetCurrentHash(Span destination) + { + Debug.Assert(destination.Length >= hashSizeInBytes); + + impl.Initialize(); + if (buffer != null) + { + impl.HashCore(buffer.GetBuffer(), 0, (int)buffer.Length); + } + impl.HashFinal().CopyTo(destination); + + return hashSizeInBytes; + } + + public override int HashSizeInBytes => hashSizeInBytes; + + public override void Reset() + { + buffer = null; + impl.Initialize(); + } + + public override void Dispose(bool disposing) + { + } + + private abstract class SHAManagedImplementationBase + { + public abstract void Initialize(); + public abstract void HashCore(byte[] partIn, int ibStart, int cbSize); + public abstract byte[] HashFinal(); + } + + private sealed class SHA224ManagedImplementation : SHAManagedImplementationBase + { + private byte[] _buffer; + private long _count; // Number of bytes in the hashed message + private uint[] _stateSHA224; + private uint[] _W; + + public SHA224ManagedImplementation() + { + _stateSHA224 = new uint[8]; + _buffer = new byte[64]; + _W = new uint[64]; + + InitializeState(); + } + + public override void Initialize() + { + InitializeState(); + + // Zeroize potentially sensitive information. + Array.Clear(_buffer, 0, _buffer.Length); + Array.Clear(_W, 0, _W.Length); + } + + private void InitializeState() + { + _count = 0; + + _stateSHA224[0] = 0xc1059ed8; + _stateSHA224[1] = 0x367cd507; + _stateSHA224[2] = 0x3070dd17; + _stateSHA224[3] = 0xf70e5939; + _stateSHA224[4] = 0xffc00b31; + _stateSHA224[5] = 0x68581511; + _stateSHA224[6] = 0x64f98fa7; + _stateSHA224[7] = 0xbefa4fa4; + } + + /* SHA256 block update operation. Continues an SHA message-digest + operation, processing another message block, and updating the + context. + */ + public override unsafe void HashCore(byte[] partIn, int ibStart, int cbSize) + { + int bufferLen; + int partInLen = cbSize; + int partInBase = ibStart; + + /* Compute length of buffer */ + bufferLen = (int)(_count & 0x3f); + + /* Update number of bytes */ + _count += partInLen; + + fixed (uint* stateSHA256 = _stateSHA224) + { + fixed (byte* buffer = _buffer) + { + fixed (uint* expandedBuffer = _W) + { + if (bufferLen > 0 && bufferLen + partInLen >= 64) + { + Buffer.BlockCopy(partIn, partInBase, _buffer, bufferLen, 64 - bufferLen); + partInBase += 64 - bufferLen; + partInLen -= 64 - bufferLen; + SHATransform(expandedBuffer, stateSHA256, buffer); + bufferLen = 0; + } + + /* Copy input to temporary buffer and hash */ + while (partInLen >= 64) + { + Buffer.BlockCopy(partIn, partInBase, _buffer, 0, 64); + partInBase += 64; + partInLen -= 64; + SHATransform(expandedBuffer, stateSHA256, buffer); + } + + if (partInLen > 0) + { + Buffer.BlockCopy(partIn, partInBase, _buffer, bufferLen, partInLen); + } + } + } + } + } + + /* SHA256 finalization. Ends an SHA256 message-digest operation, writing + the message digest. + */ + public override byte[] HashFinal() + { + byte[] pad; + int padLen; + long bitCount; + byte[] hash = new byte[28]; // HashSizeValue = 224 + + /* Compute padding: 80 00 00 ... 00 00 + */ + + padLen = 64 - (int)(_count & 0x3f); + if (padLen <= 8) + padLen += 64; + + pad = new byte[padLen]; + pad[0] = 0x80; + + // Convert count to bit count + bitCount = _count * 8; + + pad[padLen - 8] = (byte)(bitCount >> 56 & 0xff); + pad[padLen - 7] = (byte)(bitCount >> 48 & 0xff); + pad[padLen - 6] = (byte)(bitCount >> 40 & 0xff); + pad[padLen - 5] = (byte)(bitCount >> 32 & 0xff); + pad[padLen - 4] = (byte)(bitCount >> 24 & 0xff); + pad[padLen - 3] = (byte)(bitCount >> 16 & 0xff); + pad[padLen - 2] = (byte)(bitCount >> 8 & 0xff); + pad[padLen - 1] = (byte)(bitCount >> 0 & 0xff); + + /* Digest padding */ + HashCore(pad, 0, pad.Length); + + /* Store digest */ + SHAUtils.DWORDToBigEndian(hash, _stateSHA224, 7); + + return hash; + } + + private static readonly uint[] _K = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + }; + + private static unsafe void SHATransform(uint* expandedBuffer, uint* state, byte* block) + { + uint a, b, c, d, e, f, h, g; + uint aa, bb, cc, dd, ee, ff, hh, gg; + uint T1; + + a = state[0]; + b = state[1]; + c = state[2]; + d = state[3]; + e = state[4]; + f = state[5]; + g = state[6]; + h = state[7]; + + // fill in the first 16 bytes of W. + SHAUtils.DWORDFromBigEndian(expandedBuffer, 16, block); + SHA256Expand(expandedBuffer); + + /* Apply the SHA256 compression function */ + // We are trying to be smart here and avoid as many copies as we can + // The perf gain with this method over the straightforward modify and shift + // forward is >= 20%, so it's worth the pain + for (int j = 0; j < 64;) + { + T1 = h + Sigma_1(e) + Ch(e, f, g) + _K[j] + expandedBuffer[j]; + ee = d + T1; + aa = T1 + Sigma_0(a) + Maj(a, b, c); + j++; + + T1 = g + Sigma_1(ee) + Ch(ee, e, f) + _K[j] + expandedBuffer[j]; + ff = c + T1; + bb = T1 + Sigma_0(aa) + Maj(aa, a, b); + j++; + + T1 = f + Sigma_1(ff) + Ch(ff, ee, e) + _K[j] + expandedBuffer[j]; + gg = b + T1; + cc = T1 + Sigma_0(bb) + Maj(bb, aa, a); + j++; + + T1 = e + Sigma_1(gg) + Ch(gg, ff, ee) + _K[j] + expandedBuffer[j]; + hh = a + T1; + dd = T1 + Sigma_0(cc) + Maj(cc, bb, aa); + j++; + + T1 = ee + Sigma_1(hh) + Ch(hh, gg, ff) + _K[j] + expandedBuffer[j]; + h = aa + T1; + d = T1 + Sigma_0(dd) + Maj(dd, cc, bb); + j++; + + T1 = ff + Sigma_1(h) + Ch(h, hh, gg) + _K[j] + expandedBuffer[j]; + g = bb + T1; + c = T1 + Sigma_0(d) + Maj(d, dd, cc); + j++; + + T1 = gg + Sigma_1(g) + Ch(g, h, hh) + _K[j] + expandedBuffer[j]; + f = cc + T1; + b = T1 + Sigma_0(c) + Maj(c, d, dd); + j++; + + T1 = hh + Sigma_1(f) + Ch(f, g, h) + _K[j] + expandedBuffer[j]; + e = dd + T1; + a = T1 + Sigma_0(b) + Maj(b, c, d); + j++; + } + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + state[4] += e; + state[5] += f; + state[6] += g; + state[7] += h; + } + + private static uint Ch(uint x, uint y, uint z) + { + return x & y ^ (x ^ 0xffffffff) & z; + } + + private static uint Maj(uint x, uint y, uint z) + { + return x & y ^ x & z ^ y & z; + } + + private static uint sigma_0(uint x) + { + return RotateRight(x, 7) ^ RotateRight(x, 18) ^ x >> 3; + } + + private static uint sigma_1(uint x) + { + return RotateRight(x, 17) ^ RotateRight(x, 19) ^ x >> 10; + } + + private static uint Sigma_0(uint x) + { + return RotateRight(x, 2) ^ RotateRight(x, 13) ^ RotateRight(x, 22); + } + + private static uint Sigma_1(uint x) + { + return RotateRight(x, 6) ^ RotateRight(x, 11) ^ RotateRight(x, 25); + } + + /* This function creates W_16,...,W_63 according to the formula + W_j <- sigma_1(W_{j-2}) + W_{j-7} + sigma_0(W_{j-15}) + W_{j-16}; + */ + private static unsafe void SHA256Expand(uint* x) + { + for (int i = 16; i < 64; i++) + { + x[i] = sigma_1(x[i - 2]) + x[i - 7] + sigma_0(x[i - 15]) + x[i - 16]; + } + } + } + + private static class SHAUtils + { + // digits == number of DWORDs + public static unsafe void DWORDFromBigEndian(uint* x, int digits, byte* block) + { + int i; + int j; + + for (i = 0, j = 0; i < digits; i++, j += 4) + x[i] = (uint)(block[j] << 24 | block[j + 1] << 16 | block[j + 2] << 8 | block[j + 3]); + } + + // encodes x (DWORD) into block (unsigned char), most significant byte first. + // digits == number of DWORDs + public static void DWORDToBigEndian(byte[] block, uint[] x, int digits) + { + int i; + int j; + + for (i = 0, j = 0; i < digits; i++, j += 4) + { + block[j] = (byte)(x[i] >> 24 & 0xff); + block[j + 1] = (byte)(x[i] >> 16 & 0xff); + block[j + 2] = (byte)(x[i] >> 8 & 0xff); + block[j + 3] = (byte)(x[i] & 0xff); + } + } + } + } +} diff --git a/PspCrypto/Structs.cs b/PspCrypto/Structs.cs new file mode 100644 index 0000000..850e09c --- /dev/null +++ b/PspCrypto/Structs.cs @@ -0,0 +1,264 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace PspCrypto +{ + public enum SceExecFileDecryptMode : byte + { + /* Not an executable. */ + DECRYPT_MODE_NO_EXEC = 0, + /* 1.50 Kernel module. */ + DECRYPT_MODE_BOGUS_MODULE = 1, + DECRYPT_MODE_KERNEL_MODULE = 2, + DECRYPT_MODE_VSH_MODULE = 3, + DECRYPT_MODE_USER_MODULE = 4, + DECRYPT_MODE_UMD_GAME_EXEC = 9, + DECRYPT_MODE_GAMESHARING_EXEC = 10, + /* USB/WLAN module. */ + DECRYPT_MODE_UNKNOWN_11 = 11, + DECRYPT_MODE_MS_UPDATER = 12, + DECRYPT_MODE_DEMO_EXEC = 13, + DECRYPT_MODE_APP_MODULE = 14, + DECRYPT_MODE_UNKNOWN_18 = 18, + DECRYPT_MODE_UNKNOWN_19 = 19, + DECRYPT_MODE_POPS_EXEC = 20, + /* MS module. */ + DECRYPT_MODE_UNKNOWN_21 = 21, + /* APP module. */ + DECRYPT_MODE_UNKNOWN_22 = 22, + /* USER module. */ + DECRYPT_MODE_UNKNOWN_23 = 23, + /* USER module. */ + DECRYPT_MODE_UNKNOWN_25 = 25, + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct PSPHeader2 + { + public Span RawHdr + { + get + { + fixed (uint* ptr = &magic) + { + return new Span(ptr, 0x80); + } + } + } + public uint magic; + public ushort modAttribute; + public ushort compAttribute; + public byte moduleVerLo; + public byte moduleVerHi; + private fixed byte _modName[27]; + + public Span modName + { + get + { + fixed (byte* ptr = _modName) + { + return new Span(ptr, 27); + } + } + } + public byte terminal; + public byte modVersion; + public byte nSegments; + public int elfSize; + public int pspSize; + public uint bootEntry; + public int modInfoOffset; + public uint bssSize; + private fixed ushort _segAlign[4]; + + public Span segAlign + { + get + { + fixed (ushort* ptr = _segAlign) + { + return new Span(ptr, 4); + } + } + } + private fixed uint _segAddress[4]; + + public Span segAddress + { + get + { + fixed (uint* ptr = _segAddress) + { + return new Span(ptr, 4); + } + } + } + private fixed uint _segSize[4]; + + public Span segSize + { + get + { + fixed (uint* ptr = _segSize) + { + return new Span(ptr, 4); + } + } + } + public fixed uint reserved[5]; + public uint devkitVersion; + public SceExecFileDecryptMode decryptMode; + public byte padding; + public ushort overlapSize; + private fixed byte _aesKey[16]; + public Span aesKey + { + get + { + fixed (byte* ptr = _aesKey) + { + return new Span(ptr, 16); + } + } + } + + public Span keyData + { + get + { + fixed (byte* ptr = _aesKey) + { + return new Span(ptr, 0x30); + } + } + } + + public Span keyData50 + { + get + { + fixed (byte* ptr = _aesKey) + { + return new Span(ptr, 0x50); + } + } + } + + private fixed byte _cmacKey[16]; + public Span cmacKey + { + get + { + fixed (byte* ptr = _cmacKey) + { + return new Span(ptr, 16); + } + } + } + private fixed byte _cmacHeaderHash[16]; + public Span cmacHeaderHash + { + get + { + fixed (byte* ptr = _cmacHeaderHash) + { + return new Span(ptr, 16); + } + } + } + + public Span sizeInfo + { + get + { + fixed (int* ptr = &dataSize) + { + return new Span(ptr, 0x10); + } + } + } + public int dataSize; + public int dataOffset; + public uint unk184; + public uint unk188; + private fixed byte _cmacDataHash[16]; + + public Span cmacDataHash + { + get + { + fixed (byte* ptr = _cmacDataHash) + { + return new Span(ptr, 16); + } + } + } + + public Span CheckData + { + get + { + fixed (uint* ptr = &tag) + { + return new Span(ptr, 0x80); + } + } + } + public uint tag; + private fixed byte _sCheck[0x58]; + + public Span sCheck + { + get + { + fixed (byte* ptr = _sCheck) + { + return new Span(ptr, 0x58); + } + } + } + private fixed byte _sha1Hash[20]; + + public Span sha1Hash + { + get + { + fixed (byte* ptr = _sha1Hash) + { + return new Span(ptr, 20); + } + } + } + private fixed byte _keyData4[16]; + + public Span keyData4 + { + get + { + fixed (byte* ptr = _keyData4) + { + return new Span(ptr, 16); + } + } + } + } + + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct PbpHeader + { + public int Sig; + public int Version; + public int ParamOff; + public int Icon0Off; + public int Icon1Off; + public int Pic0Off; + public int Pic1Off; + public int Snd0Off; + public int DataPspOff; + public int DataPsarOff; + } +} diff --git a/PspCrypto/Utils.cs b/PspCrypto/Utils.cs new file mode 100644 index 0000000..fecf35b --- /dev/null +++ b/PspCrypto/Utils.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace PspCrypto +{ + public static class Utils + { + public static bool isEmpty(Span buf, int buf_size) + { + if (buf != null && buf.Length >= buf_size) + { + int i; + for (i = 0; i < buf_size; i++) + { + if (buf[i] != 0) return false; + } + } + return true; + } + + /// + /// Re-interprets a span of bytes as a reference to structure of type T. + /// The type may not contain pointers or references. This is checked at runtime in order to preserve type safety. + /// + /// + /// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T AsRef(Span span) + where T : struct + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + throw new ArgumentException( + $"Cannot use type '{typeof(T)}'. Only value types without pointers or references are supported."); + } + + if (Unsafe.SizeOf() > (uint)span.Length) + { + throw new ArgumentOutOfRangeException("length"); + } + return ref Unsafe.As(ref MemoryMarshal.GetReference(span)); + } + + /// + /// Re-interprets a span of bytes as a reference to structure of type T. + /// The type may not contain pointers or references. This is checked at runtime in order to preserve type safety. + /// + /// + /// Supported only for platforms that support misaligned memory access or when the memory block is aligned by other means. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ref T AsRef(ReadOnlySpan span) + where T : struct + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + throw new ArgumentException( + $"Cannot use type '{typeof(T)}'. Only value types without pointers or references are supported."); + } + + if (Unsafe.SizeOf() > (uint)span.Length) + { + throw new ArgumentOutOfRangeException("length"); + } + return ref Unsafe.As(ref MemoryMarshal.GetReference(span)); + } + + public static void BuildDrmBBMacFinal2(Span mac) + { + Span checksum = new byte[20 + 0x10]; + ref var aesHdr = ref AsRef(checksum); + aesHdr.mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC; + aesHdr.keyseed = 0x63; + aesHdr.data_size = 0x10; + mac.CopyTo(checksum.Slice(20)); + KIRKEngine.sceUtilsBufferCopyWithRange(checksum, 0x10, checksum, 0x10, + KIRKEngine.KIRK_CMD_ENCRYPT_IV_0); + checksum.Slice(20, 0x10).CopyTo(mac); + } + } +} diff --git a/PspCrypto/__sce_discinfo b/PspCrypto/__sce_discinfo new file mode 100644 index 0000000000000000000000000000000000000000..eff16fcae198bcd1ba759664fba2f0c4d10c34fd GIT binary patch literal 37632 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 literal 0 HcmV?d00001 diff --git a/PspCryptoHelper.dll b/PspCryptoHelper.dll new file mode 100644 index 0000000000000000000000000000000000000000..f7f964b54228d7131b3e53c929f43ed9e63a2ebb GIT binary patch literal 2596352 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

`noM_dHtymU~CVU#Gm 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¥nG_K&L4nj=ZvLasA#||%3fVmT*K|rVD_VL zCo$@0as0TxP8guBu#FuW@*ocqUc**j32oV1qQdyK0`%>+1d;{0a<>~ydR1C zgrGlwoe-0?pY@LS%=c7d2?vPck3&0~y7Mtv#webW+S%$x8^fGmsx!B#3okr1@>=lA zN6y}*f~=wy_v84A@dF}3rhCa?5WxuqRj

QLjb?Eb3N9f%5aOjl%kr^q!SEwEBl% z5XIC%7KsUEy?FE*~%?=Nt}u~Gt&np3F5y*!l+_r=-iLPn!%A{ za5&x7rIuRuB-0_(ht*-~b8(}hb9U%ui+589fP^DHw7LZzMEDTC?;%DK{t^0nU1)(W zcY6eOsWiWVeexskNMDiS2zJ85)gh(&Lkl~Ea>`>S`xACIxJ@0jDOZ1MQaf0fNU?{} z)2+LvGPeMh<47PT^a76Lojo|VP-{c2g&s$NG>!Mx$OX`9>H&)fTmhHvugBA_qB~Bh z@y$lkq0@r_L$DQrE3xNxr;eVyr9WN0nfy2Gzqy2uPhzj&OZT9p_X zl|xz-)$8wT>MLgoLQwM=Ut8Gb0(ci0KseT5-GiDwjyM zB`X~b5_jRdcGdhe8gqj_eL@6|ZiDTP0=X-E$`ZEt&uX(e$Jx{G<&U_tPU`K(Ybw5X z?2bq?F}XwG263@w^&j*y`c7qXk)B4QKf*x-4@k5wnwY_`E^)M{oT8Hty30I_LK)T+ zTp(_S7G$$mdE1aqi>yT*X)***15Wm=S*HgVC6}C>*c1iBf99xv6ag2$g&?bo3of2l zPAIG-7-iLDHi>j4Lk)?eXkNu`UVMsL$T>p`=@P$Hwj%k3YlJZX*Ypcb&QzL}LZR_G z==9h5)P=HKm2Z){+W(p0uzTfF(Xi??Fr+B>&Z)Sf|27I51?;(gKo?bV=4k4cV0oPT zcRc(1wLOIzrDW)-OGhD#8;Wk3b|!2n3KEniHiCTHrGtY=njYqDA%vd;P}hBh%|opR z4FaoK#uC~{t~ZpeQdH*a!bxPo*ZC|QOnl6%<(2Mgh#(UT7p}fmkbApg*1+3PvZcra zBOSpq6xdFmb^{+0X{LQBm{WxPrN=q~-OM|b+S(M+u=ai-!-R_>Ka+fVN2yYR3q<{j za`s+cz2t0J*gIT4EiCf&fQymEY`eEWP>nVmp_L|~LSeRW>KW@gSgc?-B&!aXyL>Uo zR3=_kjKGY4FxciGWi9@bi9%QNNActass6g{qFJPRF<*QqLiA2`5Fr7U^dQ`B?R%lkQ<#Ayh|J ziD|$NuNA~Sq*|J=-9EwgxSrHcAOw1F7Ejmwz17fI1^iZq;+Hnwy3PS{b082as7D5n z=7(QT1wfC~4vvrmIE8uCGxn)cvM5TLJkgG14@Mt$_AWj)W6(C63zJium&A{(b8s}5 zRn1-w4mgB4JpROMEVAR@>w9cdfl9hrG-)NbwmR|=E0Pp&H@s8ZVEkIE$VEn@^0^>Y zBLb=i#ShL*m7-GT!r^?oF(-_jGoTo&2$u6~Lp3*zWqPbnE1As)D=f0q&kgkK+JjC$ z>NzjI$25vAZKbeZ01WRqfZc9tA9G@Luslvc*CAIM?(X{$6{H>o3?1)tUhmO0eZX_k z@BTuQlroA6pfKgIzh3eFw^dylNg~rc;GI-AGFx;4_OUWc&?B6k~Fkmd72jAilE~IRo!VIXJOhbCL{weciyKDg41`&^|zywuNNeJISiE zs=pIbukH=0#tlIg9Ar!a`vb7V1jSp7mxL2{YD$KO1Y|ebcPb*r8<_qlN!3MOJK zLmToFX0ONTQ%S4Itn(sO(Ls(PNt06VS-?7IMFLF`M2JmQ7a*{Y7n-HdRe;V(MIiRC z(Ra;RX9mYU3ZS4V!g2!daS%taw#3tG&zk^uQ7|H?I3_B;TVz4sSsm5Dqo=_ALJ_F$ zV{lAu%6_5j+}U95hqwz>B&{G(j?&h;0&%7YW9{n*r2mT)F)Rcyj;4ymnhE_rQG*nG zJ!*Okk1e`&Ys~*txp*Fy&)QUpuc@SOX+`$zn5+gs89_QAdZr@wv$#Pf#@?9^9t$n# zG`>KRk;o@Ce~^(kD;CEqa8{|oju?=fc9Y+IFB8hNB_Iy@M9w};@nQ^V?5bxG%;O>$ z6m+c)rTKrub(_y}f;3GQEBXR$)mfPu)|tH)DRvm!8Be@`tief8&V0;+y++uo>sd`n zC?Kiasda~`OdoFHO6&&_KZ&|^SnDTc#Vlq~O(+136SNI$IyHinr826js>j(!oMxq7 zi&FLf#Un2<0wVLFqyK7p6zJoi-7O$oJwp4`8;5JG6l6V$78<4Df^S$zRIW&I#uB%< zJ|lL~OD>wJ9D#T`B8UOujluUJUOZKXvU{>oHNh!w3yb#%MQ-Xm6pUxmU2hj<9^y+FgcJzG zIN#@7QuimMv5IsF4!0jOME+i&P`aG*TK5&E$3}RlKIdLnEPN`OL<&EoX5~UX{E)3X zOh-jm&|!-`zDIPDz)m{x9GyPqrhxNPo>fSq1L(Z zG;2l{_ywA;iHZOU!9soeD6kp0_OnSZ$>@LMt)QjAe2`dt8l6>9PZhJ)4td$O?`BXt zd8($o40@+71U@1>Uy&M1hs9|G?_C_)!M1aF0D}&CRoMK}h0j&7^I!yae0&dMfaONZ zDg}pTu6~e!DPp#A41xMpovWZLX^n+ zOTzG8x(9Rb;JqO0D&N}D#)1(Sal*yjOrD?pEc-kUO{`fjkHq~U|tUb`}Cwz&1*-kRZb)eGJOOElN^XYCV^qqgQ*V;1pArC2m z#>)E9Djw#|go8LGQWCBY;a&E zwf;-GUEH9%^^@s<6H0OkL5N2Ln*K7YX#Bb0@O## zS@LR?!cj$w?sx6yaf@lk)d};^d*8jvfaf$U!U1CRUMWo2_XMlXs1rk!a+DnYOv;k1 zXaY;o8hR0iYl#j1WDt;$~)W;~~|DuRz7vTJ*Y2hd}YEaxd$@zL= z2%!!L4*COOL((4Sl-8q?s_K?>yio5=JM_S`O%Y^_rE1R94*-1X=oo@Th+i1{U4g9o zt5+4vkbceI?7y#7X3--`tXv2mC-Z?AWuL4-04egJKxR2z*k#K4Tor-%CEQluJpn@A?hDo9= z6&N)(*G`1TCvMnpM=vPc@2>>P+D@QN;~8PRZY7J0*M|q`6Qov9zI0k02JV9OZ=v4A zffwYW$iJB$$gtooepCx-O;2Lvn#{djRD(K>9-Bb6+jAv3HOZ#LU@1tLhv8bSSrG7F zc@i;~pImX_ZcdmtL?r0VYqsoINXu33`>(4tXA^vm!@gaW6=qaZ(TX@Mc)(FFmEguY z#iX04-ys_+c;+X<+tN-wkS}P1EjxV3S-K82lYX2{$eoVYCbAB&A)8;WdnT`x(>vp? z6)wId%!aA}#g1xy_6tu~24nd|szz89zrx}`m$3c>m+7emB6?zF#rwk0bB*lijSRw% z?LHz&J2t3;CF35Zu)@i?YzrD($lH+8!}m$7!So)NyWgm*!pCJyO|!3r>c!&c5Jnxf zN<5bvZ9j>`rCXHg*Uh8u7eoewNl{=c-)pTrvRM?vCih%H!?GhvHP$jUmV!DMEE%gG zkWVR37BF!B%U|rKYb4^g{Nmya8X6+AhNrPZvHY#i+ z=_K${UUjC*ue6<{2m0WU7lUSf&chYfu*;@U^G;i7Ss=E=uSzUWg~#>+KUP9WJD45S z&T0Wwmz0^e$& z-s8fPHLw^j+!L4q3)dMb9o(iZYk)#T;c@s000Yx@lJu%Gv;Lt=zzfHoJ6N?KUul(i zzOWs&63~ee7=-*o7WM^l@(E-pF^JW4LDN7pm_cXtmXX&8dpm|w;#pt}a8?`_7}|uy zK8>BUq_Qs4c6T`(m0`t*H2c9%N8!Sz@|U}=2uH?O7MI3-Ff zqRZ3(CZwK~xf~&hmOo$L3r?EYfTU$aaL2o zD_MVct%&wU61?{5-4zc7Od$n=D_;$$c6y{@E9hb{O(nbTRmnc!U!V~-n^K)_^sj#Q zJ)VyN6RTC2b}^Z8<733FN7~Fd6ea!YmDVFiHJr+|iRBhlrel4?Q)r)P`2w#9Z=JX}C!Mwu_ zAV_~l*d?%N8Gufx;LC>utgm#C)?fm z@eNdXOuElof;Z1p6tTX5`R{jTu)$!Fsi8o}p^5l_i2>-~OTu>PD#f|<4rN8oy!!>m znxcvj4*;%k!~{=raZ89LT)P?K{nUJE2!!Y=vUg;L2EAWdh=!=T-xd)rz&!GHcYWAD zR?T+5D%h!*b)(|-aKYxwj2K^sux3yIB4~lyS{26&Ie?Ms%IkAa^-SSW7qoYFmt;18iuP^hK)hT$f2N8nkXR4A&8i* z7|hHfC3ATv;HL47Ki{fNHNqE@EI9Xxwiwt9XyIJxzUC=<)cJDpRiYEO+Q4*CDf9A8u3Xo+)34x9sPOrdm`>K$`0N{$&Sd+l`cY=zbSWTq4A zGNy)+5~RHe*({U5-NeAul@5i;!hbaQBJ{w^YX^qJzKrIE(rnGZz~}kOCtnZSO_^o? zSq-TBVN~cN5MX8+NCp4QPQF z!%OImU7IEFMXo(w8;`^5$8)=VVXo*(rv>D_Rve$k=^DGFmhnlLYZr_w33X@@^GL)q zV3(iXl6Ic{U{o1G|LbKx8#D8di^?yt)$=~uu8y`TyKTZIv$uJbQ+WlIW={O?O zLEQ_kLV!Uqbd>PR#NEx>t@&{W*)7Euz>si%1mY=8^u5`p5fB@I{_*B`+^pHW%sRGP zsd5rE;>XX(YA2=_cV-Ot?$_c8Hw`>KTWFdFE8SXUUFsAq(>gW=)qRZyAK>`LsE6#C#-4w<*U-PDuR^G8nRb z?<`WYldD+;^ls&G5E!2kx?dvD4l@;%WxX@^GTR;1X}-jK2uK8Ic1$L&EkkiXkT9%A zzxe=WvQan#ZQX1U(+)r+B>njQd0qsIscrg7m;L=ZXC4ee_Q^$PQ7l_C=kj(_7ho#2 z>)IMgEqujHQHHW4{R z?xv*Nr(};G`#obfndL7>C4ZNd!-DEYrXFS*-`~qoR0z3|#mvVO9(I9Zc^86Qh?bFW zSy$R{z5ggl=oh?i({Uz23T__t{8B+Ihpu5%=Xaj4ha5x+|4pOSoq5o4v8ISbPHT3O z$MF#or3TzST;ofVt^(?wMhxj~P0Hn5|KwJVHuOy{_O45-tQUCbkz1$&B3hpicI9m5 z>t>b%{yX>cF|9Jz^t>gcFoR;|R!~us1(oVnAv58L9D!NavJs5_@tnxO4CVwAIkt6m z(LzG1+FoYFW$kclh)f@kC55_Fu3Tbhqg?^;JbX)~<|M(7?y3vRK2;x6%xwUzA3B zC^J8ihkVu`TX^^gy*HL&dP>FDdA=BoS=3bFd~L+bTAqk^SO|-^TTz-&Pl+zW(|Cx zL0g6Kn!$V*<8K(tn%VvT!DVEgxpZki!sSN3d~u?S&!=@*<|_t2ye>_coFHFEL`w-; z%Rss9h4M-eFUWk7*9K?$CItg*cgV;pNSvpmBoDlIt=qRLKua!>9D=glmh$!MSxDSEgcW!3!fQo=kHJU;5= z(`-B6l6`mQGN;8uK|y+x^dF}hm6fRWeABy<44D4^?$6$2UcE)>LrkQp!VQs4^{LDz zKS0ad_5+jqy^E53=I*H;l{Y-YVVU<*8rC~`np`>NIHCzHHS1w5%0Ou`R)@h}TpoSV zAR8VmbQvCQF8m`1*)v8-k(CIkDQ>5weeHytod#i1t-M{&5vmK?Nf?%7>>ki4j6JU( zU$>J)?xGx@PrO%H1>gWJGA3H@BcNee)`%ExB;~oZs&XB`K33&OW1^`;nnS~58JD29 zwYjZJ)qPe=Qd-2J!TNIu<~aWMe(8@$PdGo)NQYog35LX!7gw;>sPl`XRFZ}z4hu2S zjfxc5_oJFWpR1WJzy@cE#wQ7E4tZ&8=d~hiH*suWQZj5;X734*cBNA~&;RXG_+yP# zZwfpF?b5WKn^6+ps5;x4Rtg@(={Q~DM>jCPC>4i|!L{{#=Ln)B`D@CEoD!X%g95*Q z8Na-gF)}AG@SB^RrJOU1S_L~Gm1YkTBWtuIUw%m+pLiJo81SlSlu4YN)oF{65$;uhvl;@$7_ya5%DsiiaXO>Jt?oWn7Y#Gd4 z&JJ7JlfJ=O!7;%ipM9uRR^;51)wV0nw+Xn(p2Q+7EXU$&NI{A=zo7mglskcGtZk<0 zZqu`e$&50Jz};_QWHLG|%{vOZ+Ys2D!~=1}qgk?JTBSD@st+c)lV1T8r5Wp(HJqN(>(^0^7cv=b#XnMn=8w4y z2OUsY6iO1Bz7N(vYrK-kVOONfCvW~(NI3O7cAmBOaRN(Tj#^u(21Y%Sf}{nTUD_T- z#Qx^cF?ZT=Sk#b{Hshm>Z59SVKz8W^1gVLFHp8_y*70(KdC^Nc#nRQDXBMW1JJ0>t zyu@BkX==6;c5SZZC;a1f{9ttMtICyvn?`Ws0hNfQOak6z(ryO^+}dlh_zqfYBe4wm z>v9Rd-DN;Bq^u%!INkAf|4MYvmy4(*J&ykUnl;h$EKrdvx}#EEw@nDBGB_;Bu`u4< zEujPq)*s`%>H>JH-G8Geb8s_rsjvF0^44V<`Q{P0#U(WhQ$}COu&#D>rjsxBUxDa( zhV%u5mh7g4=!!K~waaaDIG*8h6!r?fmiDD`Lo;%4`yy}ZS6;@D>THw@qm3pi8)#*R zifR+oM4u1bfnKdzSVk;hX`1k{(MQB&kz~=a$eoACHcLB9BWpW*7t|oo*)SIcC29#B zQ-JTdxe*3kNxNh_OOU#H6r+>W*^&`ORaTnZr7)ioa`0};`LU1_{D%!jg_S9~xqZR^ zJ~Gp&L>9}8R2a*=|4C2O2BR2+hAvF;Sk@?UjGw8^V7f&ALibDCtG8#FKTDCE4>4w8 z!4mW`3PY|h<*bO)c4r=a2G>#~`oK#Y%?FRFhQb5EvW2OP^SDel|2vF;zRZJl zNj%2kA5(xO+aH*T9#l|kD@^^NkrLd>Pv52&I%&WI@)rS4V8pV}xS0xL8DAtsEFs3K zAl$B~QV&vAPVKSddsO&!^+^7hU*xIHGjIw30mT@p(?v_#K^>-Q&8D(zJd;=Tw7X#Z ze+?_Wj#H<$Cf1CNtD~M;VgWXUq;rA@9T^wZuo>Wdild%Xp z`B*@m*v88A=K8|NoE49Zimgi=U<=E$q}vUQU)deC$4}Z-!llmFJQf{Hj7ld4eQ+?& z=>jMS;MsWv)!pX5o}vO8?F+SZP6jtueKMzIu4_NMAUXK)^+q_G8{c4&bi;?Aq8%-j zDEmx2bkR=Ac%%@WgDggT*ubY>kj-Jl41wI z5K!172u;%;&qM&rsyf$-=e47|SwZ_hZ-V9b$A=;T8VguL_v%V{J3?SxL4fc?q28y> zA|~N-B-UA$N$N=xIUiT=ktwu_TC(*N@(>1(p0_ZC#jQu8WuOQ4Jr4Oa;NnB;T?$>Q zBtS$oTKiLfpA2hgjAE!(Hdd^h!fBkgN6xzIA0t}qqNSdR0?xfYg=3VgAKC?~c%D#F zEPjLvU{Jug*m>CzRa)ABXZ6Ni)qZpIr^gKhFb(1Z z8kF?`bIt-f_%Q7iQty#p*pfiOe{v`q>W&{4wc2!Q}OK*qoHXNgR%FzwD5=WP_d?YxG` zdhpcw2p;-_)sER(`_ZeuKGT4AmJvIUJW(*?#K?Q3p576Ii!#Bvv5V~%Yvlx>CWq=i`HfC)l6x*HUN#7GF^x_>Co*{ZM?a@ z2Qz--XlGoX2f3^14;J!K)y$SdrfoAvjEDdwL(XrK*`7Z*Fe%yu$I+dDC$db6mi6{9 zIBTVIe++O9WuRoMGwLN6fZsS1HMhB{fAwRV4Gr0Gc`+ui+ejy7O;7(T7}94tNAc}|~b7hh~`w((E0 zkxf*3jLjDQ>!lJtFNbTT>2x(zqK@|b3{|b6_YPJ+(l?%Hi+a?_%DGgGLc@v(N($6Q z(8hKZd<;eG>4}yf9OH?#7#DcB0w&MeLTk&&#y66vf%9&;ll&$VC3x_)ZEV$5E#gUZ z>Mpvh05CjbdWhh3anH-wa?xzdzVU3s-hrMln>nsvZHl1>e&yVrc_wVLK^*|!^pdzz z#`2*SU9N=K3OCH}B$P6n-ytr&^4cIVnF>b&EsZHvF0);rY_E6=HdY^CF{5@|obsgj z|6%B=NsW)zK$6^W95S(ipJWb}&g7DZ8-Of5b{JAZa`$=kv3YDkyp!z<$y+`q1-yTz zNw6JKFy`n19z*lSIA`*9L3JkM0?P^Je20vIE&mL$%M)sN&UFqSO;lJBWXk>Y&>pvJ ziGFuoRY$6+H#Z|ZK&vO^k}*Yl{$KDpi1u0APFI3$;7d=B8!ajB3`OJ2hS1Dm5{g8s zXGxLwvyslL?WG7AoA5V9=Ry$m9zqhRG&&3Hyw3IdnzN=%hy4$8LeuX8?Q;8Y+m2k< z`87xDQL1>tS0Nw4zy3SFHTGG=A7I&QwD2D~bITyaBPh|yqZKJU;`5q5Kr|e*axBQ; zUz!8s}Y=KgmAukX; zcP_R$eo+W$*qRn;Aynd$ScPD_ao8c(oB!1IuIuDrK6dg!83>^ZM>6hFzC|v|UwZv3 z>~hg&EwRvaFOP~++;G0yI39oDJeZT87IR%4F+M`yN*J-?X;!ITY z1Knt>Y2{JL?)fcW94Gm+kEgvaRQ!XF zuYsE2K~ejPW;n41U54XB4@mYn*+<_UsCV6R_g^4Pap|b?*op|9*h$nzZ*W~0>KSH| zVt?Jo>jHk{F(AB*ry%j?$x4lx0Pl;z+9Ka;oO<&Z--6tbFGIZVQ8sX0j_)kRrGc2) zpnHgd9qb^v+MVotJB>c?r79jhm)kFmMP|g^34j{vk-L1go566yn^A z&+Y6bVRNsBaxY@F;<-3*swWZ-clsc#bF%NyTbJK?i}-(mK2Vt%hVpBLM(j9HA2rpq zZm&Z8#Zn4%KN`C%6}vbtDKw}T{+a8;bh~EV@yk!j{OIM))`SO$zM$u{98AdFFQ*l~ z4N-uH;V|rhukpM@J_lh6s8Ajhu7|!y7*~}IazkqO7Zj#w)58bGhkkzbGV@YW`1(2o z(xg%qA>kCvv`S68lIGsFq!u`!$aJf=DM~qSHrB$D|eQq*W{NKz< z;5<{w1p1RXuRKRn$n-s-lj{a~Z{*9D_EA2k_{`)A;X13)yggfr2t{EfCmW{`V)^f3 zIr_J2D?xGJ@AKCyxTYJkDGeBD6~wu)<=spHQbob;yGwf_OJXc=^N8uw!srz%1pW7< zZOl9Fg>0-sDuMu=xAOTNrp~Y}SRUR&tJ2EXVY_cFCe>~<39m02l_{39E@35nfTgG} zkYx3I+61mJVL9p!2q|WDM+gW##q|j`_S8XAIUNYgV;Yus5IFm^{ARlUeT)dl3gZ@e^dz@ zt8Uy+*ZCqNP2MncE%Nffsi} zfGnRi9pr*^PINV&!X&cu?8BOxu2B4u{-$A(eEJi{YsSopTJbW+6pJQ_1MO$D;`G*h z4zyOzX_)u>nBFbGObI^u*DUVssKemzLj7p6!2k1*?HVKqR{f`e=&!K=*m|p#QVlUa zR&WwPL;TqZvEKjFl!Y46kzDx5xNXldSkiPZ(BHKJqkNo#hCCE$zR!u{*k~7&1!No0 zU4p*@l{pYa;up6oa>=dwOYh=a77TXmr*PwGeWX1OESd?kvJ6OFr*fu2uaB*zBnVH+oW05fjo| zFzyYy*N~i$A{*3h)0T#OShx1YfI8?rsOq>4Z%a$EH~yE>4IeF1=&xA*+1r?mHitEr zC#6)B$|k~V(85#BPB<%}+6G2meTkiSuoCQZutz?nt?^b@y1Ok#wm1ID@(KQF7cS21 zzaBj&{Rj3uIMu<5B@E#b|MxY3AXRLB$23iaSn9dl@A-YgZDxknX(L}ig{l_>zy8rF z{t8Evo`FV~o`0g^X_jg}1Z5uMq6p9=3TU^S&(5dYLvD>TLG`cf%+*0fbKUn=%80y{ z-X_$w!=lrkYfsrooG17$yQ5TN@@E(eTSj%&QBfk5|FD_w*Z@WOh@Ii{Al5D8Xfmjh zqDgpSpUFOvf`~UP`u*7L{)}(#KJ%8ZrKmsL{y@n2wt>nR=A-zqn$T~AyghU0-6|HbwmNPj zAgyq4CS#?t_1cX1dc1s@8HfA@a#ChXQq(L&^_Uj7Wb+4jUSNBl(io&`yDU4P53nHp zZ#Y!iH_ASUB!5HmKjaJlJhO|WGPDrsW%r#jD4}CX_vSCYR@EJ>{G*^G`4Dzh`NM(e z^n2krnXC=%uTQ9J7`g6Q!y=;8wnFt2z8##$SJWls4PM3{dAI8wPj&n6V6OCGl;``qF#hOu+&}vitwSr;Xa+y4QprT)BRy(7hrJv|1}O`sQF~_ zPeRT%_kkwbpFzXK=(fITFa2u!0}XI(lethjPoaYht+GJK?^_5|z6UZ_TYmlt09ELC)i-K-Qy@sso{4;4|ajB8#N9pc* zzQX5FXV;MWm7vtad#iBfd+T7XZ3TNdWIAIPsuEBTGasUpLx#Ak3pYGwnHAz9*EZjg zaTa!qHK;on@jH)l9qJx9Y;Kl0y798Pl2j?@C`v|re`~S{3is6y-wELR`Jt<+t%sX7 zA|HH(R;Bzh+2sH{W@NCCt4!A#@~RJ2h9eQD_0^kgnBg^aKSsk@IDX76vebLZNcVl* zF)!?$2u#fr5O*Wv_+2{R8-K*&#D z;8M6dY%Hgf%9r9}XRWJAE(*|0tB`~}2rKppRYFr^gS{FVqPMwrpeNEW!8b}{$> z&>%5og|wd5V!X;s^mTpFvHVH5f`6biR8{-Chxa-x_=DnN(9~|Qpc9gzpNJw?w8!(@ zUI;4|%0AuTG0U#Jr4ujZyniFcm%_Ry18XbHO7$1H#meC-Pp(CH)CWjM;Br~clHJGt zyA^pIK_$Jz$_Gi=#9jDi%J96sh`52-pPnb*rd?mxH0e}uB)^LiOB36H<@Rqp@(YuiD7Qj+gbKCxhZ(}&? z6Q(r*NBA;cgFKVZ60-vb=0|@TZns;|6(pRL3{PHy_|{NzB37POJ^^TDI_v4X{6q30 zKOnu0N^?T9ly<$HgsHDa_!4c8DcUpAi;BOdt=}Xj~-+G<^kKXOVYj9*JD*2Pt=+=x}&a*`KgD2 zwt)-)n*HIbQK?8@itSpB$0knqB@Q=dG05nyB%LwQ_PA;9v0?+g?5RgOcS}7J`!$nSJJ~We|=( zOO$^+B#90)wv3SNHUU#_)sOB7X~F;3)wd@i)osnSlgL$1=$0p@5ndvKEa4c-(dukm z8m0Aw46cDg$fU1-(-T=Vz^ai1f`E}dOeSeQABRVi&G7WJJl(0j=hLk|XV9g+*t~NE zxWO|3Y)GPAd6QTr=f10kTkeU=T`p51^QhpDO#FRv@DD+@+a_lsSe=_;$1N**X;#8s z0qZ+V1};snR;A8*&?6y*dPsx!`f!vE>$7B51nQ@Cy5yK=HILUyk>JqB<>nNU0|Y6t zh_oQYNmnWS3Z4XHAB5>T@mlW#ree0R8Or3_STsfYUtEMnB4@S`8Nb^2J9*;Fs_;;@ z!AI=H9);U+sZS$2K99FP&ISSeH4nzHy{%4nA#p+lV>mL-uYWcrc4-LB$+bas(Rxta z;%V?tXXbW?R#FsnwQD@GEEINbhl)BI5(o3!HV8$SJ~roIfNeIn4n;v%-`h7x$`tP8 zZ!y$qo9kt#B{=ks%8gV6>xk%DOirJinXKtnQM)>GDZpyFAUv(wj^m>%)6}L$0Z@)&AM#uc&*SwuSUGpYV(`79|p7%-RjS{&Ca=653Ft$=HozP+~ev zH&`jr1JTE6FY5<5P(F#GkrcGO-r?caHG&gfTXZnS;TmR0o)D7AbS5k6>*p7Zwbm%Z zhY#!kp8t99MTUiKtzHosJ|y34&rhsNJ~`nWTkU?1&TPGj!nK zNGCMlUE3qMJxXnp&8pM)o(T&J%o&AM@0W0b`0J=XUG-GZ+f$PziO?=>yqd^_d};}f zX^T``U71Un7WRkL0ak>i_AwPr@Yx?l9!c_R1rZ0p+?6+$BKgcymJhpDJz$qk-j-( zt^1-wu(b0_KA95E`isY!ktrF_Vzk2(1Vm$&Q6n=cpwu}gXziK4`sPfiYw%<$)1?XnJ@O%Dab@> z416j~38My&3d^9FptGV*WP|R$aC=l-5&kM@;XoZmsK7ycddCl*HeC(k{jTJBW1rnJ zAmtV*uRn-C=u)2bHI0TVIhJoZftaC8->Cx0rU4||B527EjmGh2hjk-C4QxmI<8Ay9 zkX;{yd9}9qO7tZ9Y(%Ilb(u=1Yo;hwdc3bTC4QdkyAs1mr$&-XX??g zFYY$*Fk;1YYMOVh=8A)>$oRim@a8z1naYFB)9yXv^}Z*&sFD}}Z*Mi`KE2<1%;r14 zovhI_9LUSA++pbE3Az5{0r>S(q(`EVS8g;X%L>H-8_XI;E+i?=k&2lwN+I0@d-A%} zvFRQsB4@(r(I+r|<=h|`Tx8K9oOc0rEF^CT34jjz2NVYIDRCHJcjP?s(3{>5+upkc zbA;I?GDt6ICxm5Oh5iTfq5ozZb&jfoJKfyx%AG7&EJ(CZ#vi|p8)Vg3@LT@rBiO-UZvdw-1{!%ke z(d4QGkQ-TZFHJ!RV13%RP7>B$?9>xYF}a1g`8L0ntJ9LD-%;PY)xIqOeHU!sz+-u( z&ne1>X~+%%|80`L_5c^U!fU_u(c7PV^%2@+kK7pCb*?zsdP@hUq(o2XW&kG?PL+Go zIE|T^nvV0dvWmXJ77)M{zgxA7Fsq^tv~o1}-Do}cK%-^T>*2x~XmJ|A85agLH=gt| z*0k6G@mD;z0C%(|JVM9=YJ%9L$O|1ZIOf1hJRt;Nfo8zLp=6BrP>uobZ=v88v$8_l zk>ATPSZ~wd&j0lI;N!(i&2h}l;NvNw`g~l~@yoIgpljRA%j3Mn1pvBg#LqI)FxwnZ z?>vgtF7e!*Ns-`(oX^q%S7Mhe$9`Sh-PceHZkR`8DiMEx_qzRv_UQ_+DuT&a;3qw8 zP`STqPoF}HheW5)31(5-D{)B%-?eGySGp#+@~X!l@?A56mD!VAXYh$5=6h#odN!*`TrcJKeh%88b)_nApea>TMdl zuS;CX+pj7Yzv9sS8pJh%ygWBR)j@CjAwMt_H}VJsAvOlO?a|Wgny}N#qPDA2T7_pu zdqIn;+*gsh`#u3r2Q`2GqQp3=ODqu^MMI{WU}`|23&@jcGrBzGBMsEvV_-%_C616( zO&?!9F+4m#V*b9KB&Ko(wV+NXaT^^)nT>(}dCz%|Lni2e^*7FQ6W?W_{UJL3Pwzyh zmJ?Oa_2=<(YiEpxj)@k=HY`Mp$0!_UeK87NSmouaiU$Z&JDXN*%863q_acvVqA#yU^fZgZT`Ra$jlTkJtmZ0|7%t!FzoX!#_6=Rx~6VE)z|Tqn_kMQ z8ilu)mpgG*A*G5@_F)PY$LLoxX)7%3;^#tmNTq0KWv~B@f&6ITg1uweGk1|?i(J}d zD03ux|B!H!LBg2Qr^!np{d|B{x|g=&+v;RR0(czn#Yus*&&}xF1SU;dr|@f(uQ(NK ze@zu9%pY6cFp|ubf=`BK=^2ICEGV&j8r>+kI+sSiCU%XWvepRaK#i|!^Ruo^p&wUr znEfsCFLSYljSzMa77Mv&Ao1(Q6SCn@)}|?;s&FFB=~DBi$D&B!^bd8l8>Gg%OWP z(%y&JeaG^xBw@s0@HsGF^p)Uobqm+!59eTFP=?il1<_FVr(V}NWW&03vC#93DHG5k zO;)G#r_xd{%>*wPT2|lUniSqo_p+a->|PAGm&n~BdmTxue=IS^7;M*>R~n>hJzovV zpkq_fC37Hg!cwxnWuhwR!YDE907t8oNk2G_V%Tm)G1P_Z`LX{_2)ca%f>BbLXF zN_J@cvim@^U73bk4erIvzj;!F6gLr9c|B7ucr`c6>4rVFm+zX|~GW`99fwG6q{65V;KBP*rnvMv# zG{7d^>P9`i$xZL2Z)!}6Se^NzY@{78Rnx28(ariA&iKP}cm>pw?oJHZlsjU|AGC|o z5!_yM5eNKGSarxfeXZG@WFDQg`!Q+G`Ou5WqM&zdv3##C6LiFLTTJg-EUm^Av;b>^pp3k16Wg?TQ8*?5u!%@qyC_TS+kE=xenDEr=qL^0@!5 z_lvK@_Bj8QjD5u)H7(;%@IY3J@(cVxOt$uKFq{0x(Q zN8mn&^wDoQ(JCfP;rE&Y$Xj;w_5`6@OM@9DVfh8oOdHmGRLVQnrCmX#eDR@(pn!sR zEKw;VNr{%kdaBLNCXq@Pdn6MJ4t|XM#KD9#(h$KZJNzAkSBLBPQgjBu5&|y*Y?7OM z?rCp7^x8u2pe+LlD;={wt;%ys=D94R!76OJ=4`Be!GFIPjcw zrFU#hSx}yFJmNq%;nbv`PeyU zGkh}*SQ;DxKz!zCViBeQ9}1s0J3;T2m8;_E1!fZdoEcv)g^O2b6G^x%DFeXu*nL+w zFl3eYi2lVX^eVW6GQqvgCh8lUo-+iThM=EV-?*pGOm3$j$oxsUB9i z{i-s9pqHYiVw-?-wTlg#Zv$ft{T%wzsJ&#pRqGad+%4vK_VrvpD|P(`RK#Z@zI#Jn zYa2FRb>LKk0hPs-*;KOfg)PAj!6ePK(4wO>Nd^;CuJ67%^gK5%ENgM#!t!A`?W zUT}(>C90jUgin22(9r48osu})WbgJ(M!F%YV+U*D`}mWhc+s)Ozp5bB0=D%JdO116 zy>Zr*`jS^q&zOCDVNxLxXANF_)TYI!F{T-tO(#WqVj5A%nGL3;9AXv>%Sc|&h^zbe z2=3kYqHG?kJb#Ag&IrAOCqBU{ijL&>^{KH=-}aP5VhSIa>x94U@Hk{sGTgdJadl4s zL8ZP>?vxzg5PinFF}x?Yp>sVYC~jyl@*L2!iwpf#8+*4=b204xgVC9ww{i|=w;wAY zUt{5@GmFTr1=)%DY9Z(}0gbgOAdTk2lUn6(k{tLEKyN9JDn{`Y@?t&eb<-6YeV51q zqh)uY_9lDe3UOI=rFT$M$SJ+xSV-6iZI@qR!PU-bh5ijUR{AujJW$8GbVaO@?D4t# z@7E0mQ|#B3d3j}S&D!$rHG-hx^BPf9J?csyq)IX#adzhBaefX}_xe)5kWtOND#kn_ z7Sn+EUF$L{ek?!@J6dWmIlv2~8#;KF{Xq&&)Xh@@n$zf^IEFaKCcc1Vg7bwU9bPu> zHe3trkByBOf=v$}NE+`nn$m});X{uaI{U77Z6auaQ^Il{9(9`b@l9Fjnl%KDX9P^q zef?iYTY0UV`Y+o61=-IqIV@-qrV_!WbkH<&EZ8)mFyw}>0 z$bn`pUXRlT!#`C%P?ukwo7;Ty4qJ@UD#e99o5M#^uayTGC}`ci>oIs#B=mwxC!#0w zo%lz7c$|?oEIG$R898|5{{9vts<{F2(g{!;oSxjgMT60t6Y5p_$9K-7G2*rLJMK#ZvJtkP*04boI?{YdUP@H zi!lelx~9ZaJ8xsu5T>&>P|%=tr@;KH zGCt!xmyvG8?zvsnM27dVA`C+2AyWj}qk*0pJBm-mDl4SF4ofdpB3R;6z`1HSs%HC>*%S)31~cXcH8!EqPMD>0_&%10_x#n>%X zU^Ppl7Dssv;7$4clrUnPsU%_4wCJyBT|>84p^gn~Kn3zAm}nSt6n?i74@r4+M%}~P z1q?gDLYNwrmb@W(vpXA$3N9ckMzeFJR!t>GU1sEWggHLa{0+AVHbT+syy%9jr%Mr6 z_y<0!^7Ia?i5ACNuh}^Pgroe3@JeKMh(B|3N>A5iD)%DsdTe5)cNLQpq}BLv>)Ex{ zD6+fP=iOR2G7B6GhKuMis@pyn#Ax-=0^eCwgZz>eewxh>`@rfe3Oh#|xc#QENc`vz zZ%iWNTi<@@4$uo|^?c_b0$(^Vve80Kz?iG)cWr^w(vi_}<=Gbr%GzWVf9;PSS*uPt zmN>hAjj9Qvj>&4ix$|UI?^HI|1s&4rbdA>&#HrC}pmRxW2p8aCD%0jh05#f!7y+(m zr2IWRU-)0P{aCTk&Q~X~dt+=(duF~H6bN_9t zoxV(OfoIk0*GpGwNwU}u`hlo{{zk$`j`q#%b_re>Yq*|H*7e&R8VjW6RXWYzq!pn<#RSx5#1D+U6Oi zWb$_drIF=`VpC`)E6O8vy{A*8GezIQ(l(Wfc`ui~FAwF&&q+0H59Kq|US`euZ)r1c zwEPjyto>VQV3>TbQsNGQEW258;SWV+do_!7zP{i0a_XPfbbbXZMHh3DdU1u+42CxD z=t69XEy5E){Y~)N6G9L~x3}9G>Xg$TpN}4oo7HS+yMiLy)U@|x< zW^10=aa3(zWxCH9r!IB;0x4$J|w+&1sKSGKv=Q-%uD0l@-FeNXP`TTPesD|Ac{k zHmYIv+j~SXjv|)Bo~7iyLs88U=C$f;J8q)m(76H>KcTbJW5|0f!$CAxZa$1x#2zq9 ze)ZZ4#K~{Mqt26b{Tt_&RuuReKTBQEk6?^yiYm(5cCsZ0_6P4%R{SJ*()*b4}xkS#gXhCcN zcv(Zo@FU>584lwhh{#g0uW$O}MHVESU_OF`!IfQe7gLo3b9EU5l}S7BsRBz207fTp zS@mlN2zB+UQ_RIODM4ajA8oJbUMpUjWL> zhge(kV68?egtFDLesIz0ttn512}Now+*whoH<%b#z?7ef16!r^QQT_J)w1I?-eMOf zQG0>}5-`L}LKFhV6QLr_X_b5=8rCn)%Dz(3a|xwc)E1{>5n6+6&+UB}?NwyL=ZVfq z;5PLqwG4^OIfalIp(;Gss|krP(>u2sD-_f0gsb(lS-*v9#-zUTXbLhsA5^4mIK_>v zh7rWKuuk*p#Fu=RDFJvK`ni6^>%RG4c*G*eBn3^P71SI?D(sH^j72B#P8r_&72z8l z8B_nX^wwrb*>w10$c4 zT@MgFX^&=~tS?dYbWCg@)q!VJNG?sw)YKu+EnEo7!cfw z0&7x1oZV#0Sdy2bktKJ#MtIpsa4A?nr#m5h(Qs?l)Xt#jwd;?OnG^Zs+V1qM{VvB6&EA#Crll4DH>>7(H3hC%) zA)0M-IJ2sp2`7nW{n80sK^L|4T%IGk~nRH9If| zp?*8j#cOP8sxi`@UXPoV|&TJRCezR#=IN zSDRaN6}Pm(K7pYZ@%XLDl^1kWc-m~ss!^RZZ5vXz>A1(^o?2`5-s6JBv50c z#Ah}LFMkLaHKATMUF0^=tm?S&iOH^Wgyf*eS&}RiOEOy5h2h!(0noI!yp(#I3r<5t z3(MI1uQ@~Yp7f6Co>s)%nG&T5iZO|I*d?&C2GX>Fl2_>Z7^F(}_qx}tk0nq(ONN#y z+f3-(q+duMiE!qkT53#FwkwOWIh{+%YwV{n+915vW_KYSkP&uIUON-F1jFV~NN8?-Spl{Lq`rAo-A$ZBlhS)zZQQ{+Ju)J2|14QMc0LN6nClI?%X%40 zpN{7k?>1~H5Z)XiciO~)a+c_R+BSHlaMBmH3s$$BF`Uitkp5NV_sjhO^E7jQyF9f& z?Bsn;q%IzK&`&rEt+U#SDM;2+FYcZaD@kFZ7am7?EK1Adxvlx+EiAg1Z=W?im)5Kb zhG%RvsQAKl!$h=_PV`;a7onfWG2e|jawB+FxU;&u@{s0WYk>rduJ}<%(&Unyt41Kl zYZLQ)p>&K3bjuEyUH1)m&RWJrGc6b(7rX?9cxfI!3x zY4hL9Qh0eY`4+V$^(EhzEv-1Z@`&?ByWQA{SVnAR=#KMnZn7|luo4p9T>T(JvhAYe z&oEEJxMZtVqcO3oCS9D_%psMz(2<%fTf;RMJ z;Sw960a%(Hb<_^xEUTw@)!;#zeg@$U>LXAvUq44MmwlWTo!~m%&H1j+q`K8y14yK0 z3q;pk-emJfYL*k+!B>MT=qq~%Dty)H#k8>uJ~saaGV4rw@2NG)P5Kb?#%O=}QnR<) zSkD3rP&;=F*_IzXAxJLj6&T>|uvfGHIe_xu%_PaW^XlFQ(}&zkScgSEl_ zf-3#7;ODL&FfSIpNf{-})TI|rJsf^dgUjnGCk1a90cb|~T z0*arD@4tX`Ie{gR87IQP%G1Bt9u>;TxB;Km7ykdyCw_P8qy0 zPc#GFwIE}Rr&4UDll`{P<1EOFK@(rI0X5T)3`He)6r;wchQ!d4nCe zXIMinZd3~!VBMwbl6^&4Fn4g52+}!e~JG^f-Ug6B4PRf z=6S3tSM@fI7l?u+R>M9KF&$F5`0?)*9)?Js6&KXt64aq8xH0il17i4b2$a!3+Ncfo znVpR%+!m)wkQ~LR_c28|9JQVG=&)e5t-Un*lF5&vz4+9e6#69a)6l6S_b~NbR-26C zlc8K;9>sHSJxD5T-NFs(5+uu{{};I)K|E$qo+;ky&vZIH0+M=451F$mJzTM;So zOUH+%nX-hYNX)y@BsV*T5)p5&`00!+w9!Lpw6BZ_yQ4(luZ+}S5F_m;O}H^Sbyh1> z*S_8SP~G#(Fxas{79Rs8CmsyT9fj+J1s>D}3hI-gCCz`~!ubk~Fme-*{E%6QqYKP1_1(k$N<4i!OI8&z*UfZF#;Ylmf zt);&dQ6L}eHjjd~dF#5Zo?E>ESOR*T3`>L_4IZHF$=`m6bx~qVA+~9j-JcK?n0xs4 zR?4D?y!H(jh_^2O`E#WI^akn-Gd<|UwSnzJi%9)md06w=4^8Wx4`NCdlyhpV+k2A6 z0}q~`_l?3uso~fB9%g7L@kpcp(CGAh;5dajgu>Ko8HSoDk)RkRbFpz|U~YNuGRBaQ zloKbZ# z1Bn7mtgktoh+^Zes{~{eF?@vF7IC2Z`|%_p=kG%q`F-!!~I9!n|mDf(g)CEsz(%gJk4Z@SS_cR%3HxAiG+n zQoK>cn81^(OXgp(`AI7{M~(kYts%mm*7SWt752(`31vmQWZV;96bBy}*@?yE2@cdh ztEjdosXj4xE`(L&iWhKf$Mvo1|Q3n7Qw>S=xLTx(-G-S?O=y7&ychHvpfA_dgAzo!%4*mVVV zfuVYz7Ebpz9PJ79&ptX9k)%L8AJ*z>s{H8R{FRIqZ=}r=ZX21S(5^2u6(mS`a9L|+ z-dno!-L`77yyj9JJ>3X~=b?*shGhrpD%&tg`WoEl6jxW-7l_68eZR*aZ0YXV&8$aj zH|&h%`*)%YD#QZk6XZV;QNDb{?qnC;IoCdW-h3Vez~Sj!S#--#60#*J6NOea90(f# z#FhViRJ4eIy-e<7iVU8b_cDLm%SR+@7}R&On;~%1s)Fg`lcl1<ELGabK+k;zcna|v&9;%+U1Tg|UXad-V7i*fwrp$7>4edp-8J#eR) zL=Rqn^EQrQ4wG_ezI%WpxOaRjQ2!^(1thrTVvskz7 zX#4Q7=D{6?Ij;=K*Z7omMzG})k}Cma;(CcDU3J!^2#WT++WV0S9OOPg=oh~`J~Tda zWj&i`4VcH$1ry=YGGYZYZ+gJ@QROM_IoD`P-m@Pid~AJ-{vJoAxH3>mE8XQQ8LP(C<{Z$Yd_qZ zozL)ZcIwYcHHMBG4Ui|8b<%M4E)?o)aQiE`t_A6%f1sPa0CugjJ5i;Gsc-l55H z9x^@qfS|5jqk;SQEN1ey=xS+@l<2QfZ5cX%QEsfhfq;-9hJQ=a<2*)h-<|z@a_CMd za>`|BjPLYAw|$GqGR_^*=bMm|H|Xo~FCzJv)0UUyyD0F{8WI$IKC%LY-@iwT&R9H%PUVGG%K619y+Mt%#@DG`Oz5D8{DqdGL(SR_}ktq0(y4)ZfI(rNJx;D2Mw zca|qIK5WmR9S(enAKkmwG1&D{rgjV?8X2!)-AZ1n&2n;5y?)KB&^9)8%G}rsH+<8* zc>a^k7s3`f!s6!K+D&@7W%xo)7wgnc;0j0~yQ<8Q6f$EOBY=Wh6q-kSSl41WJnhij z0i%dYpC2!wKJT}pO^)Oyu`{mc_&4H|^fC)dK-%!@pdXafgZq{J-rZ&YJ>TbR|cPx4qf@ z&Jzb=bW*=&5?}WgakiV}7$Ro`0^=EPJ)hr0-!IFB=FrC*?=r-AaKR_r&A07;x11Z6 z;cx{h!dx%h9){S<8OrZJESfm4!Zr$s&*~hfz5NizmScpfGO2et@Cw<6g+F$;j(2V; zDpVviKQ(%TLW72J$T7zWYLF1eecKkeQTvZ~RQ=utuk61wYYVrMg-1#!LOg!?KW@msOxz-vg#0nUMVjZX!v2QW~>G<{S>qOw=m!C{0 z^2wjj5g>&DLL+XD(HCE)Ry=4cH7XvaE|jWXSGCf=^ifX`9HME>K zi3EUO_LuG3IUSh@#W%1a2E>f#mNn*59H_Jg46?|DQv-eIu-o5F*sZTilV-I|E5c1Mnaz)8j3OeL#uNvI;+Akx`G7Z+Nk=a>!FAcre^3 zEd@G|+ew&$h`Z7mY7AWoxXXg~h75n6OJs16Er>bydOTz@i~is%(ZgL94n5M2HO_b= zPGZhSJ%^yU{<*L#ez_2y;;!gMGEFPa1IJFEMbCG@yBo^1>A^sm4)U}UCb1rU1i6b@ z{Fz__MJvWcm)K=|7DDIVsY4mQ+9efH3?#Ie0RC*+D(w0iwvH;PC zM@~+6ozgdk2QF@&8fxGQUM)U098{yv={`u4bd9-mf3rM^1*=0k*#3!cDmqk3Cc?*F-Do%}EV9`-gg`kr(-9xd3Ua1iaT(0!=QYy7 zLR?soxt4CufxX8G*x=vg1glUGj|^C{*KZf3*m$jxZG4F?PVQFq3asEAl5>kH7662& zej=Q#Qgk8C@9It}?s

ZBB$?zB?$Ic)anE1oSoth&`;+axFmRh@)N{sy#={)gdGq z8>~#ootT*mT)1^^oGnPGu;LsUI?!R6V=#GxZDq(i+K+E;1ycr@%BM5YWI-H;gh0M( zbapa8iv#2M>l)Rxw_PO)A`vpQ1#X@}0xT7m$?7i*W41^CcHY__D0hZYPzx)N&$_sS zGCMHaH?>Ynjl2<1buILX%$v~(-Me%>v{0klipgtqWhWUNwT!P$Htce(5SC@f6Ce!K z_#sgQFq@Zq>bIFKZOuFT=5)6IBPo&!gLvQcxU=pdQnGi-w{Lqjbt1nGUH7*SW%C=PVRX2_-q7mp*tMxTlFs zuL=|W9{Tqi{X`*LQl45S8w6=7OSWa(xiprJ?TVdQ12QhiHS=RNKu@Ko+N(G$0NTH!?P_*#yZa$~3 zcQK2S*-j*?Uh-_fMQr!qbn-| zsfLsC?bRh{wwuBpL))mOwaASgI+!{XyBj#J63&$FHIUlOhHMvqI?Cz>Mt)^F#@__Z zLx7{@nnoKX?G=sen`BptLI{Veio0LP{tH{W zrS1(pnwe72Le3VU=0Yv7>Hc+>uC`oMp#V~_a_Y3BF@0}PyfT};i+C`J<(De4%9q?O#S(w{^LY?BJXX6}ecW;qXPbT)=GavR(aRbqX zppGL3LgSu*<+s#O93AWUg$+fYnVX)+D4C;s@RS~ic! z^qxtoN*{8t4{Iv>56|22E3Cx8gstxf*RZ!qGj~ELj4W{ z@1$=F!Mm5RhsUIPB-(cj_AO1fwBOD6F5n^Fq$R7;VSmtx?SwyZYti*32u%@gV+?Fz^8Pv^VKf$Iq! z`o%3OqvZ z920iTY3g=7lykSlh^Wqm+`b_R22xLJuACeiy z3$*Hk6`zN49rntQbh77a_w(6`nfF%_q)-_c9)3F7=;q7x4jWU{c>dalg3Zp$kNktjAZac-}TFBd*PJ7^r8%@J>rxo@%SZE8VlAJ zd2D|}3SG;|mWBl9n+06l;lF^`v{&oC?%J(5*Q4eS-_scZZ(6VFW>s!zTjcoc;6D^f#PA1r@Hl4@eWxGh;b{{ zfT8+13U74DzIq?+$ibZU$i)(O2OGyy3d-x?ibdHPl=XL<|hg;;e=~_JnUDimH(F6)1)47by z9%)kh+GEtmYoAENX9}@dJ{ulnWsG=f7dW_Vy)C#?iXvZaBj(m-aBfowX<;0y0|}U} zW)0eL$*eD$#A~Rt0ea_>Fu)*$J65%CeE@R>0*4CpzoD{0X)6C%1_)yd6gI0QG-E@6 zhneD0S5duF0y;jNM0U+v$6beye~>`ylD})hR!()Doaeg`fnn;573z^+bON}hVoN#a z#;QS_rx5Q~t%GV?L=5z%ZtHha3}rK8h->MeG0S6LI3LCj3n$UNB7_VjLBc}m3Gq|W zXI-r!zz7YJV5Nagaz^X~^^0*dydhJ))3sF2OM@_q|nW*Yowm zN|E_$Yy1c$G`;Iw8vt=q4c84s-OxZsxcy=QctQA%bo=_o-wyuUs3#9F_XXeI>d8*J z{$EuFeV6(ro`Y}25;TmP(*r{IciZhHG3FbK?Zutr_CX}STZ(60YUXYau$k)6SI=%m zx7g!Ij+^S1k59B>$0lv9m(LqBpENIjw;Z=VZfC-Ltb{}6d?ZUmO&q@FmVQ93U zI_3w4EnwCOU?)CT4xz;mjV@hMI-m)VJ6yDE(nxfj-oVYUwkCWt;dYsvl;vnW*S+4V zZ`o+K@f3y?o@wyJoL8maBG$%}z8Bio%(?XhrD1G_B%w`djXX6WA;!2s0f41!93BE> z>5Q#K!n1-a1G@h^xR{vlN5&wc`ihh+n8rGdeo7K8ypbTPAM{_b1>M6`y|-_C$gA^f zIA(9_{bL0y`?Rk`TUW(*PA!qR?@89)=~w0ULiM}JegGe5V*lT8CB1~;h}?gdgvbpb3uV;nA2~9%vw=^ zJh52k0E19SK}RzFYITaPwz&w)58+YIN{u+rxYoI0X<$`KGz|ThYPfi38so>r?7{s& z*vx3#7JcNO-l`Vjquz;M>At9xm87Qegew+%T5^5deWMr1tr^=&fEkaM zf)iUdlaTzOCMD8DE?sbNP0|o;4fI6}*AQz<*?GVgvLX;54?*6KWyNO6&K+PUeto>s zsOJkK|Ka1zzqS7}0Bir0Gq1@qfeFQs*l2hjHtVp&s;U(&=+oYmQGIK!BQCvAMbw#7 z>W(qmKY)v4k@~-h*)5?;q0l6uUPQTPHkr7wA8Jj+L}_{Kl{aDNS4tG$kwmPmb0F4V zkpVcUSuJw`k29vAIwhS~?3~rs$$Vbk1`vSnu~_*s!2%jw=9X_qDP=WG)4+FXGoBuw z8?prv?*f3}`HRrzW1fH$#vw2NnqB9BI)zVlwSlI+Yd(c>-g^C)f!{&!hWZzH5(q9c zi`oprx#*kO1(*!Y)zzx^iGxA8TmClre)88`uu})hk~QF=bP?NSos#P66UdBW0`mLh zNkZb;^0)YeR49_{dWHK?{|V!%G1={`Ig>XCf~qDne~xKnT%EgnV6ENU~mt6jSo zBI9Nx%`JAz$S?H4Iv*jBcCIQheo%DKMRUEm>pvmFMuJOxAnX%aH^C*;nIM9YXO>>% zl=jrTn<{W=ggfXDIA%c^lQ2V_A&}8Iq=6PB9#^AkS`W0bHP#UBD?{T=ZR#Y0o@NMEfEx%JogE9Zx zA%>kMX-Ut1=8)ze6;f#zdSBZre*{=j1C1)S(G^R1?&ehFs_f1L@T_k*@Oth(NrdsB6FWXEB8dj>C-TuW=R25$Rom{ zQUd!;V-)VGUGr&p^zcsyU(7m_qm75|bkSbpPf-R@=6Wsx@q(1=v#w^PsK7h{Q9BL5 zM?;cc-Q}=rl${9Pf3vhP$Np3Y17e{4nWQ$M61+7UTDW8oeD8#8L}OPUzcfp2p>3rp z?N1z{+zFk!e}?3ZZ2+v-X5poxbB7rQVDOKkorVuUi=0`+LkRaXF%8Qo?zqVjnH}~D zj&vw~&HRz=a`NznT63e+oH0)!g*+uC8NTHoh<3c_k>Hrz*rLN+SpKgdaeDb%bL~<| za)Cv&KHTSlRej|yGx(^Nvf6q_JK%%v{8y}bMmeZG6c1Y$E_A1sBXR`p}^Ay5@i zs2n{|p}P>P*x}$&A{@nGX_lN4-e?bzfuu%qAM%kUU+zS=E?044UO&@EJZQc1BFSQH zO;-4g%S)ypVc!Q`d%(t;I)j(H?KcxI*xrA>^6(|q@Gm9VPUE&Y4%4F7q2EC`O3=ZR)CB|b^a#{f=Ax^0L>+{aHoWRgg zkjUX)fugAufU#bC*FV4up)GBFoB{IHC z#Au+T)Oa)ixZ6C+F6G9Le3{b(>}#d(b1k1ohtu+o!9^nX!3cF>r;2)MyfL_qR& zWz-x?WN^}Tb2IA)X}yLb`n|r$Rz@ph#H`X2$3gCTOS8cEkCbn}b4(LXMLqDYG{t_bxAalb_|>NkHj+^~|gp<^iBvuS%3bz(h1#ZJe>;>W~ay!W3nzS^DoVCpJbgNMsSbglIl@p^9- zu=E#$uI6;?&iCWf`~6FY;H(z4{iE}N#u;FQJ+L4ad|d7l3jxJrE8Vncx*o-7W-`qK ztXs#@d4kQrj%}o>y zu_3|ky4<*m1#}KzTHVZvyCJVL6Y-&|I^gOh-_JyGJ8q$^EWDVjaed-`Wp2rDdQ6K5 znbg}!HK4%N1+2qZ@sHojLWS1thp@A@KhR|GhV~o>BIOY%{~vPJ(u}Tu{sXsx+q?Ji z)9-ZqJYDO}7Iw|s42*zkRw`sUPu64rG*m>l0&n^#1)M44BRM+l(G4W4NtHtko2R5&n*B# ze=4@DicqYS5CEX=I;vQ#R*bCOX;;`Lm+y|5|bd~AI?Tb5Xq z6KisA)X^}Xi<$JGt07*P;mt6j5UJjC#Yg!t&68^MBnkoItIt1T@=p%%bFz#lKkFd5 zP<_T#?QsQmjxCzY@d>w;2no?pd}%`s83)e?Lg9ZB#DRVSJ7&e{I<2|*sinKWtWgND zTJ=_yR!G)Ir}ulQD_koV%p^1VynXlJ6`d!Bc9*wBqtgR`PZ*)oHl8IGNIFbf;876w5F*x0VFc%Q2md5#_XD88I}wlmsp6Js?oE^MfA zqiJvp0k`HvXQB_;L_^}U)@KMAXh@6%S)I-1pEwnBl_;>v_~GC2X#+oR5UH5{mfX z1!H!}rLxlYcw8c4!J8CrZ;2XU-drOB<{!Ru^O(MS(I*&3s9T!!$`+xj7=;QNe#3wT zCzdgTY!r&g@i{mn9;M(kuC({EXNhvo*hPn|M`zRx5Sn*%D+vwdPB%kJC&`OogA>m^ zV2FdB$it?c&8!wtHJ=$^Chd;VwpqJ4%yGa=FCX)DFn+~dCVxeAm%E7D#>lCr1_X}D zPJhrTWQ(fo1$*TMjeF{Hc(Fk7Ad^N($_WpQ-wEkb@*tqoND@o2SFb5tOdPF8zkQ=` zemKdXNd~p&W*_wBYe)?&w-S^!vbaKGYJil5j&#R0%_pY1hw5)r5`d;E_rd?Wzc3OWY8IlH3vyIYyi!_O`hvcOJy2#X?9#s!L9uDC$S4>Di&11iaqJ zgMZ}tM2?FeVg8|a`X}@l^=d&b{lmIW5peqd#d~RqUFpPE{uB19v8+?j{Y6?>>JK5` zF*Hz^Bo4uhF)(2rU(xS48X=w@3petHzp9A79ZgYKviZ8fkcg!?9+I}Xy&j-8DGajJ zZJ6G6)z6OkaB;tewbw5##1aqB%zM2x`yA#MhX(xSH6m9Yi`5f83y*SlG;(u_s!b^j z6^c^&J3}2Qz?PxK&S$*H#(at{T;wt9-ccNG`$XcCx>n&RE*rP_xw$HemWQHc&L7LW zMrqE)<1jz2=eae=_$#<_|0eFc79!0;{TCo!W}wos_;8sM9x_QFc`4dl66L15Hj=38!iU0npu($ zd0%Qz9<5`e@lcvkVuXv}2JZ1?a)(ioU|yK6lQhbJYGg>ds%V$8f!3!|jq8)-tUoxHfs-wZ!KBXj{&?BC4J=7A9im0pAm zz13BdX&`xu%&2v6H813#Rs97Ad0GDJ=vXRo1*}f9U_i3hSK%6Z<2D>?r0NpeVzk31)-NETG}RySgxK?) zPJuBV9W;%rIU0tf97?JgTkPJQfM%02O@1%;S%*+IE6oXd!y?}4&Zq@n!-!z$%Vv&8KzkIgq3Ke@IBNtqHvOYKzPGXWMpkoJcqyvEKd5Gu_o3x$fdf!dgrZRKY>9cKTWX;WBZk|_^dWE}Fk>Vn-oC7xwLA}V0Hf{}2aE$j!bESt?p ziwts1zRA`IZv(vLY(T#dua~LlHWL33J|QhagOnt}yzA2@d)Asp$wZ8~N4VMtp4 zS}Q|6|7kk9a=FBT4eyy4`std7?)}PYEuiAZx|u^}9Bej#^3GY9st*INH2l=oy|EnNJ8h!G(R`pfB>YdIn*udZvZ>Qt`KyyEeFnUF z<7kb_)ae-fJ}N=Gfb3W`2xzHAmO!}!*-r(go3`+oq`sOoxI9;jARPR(Fy*tVJCI3I z%er{!hky>>%uX33HA2iweI*xhV!8EPib6Ib0EJDC-(Ue%k9w|LS6o<_Hn z2bW?F*9C?bfK_V^vv72x^D9dAtoOx+Vt*f=oi>NnRtD5BPd%?T!d8FvBZ`bERLZr=V9P8$Oc zxN8`q2g1-*-df$Zxp(cOl{`xKUUK;9)OC@LgM=llN; z{^e0&kbmYmKS>7RRbPLi&bRk`!Qs@Q+$}cZFA{As4D(&neKV~01>f72!#Q?43>@3H z1FZED38~bcTyRV3ojLa7s#9)WN34q{{Cp0&`d>GYlp*5z77FdBrAC*(uGZBW^gplp zMI@5xvOHqM1FBZfvs}Byg42FhG#MB%WUPtgixKiy>`M@#xm}4NCK@57-A8%HdAx4YcM`qEWdATDn8AI!3KHxOZpZ? zrvYi5Crgh@TSGTjE3{Wm47P^jwkfjxbtkOenf;Szv?(TW&+13)?%lG(%f$0#=~-o5 zLWBF!joNSU3g!;Lf8SQwGbNIY>7vNJPc`!%52B8nw?$FADv0vGy+tYgK(}2O*ThPJ zYMNk#z;B&0#9ibY?NWQX$#g8uHWAjMkO#e2`{nnhraZ1WLlBDVFDlI+ zoHBIk$EnOKm-cpmFiesQ8>LKJDoTN}fGIogVO#ZhFHb>=!Af*E_=XBSkgn zDi*xjlzFo66x!qR<~-aWlNVrmTcox2jW0MZX^{FrE#MViKN!5;FCfzc4xE+7nQiw2&4 z;zVm29Q)iT9^6-~)?~{%h;aYL!i{n@hczPH#V-=pdHHwZ83WMuBgc&G-6c-wehP@q zW3aENkzlWMHO8ktKSF=r0)*WB3{s&QvoCqjV$KIxibGBtj((aD6L`{qR8)y6mJx1ZZI z&Unh`bkGYSKEXDNPpDp_G^Azm9rhALP50m!G{ZKCQLp&%T4fmy6HV{VLRT3&R13;r zRU@%%h~;57!!Yfb{vS(XW__8E5WnMn-KViaq6@fNAX81gHrEe_znLJNJb(#hJP|0&zXA%RK zbhc?`@iE(vgWY|OLEW|o#nu@wN0W_+IZf*)48A6=euTKI`_n$P7f-GMD;okQ{DOu^ z&xbBXh=(m+A?%jmzMJAg(wgco>nQvoxL;eCUD7HZ+?V0fiDJ4ikho^GRmQPFPqjh) z<6&*7xh||YxAa4lHp*v(e?X)I_$^|XhTQbxk2YdhE6!WFR>uI$yMU%`VFo$CUQ$9K za^@};dD4=ohGQ!eL%(A(FY-U#OG2U0oIG>0)yrH#M1ra=S?Y9DjUXz^ktMlu@4$blPIcct>RZ8|4AYJ@ zq|KLNTGT>(&NZtz4G^pTg71^mpG6xHf^BOBM#IJLbK%+%+WV(PRd@n)+~x<>KpbGQ zy;%BYaO?gZ5-Nhdg;x`q0VQc<>RYya;b}zoy`>%J+^-T^ROVU_0A0_qDKK_&%veJRv6KP zUOg^a>Jh~x+HGsi1QgO#=y6659D!3-X{ooSky{_n-aulO+zHV2{2bCE8`aw(kU{nZ zDuX03)MnKVjBg=6WL>FwQIkXj7wlgc%pn??R5>#SvnHp30Nd;V*t?skPsXrG0lBu3 zLs|yuucPxb)`lm^?Guh_u1dlPQ_;^2jEgX8dgIyI*3)MvOfXnCVz$1F^Tvxxho1em z%o;yU*+}vsx?6LplCba5Y~|1@CxkMv1)R1wbS1(_X#z*^=j`mF%Symjia|c7<8Trf z3?(<|1D#H@Y^fn-NfU>B_mH1|`h$^Hl&uGsj7+NM@}tSYCc4Se&IffFK$iXqksSW5 z>8WVwn!7npF+D(pIgdB~@*r1-wJ4@Uy5Ir%GmJx0HvT0M>I?GGo#w=zuDyT1zMc(q z9-QafLfk#QZFXnV?d&QDVmXk7>3GBv_GNG{0|By=q!F_DPx9E*>oUv4s;ZzPKLtST z6}PQA>SKPQucUtQtZddF!5Ul=R^0fMH>5;kCN^9=NIH_r+@KJhKfh~hE%UwEfy%`o zvy}?9NB<}eVsSI@&H+0_;>PL(%=Q!qxy#!M~j zW!F6!c(RE+&CDj>xQ#zZTAAb9m^v}jeOIa2a&^-StCVsGWfq+!d`Obm{DGStZq5pw>4^Av}AP2b=KKNXeTJ*UQaE0_U7rFuUaWKkjFJs-j&5I6W%mGHge?yA{m(^x`>e!#@1f{rHa+*|yM-ttrnBh_dgnXJs z4xW&(OVITE&8LlZQ?hNSG~t)Y>VvyO=6+iYQ`ZYXmQsrxw1=;kpZVkN`yhz?H2oS0 zNsu(zdg@Eqy&Boch9ROqD;4Wb@Ol z;i!Yd+(@HjPihP-^xNAo0B;w)DqbOMeWL;8O}u29OCNj?N?Fa8B-09ir~BWCc4Dk} z`;L8+XUd`_aOd=>(b2N#{d|&Cb4S{<^)*)@*`o%Ahl~?nL(+>u0DrO7x z`{>Qj=HU!9##a0Sj=Hq1bCq|Mn2&p3$tA2-*c5Ox^vX?AtlilgMD>{jUxhsj zWP+jn)v1KfIiQuQvQ@C>1Yj874GKTwDMM>NLbY@*2oEc&3c22XAyw+jgEc?6Sa4k! zyiM+f#FA-W9y{Y#09zR&)b*)`x^ERfYCenk^u6quF)w zMF;uk(-7Ez&|fhxJm?^md)?_U5w$n)3gY>Az)TRF9>*@tIFkNy#Arik5AWwkV(qo@ z(;@zCIjJ)(v%;d9O7xudMnQfL|7cOoY1%Y0OAp#vKfaz#r#)Yzew*pj10;4i3*kqS|fI>BdbCyRck%H-}Yf36IdkjJ{Dzu z$=aJv*Piq+!eEnkh)L>n9^)PCrlLi*KjqFHSQI}0Cgl_ilaQSn2I{Nu-rz8swSDXW0;Zn99V-*g3;Cn;w%{i|@sQrtxNqUDAMjr8 zx;W3q{wZxxs}(FXoZtHw~(?}z@+4LN=Pb;nsxovpUsQL$4cut|4^v80(Jf#C*V z${+WT&UbDru%=@p!%fCE+`dmJ0%5M{Po2lWY!oqhT<|d)n@V*AxcTZE4e1y{U?tB1u_S)+p?iMMsay+~!ZK2EheGffhgX zBxskz$TllKQZ`?t;=7X#l9OaynJZ)Qkhj{e_*7Xb9L>J#c}Oo=dD}g^$C&AeUwdjH zXFeGLyo(7R+5Cgsv85>4_W<^h=@*3KRWzxADLxUAD&9M>21wS)%S8WG2duUcuoK2! z1Q@kUZwiz3!Z9r^Qr=}QIIrQ#T5p!TCucBZSMZZ@99?h{UXF$6J4Y|`@WnN-L>->}2 zC&kDiNR~`7>hu*nsO_EPjRY|BdLYkadJukM$?L!IMPT2#R3-+ChcgRYsXW|IUXEP@ z!pb*F<%|pRfT_)gkmRiygCiXFHu(LF-g+o+b_F+YG)NhONTT%Q7E`#VN&T(uT&MW6 zxfQ6gjTIXk_XeS<*68tp((3ZCN$&7I#65>sQAWbNh44=;vazKI= z+#7VQ;?Fz;RNtoK4zm(O*qcZH7VwJ6-{4Ni_WRQOQlA!zr|B@snMQ}M*rgZYNm%GJ z9iMe1X0-f)bsfO%TTf(A-d+{+$^c#LzF+5aysTy|XWIb_C&t*hhIw1{P=oJD(&LYp zbJW%`VW9v^bwWi>%<0eHR|Ve%PVj5*QQe%fl=AJzoBs?`_<7W=!ENO;ip;V5hqVz^jQe+Sbsb5Luj;*Ijx8oJ83cq0{rx1kHm9igP6LpS#L~ z$T(Z#D3yt&N2S|n>EG*4cLlY8EbBX!N^v`O?)K+G7C#~`P$K8mVL`U!Zx&PU+pxtq zoyQ9^_;BqN0f{H4b>>i*6T-~S|LpqLC-(Q*PhhAPFiiH?Ol0hm#8G>lCy>{oSXNTU zAH^T-3>Pp%kL`+OKI>W91um3f3nDk%4WnQ^x`%%*bkQ6!d~O@C_U`|4WJ1L%X-{AS zgVT|FBH{me@V)t}6=Od38T&-@@nz+n`I(U+)*ogaiX7yO)W$oAj0D+qJdY7&5F3lH z5AKOLalW&mfsa2cm!M=P&PCxoPxtl2D9rKD;#-*W#|;eb-oquH)4;9teSn~6#2}bq zKv-JOVUxTLL;%(*LZ&HqQl@LSu{Q~9%G@R(O@*s28Vau285Le}u_mCzx|xx!@eX_H z2e{v}HHqulK5t3&?W+iFE&t^*i7_X=B~FJF^GWn`KXq!d?|J-0jrf!t9ZlT8JH`9( zNzkB}HVf)Uw;m~tBL2e|Qr0QKL$w6BH5SFJ(YRU2`e^3Xm3?UtY)<`l2#-UK!-MnE zg9pR#Mawh^vFyS;Anrz;EIA~PYJ1+yWgA0l@o#G9Cm7A*LDz&dPLI#Xxk@AzgZ*wo zWTW1c{6vll9I&9)+P}@nH19kS1VA{Pk@^ow6{Pzz{kP9udG?UeSg(s%g997z{=mT> z%3~dEp2-@d@RmOpQ# zo$A5G`nE{QOZS@UyA<{Ha5^YVm!(x7b=xmR%NivxZcqmEP-SWEG1*8plL+Oruv_o2 z^iP_UxV`g;JSKySAc>hB+c10-s{`NEMEw}iJ_g9F)$ox}!j1_eh7H`f|rqxn?>ZLl8oF9i6T+0bZ!gS2DbI86^eZK#u3QWMbMay7Ng_FXPk2;ic<+wJ zAk8<^mARUC-{s-)XsBt=wXl)26p8-hDB!zndh5Wd#pM3s=Ri?@X{GH)e;<-mv^N1o zPlbWam4shn+k@q@&7ljvbmVPb-F*Gzt~nPF=zT*`LS_SEte|pjXhf6Mww(LpOSyIH zq31-4ZVbVtk)xgCgb4(Xvyc?bH-g?OEJGQU0@$LE5rcpJ(y1lWF<@}S6zaxwl7ga_| z3Ujj+e{#w~*wU@kIR#_cK9NHnrxd;bXTF@UTlW0E*i@a?XwN$sH)m5{r1#%&GrPA^ zaa&Rnzp8tP&&}h}*4=xo=a8-afp|}mC9C!<4OWk1&*e=8%s;>jxlm)U7^e{^lm8LD zn=rEgFLX%+v~P+Kc4pF1sNlI9fWeT7zrLB!4TdS>tE#HSSSOs-P%7Q&4+V2Djr1Rt zgm{}$hAH?XYXQJKX{l13ut1G}GRxT|HEM|8 zXOvnaxWcYq1KqIXcVTdSQ3|qN5rg(EY%?_ecW3;@&BxtvXoKE|3y1|_vA*5J*~)}9 zG~!EjJseLKvy{l@!8+|R2TYJ_Pwag%CNj;nm)^!noz3p-wXR)*K>pX>+3iqc#sr9G zdR)8iitOo*TemONQ5c~-j@^5q*A77RE*eqSK=fu>hpdo^#7E+M0)Hw|HVW-A=kRzbT@L%JLasy%JJ z$sB(jKGH2B2$UmEwTV!%D345!xsTF&-MYF zf%?Xp)Kt9>JiVscZO@`ffM_suk%8~>uO66x)_|@UC^b!G!;+{f8$?jhfhdy+J-=tL zEtkwaaE6}8=M~~J;WwLa=3IK30*p6~%PXoGMx=h2%tSI_bWnh5`iRSJT9)EV~J6 zQ5Lc|R9bd&c02}Z3ctBW1!T^nH#4TOn8=yHLSbp{1r30!wbrbq5m`T$@naAmMpt3( zx(XqdrAemQHjERe8)HMzu68S^q#S*L&*^J`^A47VRkdr}35k5Rdh6#;5uoR+QgEBR z%NTZ;&Ecg?U|RhKPcz}YZORm{kYqB7lp;g^8f+{}#HK6_r~ZBT4w+|it9N5*F5F}f zF`y=aOlU5&Np`WI&cj5c4uSwhK)Sz?Cd>ETs8h?Gy1VOi=JPY1X4t5BC=H{w8PL@= zxdcWYZ$OUtT)Pp$S6db>k)e3~>lq>Z1DL5~cfg(;BTrS(Tet3pGTGkA=8kw+&#uU;tW{$Npwwx%#8~8_1Fu~<_p(f(s?GjIKS_WduEHisZzGuyT zq{AHTM-PXw9&{9oS?5@8=9v=z&|}I(1#e%V5VnNJ@8w5(t$eW;Dfhs@%JLxU_CL3{ z9r7D_Bg|zr+1IgftbMK8OKBb2ENE>}BM^+ci0d2c+uyV_#>DDP?zD2s^&%liQs!u* zMG&$WD+ND_p6#0SVY#s^f17ZY0DHkM?tg81P$E=^4K*EKwM`M%8&F8xN-ezd>Y$ppEWa2iN3c^iQ!9%fJJoV%iq~dbMmfDIqR6dz!B2r>ON(GPJt2a z=CK^kE9Ja~5EutZglm4NP2Xyos8jT?u#1 zI5hjcNR2tEBrmsNWp+k>w3#Y0&<^GRW^-j2j$^TWqy_+B6U!|ee3EJ3#SAwiR4J)R z)(HZ9vzaH`?`k{*@Wm%w=^KZc-E7hz1UWGYg=wVkg9)raoGGoYIj?x<1v-;aq(cZq zCxoC8r;)i2&1)B{QlH3@6-L%>JleDZleA}W4Kv5xAH^}%P};V>x|G_vr=|^r8&Zkf zOF$kK1yf0M(9i+m%^RAfb~}rwC-fT{c?ZrxNcbG>k-xqa)9XVi7r#u?VUCixK~+)2 zT^&@Oo%})WhOrG{ztvtepPiq|toB`|dp?GgHmG`d+MQFnwT`#i`3b`tNY*nT`&tAc zrmry0^meso@bu3B`l!OM9Y#Y@tBIJIIHT8|GeiSPA-wTo_%GvDRj`Hmh%6omXEcrl zmd)xn#_e`NYJJ?Sr8WsYbm1m__?&uP79~1gz?z8oOCBq_@J-+>w9OkdVo?2DU6UZJtb#K$Fp3Vam{G(y zltTJ5odn4{2p#`tZxsQ(5m*tV&7rqs$N5exQe3 z+!4qCWc|;)IKgt-KFPp!3r$zCJ^fn;hGRnO7MB(*dx2bFIR*jp zcTp#R~7OGF_DRC70IJQ=5sp5Cz6|Dg#6n z>+7SD1BAmm*FKE6w?wnJY$bZGOVSd{l;au|>wKu@!CTN~YrVe*4++c8aO{b7lYQ+( z1}FZ^9bY{fp6=3^TXcj9)UZJ9RwW^tLfW~E6ou{VcJ?-j(cTQyB3jO*=y2t^#5;Kd zuAW;&(Z7Dw3mOb8Kk_kEgyAu}z^Q9LObTF4+?_=46)btJ`n}9bNPjk>Rn8S3TLq^) zWeB5lh*Kno`Yz|k;jCSrDG9nV_^uLrkr-IDhG;hP+>SXWtHo1{QbWyq5#g$qZN zH&XQWKDG>LUJxmLmDI3VWn^PRNPUiqcu71bsPw5+&NR4eK<=B`Z}<-;93Fv6YR@R* zP~mciNE@TJqaRzrsl^cSZyrag){2ok6UKO{|D9ab#ftJ}IbR(d9l9^IGI!wgY54~# z7p1k$?)>$*@(^DN60l=0OkCe=I>-~k`S_8fM@`kc@zdqZ;n{NIr>lItkqz6T(cj2) z-2CMjMmGSc?*(3xrxj^wArW010`?=A$kZc5@$ZCQ4TzQ#&23t|BV~)1JQ4i;tkFPd zw!7$L!LV6`wQ?RX^!M!OIHso5n!=few?<)!tMyPvl@4@yy`9F=`DBRRAptHUh1o)T zSuS@MMN`GYwpIJNoKZ*RQ@agdD1@H@H1QgFf#7yAM_LFB`!!=CJ}6d5nEvMuqmx;|oYltHMN!W-M zWh*m9^8$IBM_@?7cMKFvHC}jpQ>cM;yR=_P;qTW&fEXrF9gIRRp_{9!)RQKP3@%W_ zSE`G9y1nnOy_?pW3=|j-tR+IKXCU+Y(F9v5vG*iMu%r=@f)tTny)%;W<1BI;E{|0m zdYnU|Dlmp3lm1kdzSOj*d`WJ4RN%NF=(yl>N!IpCqF_B9| zSlK`N>uSMOZsqCRoUG7Bsw~saNO9}vAGYtu5H3y6m;c%|*q?`!9ta7cZKEhylZ@AZ zm-`j4K;<$6l-2rX^xzw{*U54A+15eLt@;43w;YV!BjWqx9yg@#AIGPv-aoVXfU3&KF3NDA4cUp(hDJ@Mg zhsrb8hsHkK4*HwmF=`<;^PM#gHNj2fiQZDrpiFT{At^YehzoL-H{To9ktGUID#?39 zTUghJs(E*RDCGpUefEHQ{$9Vxmlns08OupXi5z7{CJ#NSgs6B1Ae--GsV(L-m;i?x zH9SkS#NwatvabwzCNoED4_Hbj0t+DpNVwvZp+e&Mri(a7wU1wyhy2s(!zYLb7ByN3 zO1IJ$L4%g*FQ#*v$x|D}fm`5PSckV#2)?MVTau?4O{o3)q;}K>Qdojnq&W^euwJ%m zn@Qe3ymC}|oAJ_sI&Pd>Yp9=rLTw0jH{_$}KD$shX2D+vGO_#=>vi@t*m?%Z%&7e@ z>E?7KYU)^LjSrRma-H1=j6eb5gv%i^;-&pd5xFdMPU+%bi#2r)^p)&)1svA%b3IX1 z74T-88@*@ERh`FUjjwoa=wtDfz|*TV**S4-x8%NIvIK-_KId?0E*%leUtT-1pNEiC z*WYq%P!YlLykUjVmpTVi$0^`6K_ z0{9fQKGjBR#=}Q{JV$MRzPkvxbd#)LrI%anZtT;B21WWE9P0JHT zYBdtSV~Z$(;BfY?ArcmA%SusJdVFJ0y!BP5g)4yAc01h+Ccmf>V zX4tV)s^zA3Z)VS;6JU9_&*e54QlqUJIvs_i`g1WQ-OhMur@&8a?#O*+5h}^gyxm%9 zFC3|kK^k#6_fe9)5!;{GF4}@TOFkI`KIioFM4GQLG%yd| zTZbFYfK3*xWCN&uii4|z9KItRLod@!Dfy-nXytbeOPe3iNVHrq=mcS4uJb;|*px-q z>MI9}{e(?Jg$m`~srNsadsBtIRqq&I%ZVgUdBRFO!f#;YqZb@goVjRd@7e5SUc!N#duNR5o1Wk+vsD%L*s4|6`vXbHC zSlc^_4abICrsz9BpqNf?4qtF1?P-LT5(p;laR|LI+%yGSoP0>>TsnsgsY8H!NPT&6 z4&67pi9it;1hkO>_(i3AbeLEWJ{*$e8g4q_#F(C+Ea>JoM$|(PI>UUp9dQNhimGE0 zLvO02L?;ar|3jO1cdjpqz0-o9+>FT#{_wo9#qs8UU&?Ztl0<^0W6YZ2+X)Q1>c_%! zTx+d`H7_s>Iwvk){*L9g-5nkv!-^;i9ffJBrMJtU$^D+o%PVlQ;*U>w|tZm@WlItwLTa3$1;Wt+80bel&@QPjO)@- zeD<|J;!a7Kh-h_n81IV~i375|w59`nSL#^O?8JgFbX6;OV>-$i zHC4^P0@OZEJJn3VH=rp#-Z!ceSHKpL)Eh*bV zfsa2@R1Vk$Y6CX39^?POlOF%g00s(my-1p8pM-;mH1_X|+F;%*b^uG}P3P z0IdL^?8-|E34-eo)Ej?jIm&mPm1Xx0yXTonmYf)Z*EvyCL9*y#f7l3N*S>VO@8!_& zBIu+Mpg#Bnhg#8kWV{c8#!xj|F}v(*=eIrVJ%-1>7w|e={{bn2YnM4h1t2>Oy$d(W ze+CadUVmiXJ@)C8Oe(YQ+aii5_?G_gzpp)c12wUru7D}c245+FBeKf=!;V)zaAi|) zJ~$ktq2Jc!qk(?Gn>v^TFMP z!6uwB=|;o~Z&cW~g>6@ze8MU0%hpVY$VQUep$~nVWpZ|L}cQM|a z!%F#gK8M0rWnV!ss5S;bP-koz?{&(S9GgISjIiE=Lh}~GC=o=Kpnm_IqiVIY5Ved= zj-R4Ev{|1^OrHHQ&Zq%AFu^9iAP&h#?&yMX7#IP8xqaJwXxa8p;h{|qQhIXLHi)_i zm_lQ=L`e;%e52(W`J<`#0r|1xHlA+h*(uK%qM8yVY6W%Te>7a#W!~9C)}-yj zpJ|rUX_mfgjL+iUn6<{`somwlDrlAbsmgLZ4NgSrD|ggxpH- za58&r!zJx<`cOx0x<-zQ<&(wHH*Aau3=%O%IilKzZp&jrzQZ`sCUG`>K`i=?Z;QqG za9cKoaW|?j8fPoXIkm>q&eOsJ5~JB0Z{_RA^Pj|S35)6+NkcN^>MfKvlo#v9Wh6sr zSRiJ8CSA~3ajJ)IC@iNSxS2d;S)mb-^d$dA^I9~?=Z05BOS6XPPUjEeuXI`VuO z`p<{fT_8j{5LlvT?G3}0w^!GiccE-LoD0PXd^IlqJfnW4v^^D*qAo7AzK)fam63VK zM&+itjS<$!m16X5nT~6K>{WyWncX!ITo%fa@+2uD4n(^nrOX6P?{lJaMlw0<5k55E zIl;9dJ?($^a-I$d?BwfBl4TP2`qp0ZSD_lkalGqo*X*HH5VRGM zUPN?~HnL79Aoawjaw&@Dc=k>+2yE?Fu$k~+K~9>s%u=`!{ycK`$RRp{(Jxi-m@+R1 zf8TCO$Nu?d1&l$^v|AbAN##)32$H%mIeaUSqN@Bsap2RM_T-t@np6FsKY^4#MW*+{ zTQ^8Njm4Hu#$By%GP6|UK(eXIuR5vcp{xT58y%py5fW#Y>r9S?D&t8$sO+tKkghI? zGE9`Txh}RUwACA~s~+M$-hWp~^~wpWwG-9V_2s@Ou)o0K10YZaijqTUM`x@$d61wG z7sz=olwQ=W%qKQVepL)@&r4RkqH+wl*Cs<>?g<$$Db(rPaBCQA@na@T_f{&j`!)Bl zJ;Fc$;cE812DYqoMFdSQ9Guj*JW1BXW5v>xDjEM9{>PsJne1YPuvfd{lfJ!ig=Wm)Q`?he>z^^eFt`{0%xVzxhusid zRiCTt10!W{ekheTV3dqCgD%4ngZOv>`|h;ajZjjCx-AAbqI~76M*yS6s$#RJ9*dyT zVVZ>$i>_mj{>JG^A3e@$LyDS-$O|DsR_fab41V01xf8zpf(Pp2)HkUL}{d0|Rp4g}VPBSMVi% z(Z#mwC{b>WwE`32RM#yefGo=4`m0~cpOKqIAsVdudXv(mW*;1>44yF)>?8BXOc{_C zp^7>bC9MdawF!-_K)wowQ0JQs);CN;zs3l#Q*40iRuf%8a<$mrc$K4_zqpIDk6t#8 zWg;TW_*x>)aGQXEnu3Y}0DP6w9=MaAGK7QbJYS+dG<-H?SZ7~l_&68EJ{{1YVWzGj zBT-UDbeNbWsqB%ZObF{g%N@y`yQx6Jw0KoR#T4AH3}HJe)?RCbI2$)FQ3qjx3)7twF}@p_Cva0vE=G}ld1zV~PHPI{ zKSBq$*+PEC@`4+I_e^LP$R%C5TLfMJQ@#CYj-9?q_T?Z$1RFC258IeC!{D>&{r0ZU z$Yz@$^pZ=Ra~jOA(dpi}ilSeqt_jfb#_F|SHx#9Mk8ohMAY?(@RAQV-uAY8=gL))6 zDSoFU1qqD6u5|KMZ;cs}EBM9E0u(NmJW1y;FZvhC7oW>{xasJF*Sud8Z(0C@n{hhZv$)K>W>n8nW*WVW6b2qb9efp=3L_zb}j?f|Hc*D!V6%w5=9^9(vCCT!Bq%8Np0Q?(t>F5-&@o*I|fJpsiDd5X&s^cM!z z9fp{!S691QlO7Ep-i3POO%=2d{ob|foHml<*|@$W6Ly>L)l?>a@pQUDV@>4U!Fb5y zpuqhY*@uC3*QOIX$^sQ&hi33tX7ijrF~@!d_;DQXGsIehY8yskGKEYzhJ2V)@W`RW zgX!OlIsY-aw*z8(xJXS5qG?If055 z;X0RVr>BvbiAw*wZ$ZzUN)7$}Wu6k>O_WHU!h2?Dg*@4k8oc7)m~I7WJSVuO`pp>R z{Ar)YVNfI3YhWi{_iCV()IuLRhiwJb(~fFevT8vnRj(sO8Y{>Dz{Uff$sv1!7^@p; zG>}M!4hER>$CD5F2T}{s_=3xpN}uET&^md|aozP)I{eZ#^n`zSGmcxu150y8IHq;^ zLhtaoQBd~;bN|AJ9CX~FtQdg)OD(4$@m4HzJ_S8Hg8dfCH+AICyb)`eS{rnnY~T9B zPmsqlbM9(maiC~Q&v3S1iz%* z)1GKz)z~^#Y;os!VI8D*x+jkRp=dx1$mBit6AGCS0d@069i|~Q^Lx8xvk;CU{|a^L z*AE030)Zu|v!sgsqXp))J)2+WDY*OPQ%$9H+-u_(C)EPnr3At*r0FkEuZDu-Fedxx zXNa4K$-Zt}guji+N6Ye&^i&cm40*aA38RZ}55jo;_SY9)-mEaJX{Fb+QfSbY`Os9Z z2}ZR`9pz!rt_>G`qd!6WxHN=@sABPC`<}h5qM7pHNMv3bT(@bUwL%(|nfm7@J1>cD zm2F2-huUD6aP*_yf+TrS(K_&geF5VTLDu9H)cP!qk`yE>&@NbC6)W)@g3B8e8c5pp zflqM9;}U^`|DWYuoaB_qzFRUf1rfxEO-0O6(4ZSrjvt)3ZBIlxR?5~t{xrFY+E^7L zTz{ePbd+rYMT829eam4@(-#>)M;s72osRqNEY&F{|H>;YV!<*#&-n!!2kAUsxzvn9 zlocPQy!+qYssyIywjMx9;b`now#!1H(--o9hcN0yS>5FI zML1#SjohQxBzAqe>T4eHO)&kMa2{3!7Xo%eHkD*c`N%p;AirqJfL463Q%-bs#^6c^ zCI$8?+3fhv*p*sz(QJD9x#r}6#sBS06JPE}j1Kk?1u&2CP2`Fw`fb z6IBwMrR|ULHdXU9A>4)nAzUo}QL2n(s~qmCXg`v2HTesmBUrw= zST`OrpA@=&8*&u)?6E2_qcNE9ec^%C#MSK9FWa-B&W~@I0{u_%f_OfrpW@ug#s@Xr z_Q}w`g1B!j@7|~mGMHfL1c!%X|)-a$()Bu>8MW*Y}>vqI_Zx zWN)$TkM%|w^*JlKbmBy>h++i%O@HMn!|E#YkqR-os@6*%C!CqOQR6Ca5^f&yG_|6eHX14rkA2ha+r=)w*6a(@GzE| ziN}hf_xTdREr-a8uF_e=!*j|)2Kv^>-{*m8DHTe&wI5JK&1X0L-45{_#}3Ecj+(RX z<;N&c*?~QdtO8x*uyAtmhyddsh~cXr1njDRp4qr@&tk$N^5Cc5Lr_9d=5nk;oiH%X z7Lwha{*>(ghYx+6Q`XnVDnopw#RCx9e=_haX>#6}1}I<1DD1I=!hS~kF(WhPXDBlI zOVbs|oC<=PaCoIJUWa69{RKZ%)j{&0uxIYnqO!bvVLQ)lVe(fZPXk$7&TcOKgD# zLM4yt<)K)l2Syb78&vuG*Oq#Iyj@2MSuq=hYGn9r?iIR)o&&%?wAV4BpV0&knkndu zyo5UN4e~ZAohx$G2!?_~1tcmUNs$D)wsyDYa5gbG9I_DZ{nlA9+1+uDNO;qFq@5-E z{Gk0E%$ZbaX=D)It3v*{>KFVK%oK77=hNh4=+-O|GGttKrzzNU2U`IEW{9qi2rOC` zDL2%(FoLeUuM>GgUy)IPc0>{=(%2e9hVXQ`n8H;VMmGc=arp4HUyvA`mi{`wdU#Gr zYHz}4AvxL+#V9Qedi*UWYPQ_RC-Lz8WBrx8#Q8?=6FG@x0gw$~su4V=(sWX*wV%Ct zQ!i)$w|}Hlp5&*gNGZZY>FIq_x|&n;eZ#?o--gkU{V7m%GqHkbAxM41<91hY!Z4$Znd^VMwY0FCd}X zpwrd+TJm%j5wO`H`x?*I0Byw^XFvc@X%n03g(TdtI+3hl@16$jD<2@9d2JVh429mp zaWXewL6ZBQesifG%Wsp6wsNid5h`B*(L0Y*>ouzsx#JHBm`Rzy&8 zq)zA4Khv0puZ##cL+go(%^a;z{fFq||DNB(DQi{=B)+|V4$7GQPwZ}CNas3uhV_}M zd&iFru%(<#^Ry}2$_lI_=MR0>+BP0+!#Sl>jvYbjOCK#X-Q?ip$?G*<{bU7*kXMi& z@Rr?x)LuR6xwX(O{XW87Y&IwQoo)r=FYV%&k#3Ucbwyn&P^$@?aS=ZuAL#6ROeR;v z^;JQV8+7#arAcUetayD}#Gj&Y9r;!B^{_-$usPW<`R>kZ@Dbq!jRRfMgxgUa%(~@{ zg9HoUj-H0jqn#hxZo^j~LBsUI*<=&BfUf_j5TL`lXmmlHH8(c^(%;PL?-jUO4;tVM zonixTLt%oE|*Nu{wkRLIzfRe&eUCA=*&fL8SVOB$BE z(cETbZ*VB|pLcqgS@4+YzKiE=Xw34A&i1nXJe5j3t@t+D1tQrd)X_nrfyY1bz_X}g zQ`ZhQi9Uo8smNnk(9HCwsPDJQ(3?vtk&07I?w9=-^}P5MUf$s0XTC1yQ*xe964NyGC%9&lWY&%fZ zgJ6pcb5n;I3g2g!70g@VZ~r^G~@WGo{bqo7{Rp~{PWx84GG#T@(ZyyOhA-~ zVfDxk;(e>^+U$;4Om#yiG>Gl6q-^Xssr228@OH~QNs)=(g3)Riq{u$SQr0t$ZIs)1 z=(9X#H($V0gb0gt2ehi%*<5m7ZCHO9^`TkHL&WF{P)8&rW#Y0m?|6N7*KKdo#R+>q zO{bctb#YmwUeAsFCB1A0PYdlT^3S!f&}!X?C9u`p&=IWBXSr;9chAOatc_V2ZA`trVgQBA z^$xXh+EY*Q20(o*0S66$N33G`7e;mM0>LX^HdE8dxJt|KBDBdduQY42Tp7;9@&zew zk~(abR+!7)E4>wPQiVFJ7F{vIhD4rVfu8~ZHQFp|x!5*+8~Tt&1--m^9q0AyklapA zmI(Q_LGnz^<1a&ogM2(=ytKA$v#uFXcaKzg@rCR7)d#8^qbe5jBD4bQ4&&vcVR= zV;Gido*oB<=a6*qhFy*MORMB>>4nn^PS-@!#mb-K{fbApk-fZal#q;r8va-#wDs7@ z3S!0Pss#&L&Y{>l8IE%Ot4(nQagVI3EEq`LkB!eo2L$@=N{nqjx$_Fc#&Z!M^cM3$ z5}9|TSLEuB6`%#&ZSLDwn9*?-#26$}CbL)#EIfYv{Xg1#?j}@HIy$Js0^ex}8#Ktj zJ90w5BcPEcMHDIk6+JNe?S)J-_g>ob)Q41MmnDV_wcNm4FEwnbGDxr|X^%|#@>ROw zj?nr=OEj5pnQC1{s;G_2GvW8ajdkoH2VH0T<#`~fr)yqvs&MNcGvPPb^?misfKVYW z_kSJW%H(~g5(MTxB-tg9MBl7@{{}Hy?<39kb-t+g-VGvnmAq9P&aez4_lyU&zY7IT zQ(HSW77i2f9`1sknY4PU;3q*WHhC?q9nP?9yOEF#l4m5}BO+ssWF9DwF5~Z>eGfD^ zgZi5DLuq1#GNvLIl_C)Qa6#2Q)`4v<%&w26U!Oagedi^IY_{O*gk$PlBN0mCMOYZY zs0Oi<(Cdb>Hvv%LtVGV?iN$>BfkWBT_~5vt#bObg=~?;74*mCH(vZm$J}Ajl$oqwG z+^*;S1TD+H+(%`1eFb0ocAAm(y^VO~;SH4r70|{j_vzr?XtuWUpK7= z&Rl(uYzqJTx8X0(}xy<5=vlhqS#m*>f(C{@!aejJtHBRO}qYd?@TDY??tE5vGj zR@<(YAG>H|_-&4|Lr5B1{tiI0d1D2;8(NhhG=6usyYHDiCo;Gt99McP~`wo!MSf*GwSB!I%8b2|q~0w$u2uV_FrjCD-gA z9>7o1l?)#e=bDss)8Fj3ckFW8ZGEn!Qv*Al2maE?YAy=rW-tX@ZV*FSYn+Umz)vE4?CtGr!IgvX4;kJM-jt8*623DuErqxm6Ah6PaD{&0tU{<^ zKpELSd6b2u^w-ut=rB$H?)hnQ?4AXHoeWvJtdP;Uc^+7#>|Y*gP#q^!zK^HRIV6rO zE2B;e-E(unaTdl@phOgcqBrqVi%uH~c7dKd`S~P5z@|JNXU{ZCco>wqsm;>tyMahJ ztFn0+gE%cb;PSWrRPXwTfcvL>8PCO*+)YPvLIHMhcQ6W7(xM&r3{9ArMpMSBDmu6~ z>1>;g^IF3l`RSP`zsQ5Fm?ai$Q?t(3@yHhRnB!4X|ZRA3Ywy96FO#s0!!c zIO^!G#C@1P@i(n|l)Pt#(-1E1HY&~sj&rR1cg4+cG|3G|f+rTX;ocHGc`j#OqBh!w z@!+m(s@C^<;DFf=O-IyRin@Ko&G&yK6|0?XuPA?0I!O?mczowHqazTp$$EFf$r1^y zHUdOZdL1Rb<)`e#KJhA?iwVlB@YBlLIX3xVIieb?TIHWB#$mv&1!(~pQ#ayfHD5r@QQ$oCo z*RJnl2?6M!Q3|u!7uO2Fl|ZCcO27Z5*Gyux z+KO~!3FWXSG+ZW8d#Sz2g3??-0c1s7EF#zVDK_m2)#w&1z_H0&W7&1>qP{OxByPu$ zXCh|M@nxg(GdwQL{B1r)Ln>i6NZUF^!;C9u)n$cX%;$&vKHkgLekZR^BWY;!@)-+& z3tSgNubSefZHtL~9v@!EPPr9F`)tA(F-2e9kkw417ayUWfIbmFl<@1P2Xv6e&!^<1RouV%kLSk{_8e)JFpBt(_fvJpc=C7D;> z$nKeRHt_}Bb3S1#u{=x4dF66AL&1ufDg!HFMu3v31K}Abys2(o+#@6iI{#C2JHzsnc(8U$<@Y+ zP*&KW{6>S08&5}If{{@tN}76yEKjZe=E`yay>USqss_xB-#Do1zGF;F2U_jO&cQTU z*4DiPysDnG?4}ijir&JJ-m2g>5ozD=u+Nn=t0V43uMs^)cMFK$GNJSaU4dM|BZ)So zVGZ@O&coWVkLVpKQm=U-p&fL21?nL8{?d#g*RVy19gfMF2yEBdEp-HS6_}TbrlC*Q z3)Y$YSoW!-M{wj3gTT1!H_q_}uC_h$rI=~zm-F)iqs00arMR{ zPH@lrjDB0Dswg6T8^?n@UL?J;L7@Ls7}7L|xoPbFJC>s-(uvEbecq~il$o)qXzttT zo*ihzj%fq%2nvTJ?|76z>gguz&jJhwuR$~)DCVo^S*1Cl$*HS z-R*FkRaK*ezicQ}uK*RUn&5m0f`nN{iC@fEqe> z$l_SX-c2jx+tF!3!!QjY60b9*x_DEmbh=dXIHYbo6Fpn+8z34}F{JF*Y4##sDHRp9 ze>G%JH_Eg9qw4<#go>ywKi^2?Y7n96$TN@%n~#N9t#RT-Mk5sM3LGRd2t7tElzKjW z3h}64=$Z9kSLhdmSjX@!BxI!JO4V4C}-iHn9S zZ@|MRyi5z6tz#1h6Cyw;tVkYm_b;< z#AX})YZQFJPpb&2HorVf3NJW#&sa<2=_(%^p+BBd5%je3N|$#wj4sp+C+{OGM3;Y;YK!`^c)d;tq3< znP}hi<>}QUs~>2+!@cHUuXDGGE4NC>>@47VvW*;v+k4m+0AN^e#Mcu`q(V7;r^6?g zk+_Ip{o1{|#?Lto(rZQmbQ*FH#F)K$bc~^n_Pu52kxY1+fe6awm3$$2Sjj23NY8!D zyWU@X90^HL;P3==C~xdU%_hQHJNOPI>d&S!u876!1|50?#cF}~G}N?E<;T){a$NEp zt6{Je`5dvWOD-Teo%ZalO&k6lO!1~18|EL?YrV{Qw2(0DMSub1B!FTL?BqP%na;6>qVGPqz#-)%_YVX7>8J7;6qXN&K6Y-_?c6|ZO+L+6IUTsX4n@| zjT21)2Tr_2+pLibwuCslG%p^K=ql1rO7!BPD~XOMIk2*T!Lf7!rJaY|lNM&HaBG2d z1?S1Dn|?vufSm8$fklEwDN(5ni45g7M?cdkRtrH}Z?K-nKqF~1Bu>tRE~Ke(S_Cx# zqWQLOd(suOzwd9&Go2&q>m~RKY^sRcuAG){&e@uN+*4Yy;8FegmpG1iU5@y%9QE#y z)ZwUm>>51FSj^y&Stc>9!=H?9FrTcTKu~S~?O!f$NBcuioysG6>b~d(oS|d8+71C& zhSHiwXK6nvlH1shij6tglqe!`$GZmP$%ks9qb~8rafVZv-q+UnE#$|%^07waJTA{r zor(M%VA_va8~ff9$-fABALgHs#DOV$yqixjE*Utp1i?6m?ZaVRKX^x;zU^QJvn{^q z1`LjU>%c!@KH6>xE;5%s`q)R}Hq+>lz5YpBW#7ci6ORxAvR?cR;Hm1iR>ltPSIPyD)DMdQxVnoC=lHfxjjw_7%a zfvFaxu}IM2C2Z%ufjm>)ffFEhN_t@pqtZ;2xH%>%LC8A8mwLW$96aY_hLass9+Wxq zG)m~1bV1bu1i3u8{S`8(IzzukU$?PcA!Dx<9=4kJC!y3?zN*?5=m!iQR9%v`7^YbU zx~e8%CKoezGF4J_^_BdfQ?JRetZYzrlrjOnA;^vOKP5&HZ=f-iFtAPK!UgrcyICUL z=q9Zvj}=oMemVPpTg(mR!GBx!1Fre-m7Kj!c&Tv*WF)fQ* z0~)*Cp$yH_9A2tJ zY*8qDh?)p^IffM>fSpfHFXpZ{UH+M@?@nC9nlWnIe1-eyUm^P(8lIx#^(hmw_aL+R85{&|gX)FYWuz|iuX+Kg0$ z=lUYNFHnM^a`oS-y})v_SYfzvn_marf-6}%^WZK6Bb7BLFm^IpP{bjp;eVe%miK}_ zZhK0z&^5q&LF1>uCxdG^3q;Xk1FS73w~s^l&EQNCK`gPLm96p4r7L1X)n}+To50D2 zldba2~ZaZxY|@sl&AEoVk#VV85@4Q>R#% z$i`1R5q7Gz;+L?F-hc!Xxns;C5Pob1%xnd=+B*RG?XpC%%QMmy_j++;7sQrI6VR0P z_u^K`9;EMc7%T)6f_L_`=j2K^)rgn>$mLr-f`DUQkt^bl?y^r=$r8uZZZfZZiL*xy9;^% zI6u}F9WJUa?7kEM1(!+&8IkguJ{MQ&|Z=ObUj>pux+z#<6kPAUcN?9>2`Y6 zxz+L@x{l{EtW;8?xp9xm5${;0nEyUEUNC32K7s(D<^R$$7v&LqM^_H~I3+0S*5AeM zIXld-LtIzPHp_S?@oKIm#0D*euyC6jB3z6w`E8tSg3g{#jcmhNleBxfn)RJ9WZKo( zj$8x>vyoQbreO$Jn=fiFwbrLt$(WQu?hdL<9(Svn-Oausym(Y$5mtpC|i#Ys6K2zl(Yb;q{(w)H9rK9-7UU0}wK?Of={;ExrjEV? z+?;_(LqLqq?axILCYzx&QqQy)!+4oY;{6uS3sGJF!lHG{cPg|GmM1G*iJU8>?XY#} z4AUmeVj7cEsQpslz)+T(#v}7H#=jNcC!$Dwt{X^*$0|JseHi(8cu0MKf0h~PD{PN# zZZp_nSaw5-QqD}5xQJ2ESO0erQVC%hYWp{U^DoqD*Q0d03j7!BrzHbOc6`X2he%)7 zGIhag&RD>#e(Dx%iH@{(A;|3ybu6;rFq)(~9YQWFe#1bhPw0F=CixuXR1`hHs~AM~ zdOyuxU?#unrPiiFBP5PyYa#T6+a3Y|0R*`1HrR|EUX`mtBGTm1k!2iOnw2l@adpuDF#F82*|b(vj>kM?RRu3TUs5imkX zhXNRKy#dpErS4NMWAK*HYfpeyJW;n`J=LwUzuzHF^S^-vI6y;U!;fI*3EXJ_ zj||CEa>9Y>XNE;;vy%I+rUgd3x12UhET($`_YrmTl!FhQ5MP|fdvprMj9Ye!*ve?0 z@K|f6#el4S682HlQ|I-@4qKsgOT@UciBjaBm`8K`)>wpuUv#T*9QJQ2Hta#IsI#Y- z3tX<0HgG{B?i+X zGFxib^zpcMe^?1oZ=JRM7bA_}PK%YWXpFa;2#C4a2w0j9SC!{F(=~6ym7|neyHF#c z4j@bDuM542#~^Aa83`@*J|r{ZI^uW5G&$_iV5F}HIJu;D`uMR^Dz}kD%EPcsk=P<8 zv_F)D5-!yT2~ey6Cp%;8l^O~AaPz(2wlU%h^Y|5C_M z?&aAKU^e=G5EkRGhty<(+&$8UYX?m5LjitFH2nj`@_I43c$;9!D3 zK4+Yf%%N1}N$dMFOHB3TpiJ4H1ppC8TYb8$;c`sx%m@ zuEfp!w>g9)vEGE3St>kf@s@`T8Ao}vN0OTqa9I(h*|}BjW)*u4*PVW3Ud_w*c&(H& zz|@-;tXTuDBEo#Hx)D1P=y;-;pcq0S^f1(Ji26k;<<&U?w}o;fu%+p&Jp8tClr zct1xCJw5i|FV{i*Vymx!xFF_(gB6p>YYCEUZ;6jr6QV(@{M^U-;$9mE;6|0+&kAJ+ zF0iplw}HStpzQgeL7v*HR9Y7&r#k@;-NYIi3qYmQ!kxMD;+d>qN4r{yqN_rm{!-;5g?*JEK{PA$Z|GV|doX2Ul z)G<2^H1?;fD!k|l;U%ZEim#qb;!FiW~on&JC{pQ2hWbse`T)Sesl z8A$6+Hs>>JbXVtWP&d?N6f^{!kqg?pB~ z?(b+rl3Nw#x|hWDG;Pgrv(9YO)z}^RdyS`g#p)_zdbV(V0cBlhKgAbM;b+5Wn8s!r9;mf&~NEKe=+K$+u zY_@kioG|1@H-Gj8SXK4^0c4}PH~fWDnn~Hp;)dT_mAveuvT$NM(@K~1WUjCAEqnfV zS=a<~Kd?&wvpp&bbOMKphldL?7Y14ZKPGJf87iaBFjT@AP!-!ftLyiUx2a>3_J%qH zIG@^RnMH#rhMwBB>)ZBpp z-NL>t$STASe?7_|MX&vZust90+DS!#K5e@~T&)?3otE_x75&>iu~KkgkSk~#4T%vf zMKr8~I31_71vq$H1vsQWvL#1%5+p z5D~T3My2S*w%{mc4%HnYxbn|boh~V07br(vRf0I<)+9IhI7kzr&D|p4dB*Z+S1N54 zi0(iwejXgsdm?{B^iR+ep_D$~wGH>tm+(Qw<2E9tI*mct-W+-+&|@WC~i^#*5QsiQX>JJPG^HRkWGq`zdS+IlrlGDHy#A^8J;q%} z_354fO|$=Id6>=$F(zIaYxw!7j4C*UR70NRFEzq}%R41;wRA+V$m*o{jIF^Eb-BTp zw&0nbPCFU!UFZQ8C!Lf1>O{@6WwKA=8Xo1%eI7EFu-N0Kj5>~(Nnld2Y~+onDhD~( znhBA(ma3B+`4=V=F3NvvamWuNhccC3ZxE*SFi}GUo5xN5&#!oIw;Q)J9Hxa0U*Kj~7Z$gKW!ukq^^F zI++jduX&hBu*B4308nrqDipQ_*4d@QL7RNXqKhvt`~bWts%LyJ>t+dZX?R)|wf7UC zyDy4Yb^ALYmMdB#PaG4zMz3!t?eo7QBNu+rb3~g)VxKd3h#o>R7NjM5Sf~$^Y+Iuw zL4m;Axa(QM;UyJ2ad zqxbKPTY@_#bKkKzH^aUkN?@6MbggKa)H|9q$yCt>smB~|P&2HL4pwZEQ@e9SIX))7 z{3v8JbT}NX*25=%beOVB)gW{55M*{Z|OAb}+K+%3g2l{hLciCUPTU${FPvb6hY zV*HKZbyNB3tm1xbuuuCRO`I@pLWO5XvCi1Y7n@~NRSLK6o@ARe#%rv#pnq#Gb8a2S z;-A@~yE=_x=jJe(x0k2q=KdH>MwZ46Hl17&-oGbE>$>CB1~T$T+x06_0VW+FG;J@M z_>VtRcDNTncY!&gx@xlb*{#TRwlj8W#S_ag6PdW65`gS6g>`rwjSMF{oO1asfm zS(dAg4k7;=&l|U3y+7BBL!znPv=Ln&&6P!+g*8Z;G7X$oZo7Rg4G&6Aijv=6jswP2 z(OQoomO{yrEkE*NG$E0AK)GPOajR?~-fS0R3f*hGR88&-vdL60z$%}r*wQ?(?QS3! z=J*3NWC)WMj`qj7J>8PUX%vH06PS-A-ywxrqKGx@BYs#cnV{E!^6kq96qYzu3X=vQ zzFWTQ4KfLTfsrJ&rT?d3Q3eUww8-YE()Rz7IZv#;!Rb0!p{II4;(X(nZLi?8IIAex zLPO=sm>dcRP`BvvSR>%ChR*LO5|ZjxxVSO=XB-L=87c)?t*+ywo(8nDJfMH#)-p1P zeccaRj+5J6{L3uO(!m4|SURr~T=dcu=7V^(ASPKRUwpo3e$rp#;b#@8MV=F{s@Oy= z9mgzJ2W{ickdR7(lcL$N*CO}djkE+zuz&kQZ{h2w(u!h0U?L#3zdy~ywggeJ>@;kM z!2`)ob-*yN$|EfZYcuTSZXh0_Vm(JQrb&!|bmH;Dvi9k0m8*y92DV~I3xi=_fsW@! zXw7FT2m(x>6F~lD!!DJ}zq?YaHkfNd7Qu8G@QqcTLs6e6D{-v4qzxk^S{PO18HrI) zkGF)~{LW2UOns!X580=l01mUinygiG((ZIw!!P}>kLue@f-Seo;uA%58gkAo3nVxa zhCvHZq`K8;nHdEoj1DX9R~PHm#`--dKD=)AH=;-`9z(L36!uahUdt+LlWgFpqF7ZM zJ+aFp`4iIqoBLZrpLv+6L=kYw-RK(bl~qi(JBte;yUso?@2L)_`EfQ&Y|9rP9dB|^ zM(;;N9Jzp+dv4Q1|O-`;ZxogYl?47b16$@i`v-a!_AP{1B}P~^?WTz<{u$!bxWJQvt|@gnCi?=-813*^fgDW7)?B#YItdO?~uxs8?j@fSL`zMi$e)s`Jh(-8w~`K?kM@ z5tPY*%4@q~9;f>AF~kQGj1@F`#4DM+zDil2Ie6=lrv7cD%R)aj`>IIr1bhdtWzFpx zNrdQTqbVA{@*y1MKso~MdIhCCQJVQK2dlg#e*UA+E8{@n(rAUBo%Yk~ltqxS`mYBw zml_!KFypp%jn?GBe_F@;+NOtzl6~*BvYk{~0e=~&)PN+mlLW*IS=C`N19)#PwN*N> zt)4Iyf$Kme#^sV)-lyJLJ!;JGG1g>Z-RP5ionD>%KW!lOzL)aUi-3JI;MF6#^-R(d zX-`Uus?7;I7TDyDE(p`_=?xBG^O6B!(s!olLf+DckUSlB=@Ue|1ak+T`< zKPJv{ECUDA18B?0XbE}F)j)sncpS|A#5-vO#Zw~gu6Q+~bR^A~ z^)VBE7uR^RyVufg_XrIglszp zJe#qECfOOsXLiYQW5(+uMq;%B`UE^>Zn+GqIjW9q7pH?iW1;FDRx==T-!nyq(5Xed zkib11EL7lU!C9*BmdS!m{O1nnHTUCr+ckWXlNWpa+r{`LoE648`JZAZq8oc>6)Td`>YR-tOEw6 zGosykTj^V-}wI*K z$>W8Ojbg0r`ezO-Q)p&TQHCzCre|uTs;m`3>|C-X?#=WbaK_|XR_;mIV}1N)$^Lgoh`A1-Q3N#~Qri16pVjSGM%LE$pxzSl<2jrDDj>L;DjDQU}zbWOrx&TgxjSur*m)_yVd!?*UxKK{?fSmg6s1(Uw1`Vi~s%146=+6 z!E}RPwpkXMcnmWlq_nt;An0%E!{c@Scy1`{PaEHS3%G4+EnLzKSMvC|mBz%qh! zdnDnDu6kiR>?$b4KNUbD__+5kJqE^1`2;gq4)JJ4V`uJkuJ18?xv4;HY}>k4VrEZc zEN~R#vV>9bRJq@u-1MQXg6SmDDOML(+kQ(xSytl7dNe>^&i@{8HJ+?ycdyE(0Wyg1_6z|59 zz%f&R47PSD{*O`k7S4J*S%o5mmn*)^WFM=xAtbBW!1Cx^2H(MSaeZO>cx7L!weGK| zR68aq6aNpr#Sj9zm!N}~uw(3Hv?1EXfsh)X`?+3O-qFweZ{G&3?CbEj$bE$si zL05DxkBA^}$^cyPoc%@5dLb80|M`ySY3vM9%{_NFXt{q7H_`kx~PY4H=MKbCdpg@2&K;G1VX=F^p zzgpH#tGJ%W9#j(_xg6jUP-*?!ngWmPX>~k)7A&YpSee8$Qre&9i=5VnzY8{@~6!6^JfyS#6z;5&qDgWIMjiWv^;M4 z#0CcUzlwK3KYipyI*z_xjaourArPjQp6XMGw#zwg5PC;4Z8-DN0VGo)w7IclPgagU z)7pWEgPY1ek&_@!D?+gnfxKywFFjMovghl*7ob1Jm+{I#tjr;CKfRUvDlpq4E7}Im* zlZJ8+Kmh3NCz&%#fqm!@nd(Gx@dQ>mv*JxK?dI4ag>Lk0H&SaeA<``=S-aQai#wEh)_mXw4 zayI;-gJRHktp=8voz>(}#xGRD=Rhh+e6_1ppWCPtkCd##Wkj%omP~CGy=Y|mV$kD$ zjWJ)WDHo-c{I;I)%YpseA&nrcOkq1&+Q{f{mwXPOFNz)^xN_@HZq?I9mkN|CeQ|>_^H@>I%F3 z>&JzIu8LNJBRj`rZk7&WhkemUH0w#LU7(F2co5z;a%%yqi#=dksT|IQ zL4M>ip}K%&i)5~3TMZ84J@xC#!N}9J{5y>avjko9PLU0j)8{=EWbakzP`D;)V)8z!?LH^}J?^CV7*4MTHG)_u z^Ku5Y$$plGF`ZS}8SR~%b7IgpPfC;S0TepFyt4D<&e0{DY;v;=hr{Duin%XbnW^ z^v*LB`+@J?^l4v`yH6wb_H6N3;pf@IKdyE&3v}ti_K8lR)?!Ir$a`lM4bfPA2canmxkt2iFjq!TQgc`>d^FS9#jIZvR=_rXP*CcK|r+V&e z6)We$2T)8Mia_nfSI=Jrb_KRMo^hYQi-y7hJT!eRJ`jFOCCV(KzA^EsIP2!LV&{ub zVm`AIG>!$)Bzqoyylt`EtggVmY*9cRko#OKyJ1>bJNA5kI6I{uO z;8)j8Suuvpjgj6wD#+kV&;2HpS$i%m8_P0D0>y#{u2*)QI$`Sf$^G$cU+cnx=AVbS z=<2qAyc}8J{yin;kPK;)4(~jOG+~ttbk<7>GSm0{m;uP+0Ri5rws( zir+8`Uz2^T=9!)}N#|uw zE@TNo@*n@A{}3iwfgE+zM;%RLlRB@3NqcECd`E{y$`}Ll??IGhwG64tB=9y!IJ;PA zVF5OGAHIe02Y%dqSXpcWj~%aZ;2RlS<)zG}pBBHYPSE!}x@0)p1$~WwHeQiko>Zo7 zhl;eKKn_?|#)%pk+^Y{Za*0)nU5eGg<4a6<3V~c$`T}o~%OlWyase)B(qAg<7Gb@vg@VgCB z_B8cUI>Kbh2OS}s+*|Gs&h8FaRRQ;CffwH?B5?{eMk&;cKPL%DFn@+wlH<5| zAdPZZnO%X+pmki~J75;3XGr)QTjOv&v1Q0CKCl^RL_q=}F(0hjL15&3iUMz~7m$2& zC`bzFZONBlV(bymzyNnMzQ z=Kz{YQ3D%4gC@of*3E-jkmsv}0T-dQFJMqAWFxT6hL~so`{AaL&}h|vAz1>)A{krc zK{3SH9D|c?_>~NYHOk*8=!#Z)1;XB%b&C0!rB1{hJi_;)r=V4b_QaItw*S?5goF~} z%wsj-`?2R2H7kMOICD>V$$g63NYGO0&K@DSqi)DnKOlN}^s;B8H0z+7rFN0+dnWS6VsPicnGLHK?oTYgdyBWPM zP28M@Ehe+j&&S3`3!IkF6wcHrVdWFIQllT!wmp})RXsSrx8>+iXoGPdSG(qxMRCW{ zEn6)g^PMGa!90;AiH}xVZ=&ICTf7*qne_F(6QtjSuJFU`U4!_r3Z!M&3+A?dH%l2g zWLaftN(hoAR@l7Y?ITaQGvr)DQPyft@aX9FO5xTg7*P?&z?Fg!3OPL%x*lTWp5k#v zdvRk4{D`HZ)N0`V0=@zwE5ZcESQV2=$nz4`OHI3C*6$#e^rTc2EtSO}ZMu@egqO@7 z>bNaLHZ@Di5X^yWe@#oRaJ4|v+@XY)9iLp6=SMMtpEm*Znba$L3C86hUXC)RuiQXT zRe@AWB(?W=$-hS!@ua!2C0Np*=b#SHer#mxmJg+!>TN`m6oKn$&#T{eM(`@=_7lgC z20FfpVcEDI;ilEZUhAK^^*(KN@@ZUL&m{Tl3 z+zdln6N~c;6{lk0ijfSP4m1qj*dOyQ@ViZ}=b*1SV!B=``$t%dtjpZoM;2?yh4cn^ zszSEk>^N{`H$R8|(@FYe=;bT`M2yLZHzMkJQu^`Ds04x6&%OqF%=f&mbos~@LqbVi zHr9maW!k{FpLLHvM7O*!8R&Yk62~>G2yRUOjY5D!`1+*pubuHy~aEEdf#@ zmaog8-q>Yq^<=U~RC8CNjcgk~@9Vx)Qh3goZ8ZF0p=7&qjSkh_N_k1gXEng~ek^rm zV5b$!0dB4M29ArvbGB!ghj;PoAtioFyCI#Ba@2BhBTc5%%zuP6s zHxfrD@pUQkb0?=Jv(N;NPITOJ+is;6?WZ_!egG^tel=tOw17}s(PyFIOWq@75wr(` zn8A_|S4ylt(h9 zeDF$(PjuaT$v@TzFQG7q&v4CRJ3E%l0vX}{Y?oxR_49NYuypS%qR7`>}81kG}^7{lAM#b7wSIoFzun@b@+9JeAn- z4_nsa>gXv(ZEJ%jRyIoBPZ#PC`AO&@e{=M^FUEeX^$B9D zrQ6uPt@)*?*R>>Q#~r5kD1C71nW@SPMiF<+PrI3JE$@vj^mB@PRU9XF#Eapi7uLSAiv4{7R3v7q za40&iiF>X^<)*93|?%kp_7(iDjLxHn#?e1!3)O zJJ!GiFD!A7NIk~YZ}40}j#$~OKQ@9tH}kq4XTk7#Q41 zisV4B8e*e4|3d~j_MYPmWN24ZXxIemnZO_YD9!A{xiFEdRP27z%R|?;b+qMg6e|TE zeF~6*nvDf=D<+YWBH(2M-wwO^B>9#?XdwQ`S^8Wcr>|ZFa0B+1-PahNX)s@vB&*AU zhaNad>5JV)n`V*k=&C051_rdGQD^))xDqJu0I2&EVi^{7C>l)4)tEy`gFR*L01R?` z03g_~o@v4*Sev6@<_?I2X&*Ab=D~3QfW{Al)s*eN-3ge zw~O-i^9ue=D^Z0FYO$H9ijWFbwsjrcISfl8rw|A=&Y$@te4LoP$Nv*Y72nKMn zviU&5Zoh{>^N*;eW-v~%1BRF4hUB%vpZT*zHs-QkKeY!kLdb+iWC-2^FgaHPdA9PA7>BbIG zp13p3B7cjYF#^jZu1g3FP~p3iCOtIQ=n5|p_}+Z-wg0S7xj!Zf0}QI(LiC_v4exH# zIk6`f7%urJyx0o2-sT!GK~ccmIG(5@%Kg%1xbA_K_>Rm7?iE@TR*(S-p^tW(;SPxJ zxb^Q2iX6+k%E0jXILNro=^ie?6iSM?*oOt>nD#H0c=GmEV(y6??>Oy6J2h+m;ZF3k z1L<(bz!j0-F^_?vz>dep%iOmlGXQKQygL^2(QVlU*zH{1f~qs|vVMP0t~wIUVq&88 z{393D>tFy~O@AM+qw2B&T%7UGtdN%!o69VwN-|Qd9Go9i-$!yL1t4RBZ^^ns{xVmD z!=V8mcm7g@767q5p=?xjpKzh>PHVMthTGMtMP(-YJdYlb9GC4Wd!XZyhJ8-CmbT>V z*>H8P>BoC#pkLfXNG!;&Otff#+C;yzG@VgqF0)$sG&m>#pm<`JhhT2@5_4h>mSP@N z9FXRFvWJ@=y!|9Nh39~M!mKY*3B0>nI~1%rLOBLR8EiK|O1m`?720EzCboP#iWIkD zkuNfD9$z#oDJb9u!F|a zGcN<8x^s0{8>46Tv8jg=uK)BJ(LVi-bg~SZs@#JX-)~(Q@Hv7(IdyWJv}d|D%OS&6 z#O^E3go{50Rso1_$T*FIa6xw6Q;Aup98U!)bdF9C;W^1KPmx-)!Wx^Lk4xv%UgJ5i zFB^EWz24BGdzmedV}%u&&M%&BWJ{cXg@7J`5Ta-*wL>;UPSplOxQ-2DZH?XvY7|0a z6*6!lHmY9oCa8;thZBaqSb8Ei9Vieyz_vp<-c7Z;uO?$(EpOY3UQTr3SnxJCHg;@v<=UJel^H*A=9 zA{X~63}|yivdjz*Pq|m8j6*GFa{s;kV@A&r&e6KA9QW9r?Gu=$+8>stax(Nc7Hz#5 z0sp>u88$GHPrUyB-a#*wdb0wzi96<+r9=)<`9BogS2Abxx>DB#BET7v*0;oIFv3Qx z0u}wkl4CAL2qGJzu~%SYTN?~U`5)y&Tx?cR%Y|-6@Rv0i$@$Mp*_25P=~R<_l1Cvj zlcV4(>%k4d`X_|?8S^i~*w2V`QkDUP=G5Xwr>bXt1}^#$pI<(ID%rMP6_^XdK$S6@SVAcH)`Oy14@nrypRU@3A9JFhJ7~jT*|t4t5MdO%V_e7uzlaB~ zHk7f_V01L!SyYg*eH!7E;P!Jje>Jv_lYiV|s@N-e575Q@d8sV9+<3^DIs)t2KIk_+ zzhD(};v;Ket`8kqj_pIcBPPw*r)gjq>FG4IiWXRi? zh&1hDQ)Bzl{9D9mMhJruUNI}<$ydXSTP|BbwR5qgxdP!aX_S7Mu@8IekjNJ+W*gRn zS|aSrc0BILqXzSCA1c!$7V$h*>0@YQtJUTvTmW3YOo}@vyXKswDb?I>vD5@os;|V2 zc_VoyN@B39e=uwIZRLfSmhvk+iGczGcFxzN8T^rtRhq4%+@*DQzfev3v(7+iqno^7 zV6Pj(hihJo-J>Y<>qy4-ks<8#M<$%av=3hK%2XM2#sv7 z6Y0D?#}BQ`2Z}{|Se}rI4Di&1+5i)*W<<3}&|E^l*y)hIxtz5Q8$&+Z78aTR>>5*g z5vtQW-1J$~lCO~|+CS-%!XBHvk$veGY`b>m;jFJ;*$H*yT7|wx{!=z}4rmW33m9{3 z!nW=k{qA)(LT$F6^39~!Ay{jdscPi3%w{!*a__tV33$)S(qYkkf>c{M#a$D|8^0$m1&?EFpx+1ZB%%=7Av0}lSaa!* zEEOg=R}71EU|=*VPwxkM1oxomfBt|5Ygy{?N{#%gxu0pE2At|%JVc33ys zgA%s@n0sJtsbxsAHhCk>8{H+8bU|QCPcJh^AqqEWJ(AD^B81%gU4KarUcNHDOvjyd z6QCEjymfT3a@bJGcZ5Sd*5Utoc*f3fj91tcU;vwoYrA-;nXSnNIQtQf0GVgA%0$gI z6zI=HvrjLA74D+zrawqfG=jKEcPwQ+GA&|Tp|HM~N=Mkx032OS6+GmudOi->fz^Nb z-6bU%#zZSQm#(<$GoWYW((<0c{y!0a>Y&wrcJ>*Yskd>FC#?$ITZ#QBl+etm5Y^H@&p})$mmHl`3xBMs)U%8m1v2kNV5my zo0YMLd=xxa4kecOVGP|m`wp&NGK|VzbO7yeKmY8O7Lhz1)S5A6ysUHY$FcArpaFY7 zNTFksvNGzHk0g3=zUqnPS1~lxF=Cu;R6O71b)%DM{wiuilWymZ$Y$XHk@&=uAcs#4 z{f;DI>fn#+vktCq1bLked5N!f3GQqy4`oft9tT63U6Z5!3p*&u()?!e1!ifrUS+>)nPGbl?3wO4({!jcz{)6Z-dcf0gMS9fQP2bb-|Y zh2ni{>nfgja2;>LQ+oFFpDVh<@15UgHIF@`#uxb4zLHz9i7Rp^Zo zUktFfYz4C9g_|tU`>x(CiBQpvn)3sNd5a;2Mz}W`UpKedZ7DX6iH^6i)GSBoUGV8q z_TLq$i|H(BPz2OW0yvwlr)GrjwWj3&=McFKkE*lkRvfOTbnbo!rLwvpModfM5y}np z(ML=!Zi)Sqy^ktW*~>n+fXIFDFw6;iv~sNw8n~k%4MA;fYE_)k-|}w-cdziMebE z>7s~)KT5RYUDX$m#>N*!^RldvBO?%Kfe149J9BIwrs(ogb}gK?s&uEk#g*;j2UGhH z@qtOgb6lXwSd^n2!jEV_cFTOhQ!&%wo3gjRq zmGPvZN}>nji3c*gvXXNrX`UsEW*8Y~#`ai?EnUUCvK`J=%J%fPi`79Cc88CEworEM zNyGHI7G997tYP**Q9@RrK;zd1}uIP3-#HDEtu(ZGHE<3e}Y&dcfM`!Wmyl6tAG|DhA z7c_Wm*dBjK;w81XzV(hczfZUQiNw2tv!IaswI7<;3iox611CSd-Hk7QeCj1|47o(0Wzupt9B;vL* zsB-f)!c0;a8*iW3;Re@RQsb^KmS;76Hkf|=BV<!W3jhX-$!?oqb8zK{3vS2g!>OO1Is(Wyul{$3T zo+sCzfS#6hs8Do|GtlKzy|yyEJBTZ<_jex9>|+qj655bQz7@k*duie{)rH!tbPgAjsegiov`{UpEA#f6jK0mdI=REJ z2I*DNfZ~t*B7bzN6ydVv4UWyQXba)%+qYV>)PSU%I>wz}N#RR7ywrFt5?PbDJ?kah z%Lxcuz=}UFaD3Cdhe5xkrWO=wpw5IGi0?yr42F+giUT-`A2C5^AYuH#E{x}aHP>2! zE=@_7Kwwz_p3JnmEMfEPidFxf!#19J@4hv;LC@RI$3+;xm#MBQW&>B)_5$SoT9S)m zA=gc=v7yjQ9K-{4vqzso(2)b1f+L^`i#t%mMNnVm5Wtv-COi`E2D&!Pz)ms@%+fs- zG0yrQ=`-`b^JKHIGUzCC|BCM$gas`Gt(zX=A!;?>q0*cU8RXf<^th|jU2%HtUCjNcuFik4upPaqz`o$+@k0$ukk+Od|Hue0qY)RkhLcmF7Rt)oB3YEKLV zCXS0#&_wLvPymZO6yil8Ej(}@s`ppNXvmmn~PA!pV(zf zs}+zByVcgW+Ie20T6b*lA@F3O?8i6E*a_bTr7!Pi6Ly-$?txo&q)hF#xVsQG-YzDi ztw9EZ&Nm0l9E0hKD3nY!=4F7+!GB0uHoLg9dOfiqL0*ME-=*F}0_%lyg`v5l;LoR{ zjvE?$hOlQDaf3S`MK+xX4PK4o@G#jP%-mA1yCA>E&b#jrtz!bK{Lh(+kJ0>Xo*CE| z2Jz`Xcj9D7NN@<@=y?ExA~cGhm}wE_5-_Nod6$3zkZOv{X6fww$0`T!ud|c7)w3-s zI*I1oPrr#`#62c+W;x)=-%^OSV)Htc2*-LWG%s9D&YZihdM&kjkbu9?ykQ~D)AN^2 zbYclK2wSq+9Xf2KjtKN*t|_xQDkIfZh(0n$6rF+F0A#-Nu^k=E}G) z!Mh?uN?d{My!gh)fMoeBsRe;iorQYy+rtfO89^}SeC*@NXAlh~OZM5=jUPQB6EgDX zeo1zmVO)z2Hy6jo3M*aVXwOf!Odyz&XS`w&!rluJ5Id&t%yVhQ8s6ZS`s4YF3=Ta$ zY5QKZ{M6FvNOA+7Ps1L`Er5Un27xm*EqB=Svh)&@T*!F9w9?d5wAE9fy9Jf6!; zYyZhtC_Tnrw4sv57$LdJyM=NTMC=OhqbVNeni%{pFq2I12IJfxwi+2b0+xd@SVzOU1^Do#3qXJm_`qIU27!o!UK`9J<#5CMiwlO)N-f^sR_Kv8R=7)O{KI zL1R4DdQP0qE=))%%5Fl6Pk-KtN;9w6& zF8L)k3W+gBR$PEv>i$;%_C9bhFg&k(be}jmH{De0+U4L?@yW@cQO4gvJ>mjo zHay^t1n4#RpxPRb5sN=s8qS18JM1X9MrJH-%*yBw|7F>95kGkgElbpl5}XMPMi+||gKB`jI++t3z7J4a=^(9T3Y>oONemI6oc<-iI6yxjA5l* zQQ=t8LfLmKLuKZuXZN#qr;bB0+HCg3pu+eUp?<(OS4 zTbP}U`yu=bK7_Cq0R+i0MX+h8v$0b!*w=;dNfzDcCv?k)%sab-&OONh5hiykDw3gL z+IfEHoD}l{D|%Ri?2_Ju6Mhshe+RGAgG@*|wO9s3_;~75?0N#ib|8^`7s!+26i4%f zY&XIcUGF!8ry4z3wiSbrDuwTa#C1BA&n~!+3Hy^_C}?q~=IMp5AwC4^G{|PwNn5Lj zLP19uKIA(`Dw-i#xDWI^-&oA)S!E8i6=n>M{vG~sokdpt$g32F1KtIW`rvzfTm$K7 z?_-XU9f>BSitaWp_epF zfCK}*zT!T)kjcVen)|2}5hXePGELEDGsnujCBHUYNvGRpELO>jG_6J(FikRkKycO! zVn&|NB*U;W)t1xE7sCS%8X;RZd;VV_eGLiP(nlBjK7TtI#ZR?gQp=(q zmumJh2_kMl)P}U*V8PvGG~qu+?9YEu!Q^&Me%gbz0@K)`Ncz@@&2m^KPcX)zkc%fg z9c+Lcyzt5;=TkLYjh(v#=2`#Ug$sHz4^^ZW!t9d@a>d>gBGzd^w}=Z>SvFfm@)B}~ z=gPGaL3SV{G5sA_f6Ej5u5}UGcgihw=c@l}Ms@2l|7_bkZ`X91+WcX9^FVZq0wuMQ ztg{;_Uzefx`1>G#34`C^k|vk{2uoFLk8>z5FV3PDH*nmPL4iO#xf6LdmgpaoMUn)j zG>G>^)SCT-k_)(WFh<%QC|9HqsQskI`C4ZoP^uJHBJKQ+vvxM z$HUL8yP~0D@+O>pK6Ae#NJO~x`!9$)Ncd}rP9nEJ_P%9{I|5V9_|?>r?7ynLp`Hg2 z6%M2(U0u-Jt=Sq+jph@pEK?C&#k?v{jr`j+SW z{_)H6fHXJ*cvbI<+u>#P-F@m>z7WHTlOb2T@7?}mXyU_e@N^$?|5poiDi_-*d;N>Bh(_g(WLY zc|+UYR=)4ff$riRudIH1*Q~+)-fDKr`RUf@KR);EBD*mrsb12;d&*u}*X!+sU-bGn zTBPKatlpH@<7?kxzo}hf>p?50G|v0&(bBh0o)2%Z;E|j+miKvG8k0YFy0z|x z@7tx#EFT`+@cd(OU;m>=rb+D{+xC0^HOpEz-01oz z7xjPk>)bu3&(=r3+PQy}WooxMUza6ETsqP2$v%76ZqM)k*zmT6H?KRCKDWueO@2#g zKeF$qZe4@zb%z=**&B9P`M{+K-CI8O%h4+fznwaM<-sw}-S_j3F@xVR%~;yu(Y1Ad zjja388;$oL8JTHoQS*qcc+dmyje6&)Q@`D7->~NDmHKCo|J1UyN%~Fp`3KJap7eRc zsV^P6`rxy@CiM?Jzc@1CQCHzW3xK2@%~)p5W((@(8f`{xTS5+7ESOK0yLdUDnDgQseYD)@S)oMem- z>G}BK%qOAbzwGvQ_s-4_!}T}Cp4--7<71ubW;I?kwcfxxf2q}Y-0v}6Z1Hb(>+sA6 zJs13$K4D3#g$GveFdR5FsMR+k9{>IKPx`l+JmDAD@m6mvdj438h>0Hrk8Csl#GoDB zHh$5u-BY(+?A3Vkk7;unOG}P!+#mONh1F8W@P1g@>o3*3f90!PhQ1KFvHQondXDJy z(vs_6zI*DFMIDQ#xmM=1y!rXm2j1FKZ^e7>yq zS}xAqZ(Tm|!Z>SG*0Q!QJu|b(mo4n~6_20Z=dq!6_4D>^`s&Z6gWtMvTZ5mcNVONv zyXAf7%x78~m5H5otJWq5M@A1A-f`mD2m5a9IpfR|y>2TByG5?k@xqTQPrtD5>^jqo z5B7GmteE-J&R0IYI{Q{j__{Ow+kazwIX&z4iUGwlBXLs1%9S~DpMGJ<4D;5$t4F0} zJ@u-6!@<|?Z+q9RZN7ix)Uy-kem3#Y8t2-K-?Hxyi@K%3iKFwKDeK03eBr{{Pj>CN zbF$L7?du3t?*0m=4gRbee z_Urri?(^0CPn|n6@Aw~^H}}0<=4$ob@fIKT9PrD+&V?U_e*50uCvv*|c;N1rk9~GZ zp8tZn^w#5fTdz7QHYa~K?xXHSCthpw=7*ns*W8;+Si{)xz6^A9%c`mquw(lbkz7W~un-9Jo_Rah)+l^hj zy#HLr?(FS9NMpX9(4s@~nA;Ei^!i55>^( zcKi9r8@KgY6=S6lomHBAPzGL3c+d8sr{rqG7 zCS6)P{h~QzN%H-M4_+Jp$eYX0ci;KK^L@t6f9;o^Pjqzlb==mheQ>i;Z*eax<#cRiZiXTp06f1dQ?<26sX>enB$_56EM*HJl3UUY`+u5!)8;0*nGp}}lUqEVvB2}=A30BS-TGwG)sLQkzVrRBN9Hy?eEP`) z8Hqj0`|s4>)nMQ=9##5dU$1fdEtg&?xFdR}ZDI18>9;L8beFa3*rDe87dQFzWYODA z-@a$0r*pS&ZOxy2=aKHwcYeHUuf6-|KF^O`_WGgxSK8cH_tA%HMBV%Hcex+VZoJjz zxTW_!KbXH*(sNJ0s1{$FIyC&a&&{JAod3g)_FD`ut!Vsyzh`=mIlpGl$Z1pEUp*G_ z=b}BIo!Z*^*v?yjy!+)t-)?=;6Y!@C_|xN=vTA5)^n4lr74(3=tN0H_9+kV%e-QF- z^iS|_^iQz8e~ehW5FM2MK)bP5)^A*WG5y`9pMG`e)LL&D%A@B2YjwYUo>#DW!FB#5 zWx6RuQc_Hzl2Rlo+azU=d60Rq#a_!Aea>R`{H;a-d7pRi6sHa8QAs4vhI+>F57=G-R z3(m-O3MA*zo^pX(cAo4h*V$;Tm8bOvu{X_#EL<1i7HsHS@h5wMgub?9=w+C683;LRazUCTzU|@ApY0Wt2 zlSN`|yf#Q6l7DBAR6`#z&l;U+jn215@32M}QlDddYJ-#d9fK1CSx>am`2FutCi=J< z`q00tb@~up+xoRfwbsopr)NXDQcI^BJdkKN4>1pw>_L*U)8gpbf{3*D=!%=9IHl6| zvXUuboGJxUX;O%+cjAX++o6+{9Y_e3_3O0+tCDRstjLv>ok)+6^&8M|QE-=B+TNmM z%ld3BNm2@|9oEaXeL9(8=5sj$t;%6LB;|~3+pdFJFiOrdM!CZV$=*9eYPQ>|-ynyb zWvwD8^ARH{y9IS;1SBs_A{|7!MfwJfR*RAkTA_QX8fZOB+2}%9pG7(;Y&Q|WRBw^= zP9#ZTpm>vP+pkf)(T4{V?_`P#0w~S`#i2~`W;yH}w??D5GG*Xr70P@RGfAMwM={jN zioHIH&k2h2nW@|5uGnrZdauFbla>{ypd#mEMHlGMh<+L{M14FgN+Ppu^&;c@wr=X7A zxI6K`#iHD5Q6`~aUz8dEvv@DeVu}g&?I}|z8OR99KHOxK@H3K3WmI*h(yvqlo^Ck) z<~-sbf38a75B6WO_W}ONKtGeT#cQQB0(ZI_1OUgQgn?9ABrDD&$uJS^D8@jf1pRu8 zQkEneChlRjDLXk4gOC%Ch(wIUQ?i0#h+2E3(vv(&%S7H@{+&fXtokcF2KHb~Kx1WE z_4&g4u!fzmhMlv9mB_ZeI*G{01Q|2)xRXJHOAcF43EK&ZH_$mfUPR z29+4UMzrgkU&~ z#BfN$aG0D1+ob!0ZPOCmvU>eolAqP?=H!e!_LTI(AT>l z!RNBpUo63^bUIy<#Sl~CyhztAoH*B;n9qs3IdOqEv49f~apIdul}qi*lS>reU)vXBgooMz~WLLMItQSix&e;BlC~ zGyi21__xM)6WBQn?wJ_eeaO~*TI}(-YqFus>h*oe9vay29JZ4^+e!l*rr~#JZTh`e z4ug944eayGj{wH17Ufc)MDxf5ykbdNPxbe~rZLmy0P z3}K&D4@2m(D9403v?^OAs7wp_hkO0zaGx-Tn1mR5?}8b;fd5PQ-@xWD1?Esvn!*ff z=5Ur~4*ULl=CGRap-s-7c%ZdTa)e(#i?MJ|Wv4ITS2gwh^SfIC!f#5&OfRJqyhu#% zs+d&uIj9r&odJ-=BNwjD{;EMWp@05uN16TCqZWJD%WlLxXl|)uZb`0{y+;`(@idU! z7j|sn31P~_a|g^U<=*1)Dc<1U0wO&H(_cJWBQ|#%XezIst*_ ze_!$iB22${5{9Tk8A~`OVVJsUp%O@VrAZl2*nFWB%&4d?1t~;%{aG1Fka37V%oVve3&VoB_f#0@=Mfc5Fo;c!GJ+L zW29&>2^htjv;vs)Gu5Du!fmAOw^$WSf%ae?X;m<{+Q~9!h0{PXcNj8PE;l<*u?5V1 zitrEXa4T-VYcf|((;b@XR4a3VBI2KTnAaI0OEkZr83@!@>}ja4FyBN3iYRNnQi|Zk zrmXe$(mZlnc!~_~{$j~_noTe#IHCn5MfT0_mIj!8zxx|tasBSE*Y~07+Vp{q>ruik zz+ehwFBP6nz#IY&6_k~im{2*)8?1uHs!U8EUZ0N~h}Y*M2Y5RgF*}$T7ZJ0O&2u&p zBmC9?=N~#3n#)T}gd7HW<5@S9sFTV=29{&nwv%Gb?E>d#%@tT;?g|R`PqtQ_q}1Hp!jvy#hU7 z9w3~2Zaf!-q6u$w!Xq5x3t8*)OY=CpENlIyQdsHTLLHI&kH%xzW1FROb3FQ@np}vR-Omx>N$2}gO?y!a( zf$m@f3w!0)9aiNsbcZD3CRKF@bO^#N0lK3a`$REzHbb470-tXg4GS#re=PQP7mjhP zUAQrSp+#XIgqppc7WEalo)+~LAT3Hl90v0P2bs(?t!-_$Hap5;V=M=L%}l41f}b}Yad;t%|-)sg_g)*kyh;^VF+872jpokcZq?+3Sf(t}1^9*)B0(KuY5 zNKEQwm=&z2t3@A5w?TR%-G=L<=r&FtN4KZ+iPz|Bd(B(=wAOuQgg_cHwqYe$a-^H4 zVqsQtj4m>CNXGxv;&?*`9sZ?IToR4A42!~LbQ~@d5^+f$jmy+zkWzErQI99dY?zow z!7vJh;r|5w&*8rW|DL3H!=%h4Lvp@uRVa`qB^V}^A*~`4m&xUndlOwA45jPCM!G&4 zMb{_d==yA;5+$aoXl8)-42!I6VZXmR9l6Pg@3&V)8c zK4nVA^#@ZLT7rkQCK~sW<0I2@+?m%GMc38By&sUF>u^EWp*ybAVU@ecUh~CxU;n)F zZ}t!PSJQpudL3l;bxa(n2iN=^6VNB3kGvg&&WI!a{zYdPB3`E50NfqK{|~_Ds{H>? za@{|UvdgA;C0bUFlTc@uL8@_?TAV$#L!1s5DHNAPBQC?Da2Xwk%Y;N+l1Jk*H97n0 zq?)@9dpz0a&hN-RdA>0F(D@Tc>$Hs0W=slSg?pBB+Qhu9V^4R>UloFuBD*vGpEn}E zR~#-6kH+PLG|HMW>GNfDA3QlfyVK+g*}E&$>`oQiCC9={BxgCTg%`4%35DfZ<{pL4 zEOWcU!&&B*g{QL2;f23}ggqePNS1TN`Jyc6)8}_)IUhTJkba&d#?HyEm=%Wqhm5$4 zi9_rEO}5I8wv`3a&su&02Ii|@Y5Z&aU34gbzgH)Kc+Ce<7+?(I@=58!b)G4Sf7 z+wlJ^@wgxH_(9_F=V_ERW72oa=zeH&d3KM<`?Fi(eyE}-yGKQ4cFT$)jn~<`3(se7 zDJ;!0cP-qMW$skClbC!y%Ur*(6rA1-P9G*F7Z8&V(9>>uI!v6N?MF{YG)UfB{%-g$ z_W&GvOO3XcMo(gKuE^4;H})IckNpOiM+%=F7E5!vtlB)XkXHAK{PRe5xnP&E2c{aA z%W`{LOLGCdIe<53;MQh^vMr0IGS)E67q(+l$3|;dxizB@mJXof#S}GO1C3oQ)q!UD zv2-Z1G(%t_%hC*lZj_aC04~`w5$njag#9TY{%pnaci1Hco&ewpY+QX-*SF$&~ZN?3Dm7%3jRSGAw>c+^j5rxKQRN7DlD1j#Yngc|W6&TcVHC2U63cJ(ic#HpDm zavg#Q*EY2^D8l3kT}R?%rP;!t7Ucp@$1&Xx!weJ)>j7J$;7-OPA|H>~V|*FzaI#dn z;4E#4A_uhKoLnah)8n2re=YdUT4y(HiNe%XE;vsU3rwwh%5}C{>umG-pb}bz#&i2D zCa`7@Enuh6({NK1u^>h?Hw9rv$+6ay2Cbsbp@yN7QRGdZl6Vo$96W*haX2Rip3ORRW7iQ%tK6( zaBmVlbd~nCIDRngL$e`q+aY=TAjd_JXH~L~ESBsO%Opqp>o50LGk*N>*H63qhmcZr z2uW~uFm}ju>QA225b~Tx!cCi$1UGHc#6*lO>;}ZBNjIfo%N!o&nHGqLMW$uqVX5gG z@$iu;Q#^cW+RhK|vj9>Hj_9Fp@t|QyGPvFNH4ff}wYyVa9%PdL3X6TIshIB{naVg_LtR$`LcAo@bSEH$fq;ZC@E^3_U^SVTt@c3JCWS?g>DBX^T}JxJV14bVUZjyOgCg+8ZqzBQ=%P$c}q=nzhSLR6i|Of zE#V-rp#MWJfnpGdwnN2Hm>pYD`iHWcWFHctCeUsp2v{1>(5Nw@Y3lY8joFlF|I(Dl z_j#u6xD#JUiBjP>DqI5)s%)!bN#rsD@IM(lI)b?kVK)Lo2#7km6PF+sgM0#l`6~@} z2#7|>5~@WbjcaL%!DtK19!5+y{T|Mn8)3UABU0*g^O6P;9)6iGmNe6 z_}`D*Ec|1v9a)e2F5EZaevoTq0v*@T^fA}Kyl^NK>a0;vr~!D5Tvo#yC4LCJMP4Yy z{O~vMrg>{D2&e&+O$lviCA4u|LPMJ=-R5PRG^jVcFteNLfgi#wm!hHPsyBJw% zKu$Syzk&WC%iD{9RAXDNI~a|Csn*kKQF93q-Twm7euUhp=xDA&{zG4;7M^YOhGDHK z4O&KhACYBtTd9=S6_`%0K)i4TGO5%HTnZ)GTlT_hc z+)3oc@1-0!v)Df}QDB_AAQ%9=s{031CX)Yy+I!9g!LenGO>;aNQX-~L*SOTv;XVvT zW;NPcuw1a7v^4>w=F#+znkRn43FY*UOshDZq{CFCF<+ZATvtgG_EN;`wE2eS;5>$z z)3xf^2nm1!6Y4oi{2+Z5K4deEio%v7`TlPx$?BAI5gO&gNcHvs-q>be=9^o@S9f?qT#h$g1?L52Zm{6!D>_WOw+1FYHsBM1C7;0Y5>9;sd*nN zM8yfie-){j_Scb`l^lwwo+45++tfv{OaIrQ8mzyN76exVXQIohhH4bnmYR`US~*nn zG{;5;T^p+5PN)>C=@kfOFf(XSK&%F6E5~Y{)y!}jG+?Dz%|kbc)x4w8WBkil4G6p@ zSo0nh9;kb?U`_GA5Ui=*f2=e_{*MDOl}8c#ibbrYy}n{Ge!-9YM}ZhDo{KrljRP_7 zLI(bRAZ8#W^V&cR4|=0Iki-eDLFGwvV5p&}nn27i2SqJ1Y1O-fmBe534(4dJmT9%< ze(hwELqFGv!X#)AXr4oO>0VtFhJ>9$Ib?syf2$OQ(Gd_-EIa2_H6#p-g-N)sW?0Kkr1})q0!;*h9nGI_9EE93$k4RueY2+SA_&9xDj zWq~SHQ>3&`LGmjYS6u|AO07)bR|DX>)wz>$+Z~r6$)26xz45ScrlZ4Ry zZx0$B7_5e(vBaV{{2&N)uRK-CB*s<=zZ}?yS}08*d@-4J|3Ca9!dSkEMIzYan^=%K z`kx5DRJT5e^^HpQ^X~;(sxfmUKd^WIV}X`T5d^kDYy$!dt6C+_7xk<%?2Le^j@4>qYKZ9m7l@)XYDaSw3bf4R zK+AP4fTlW!?Ii-?bov4f+V+{?5!j@O~{{JH^RiphhW>{mBD}n$lhl2hC z5tboC2M+cg%CJHZus7=OXxzjSyVLOEMov$oIAel+ zV6Zpc_8k%=M_j1IKDx|epMa(IW&4n1RhAdw=6VemUU~ax_?{gp+pFSwt{)JG^m7D< zO7;n5l6_b)aH_yrWU(h>aeeA`S$T5-@X5;ZWw=$Q#HxHl%cxdm6-C-4`&6v{Cx^-k zW093_DDo&Pt7tgL_Gyt8C1>?pc)oH``k1z@;@uj4PZ%$G(593jkrv0u7>gqbTZU+I zs|+`x3cbINJpnPE@Rb`{=~kh`bdoJA?j7?e^GNdu^Y9Umj2d}y!)E>)vdV~T_2cKm zbyA0{c%aD0Pr<7J>&;3rDj7$*?#T(Fmm-j)j%sw-BV}Z!7;spE^1H~s9Ddp3MPVGd zL4!9~z$20YuMsNCN^t_HXOMI9d?4#-AVcZ=1ux9L?JL0)lxp-2T@^_L|1pgTCInE$ zqX|o6fkjrzfhBU(NY{2hnvWX_${PzbvVv0pxXkbXRH(CIhz5Vr-+`a)Yes?wza^R& zJ2m3jna~qNjt>Z}$v-NW9*_5uR=aw>dq;$kyAGMa& zu%nWKmu_6j8TG`y0F6B@kh=0VmpvJoso4q9CGpIZ$VWzu8fhL5Q66a?rOC0k!4#p_ zse7hy8zo20dCi;Z0*i?JmEQdNPa)s+)1b>94S>jB?9C6iQ2rZ_`0~v|n3G1rk}OI^ zV@%|yJQa_Z?(pWJvej%W^Q7F%Z(+_1(WN#}rH{~U3Qlg%1^O`6_S?a9>Ah%BlitD%%i?5N{pvh5{_D>e=2VJ#fyIxZC z#Oju^-ONOU+jUBi71Y$_6nk8Vtmro626^xo$SeS*_G*GNTL9H$WJgMpM8bo;D&dB= zT0upWt4AIt0Z6q?SQ^=w5U(S^1$eY3*SowiLisnR_M zNr=_n%P%9+O&xAl_CZsq3lSN02St}?U5*~7rfTEm&s1LnACl~K)S6!~kO~07*iaYO zCh5rm;?!t$N&8Fi4eBR{wbEG}H9y|pQnzLab>FwQUh+hjtgeseH79XNh|W}JM53%V z@-~R^_k5iootL{l{HYp#Qa^RnIiFwl^mwI)PL~=^>KVwwn>8h!3-yU3P;_`Blb(sS zn*2l&0Z|$dd(8%=U|@8Sgl%1sQf|D-h-{sdOY$K)9NlL~ACBKb#K7~n-z1*zSD$={ zcy7q(NturDbS-(1NKSVE0tD0%Zi{G&<^weG{H1(oF4RgtMR*+blaQ%?`v5Vo7N>7T zI(fUs)zmEYlTVOkZzW|ErgWi~D{U{ZU{89VYT&RUbZ`sFUROQ5loD>2?DvPP2?Yoe z=}RrBDO#J{*rQaxtSPYdW^B1sb)5aldbX1x7~i(5`h;dB)GX5h?$AzzSlN(qE_J$O zn+2o|)l17LsV^Zlq_>5{noIYXZCh(fc7r;S@wb-j1481J->E~V1H0=gM2R-p8CAz# zRCa_<1_i{2bwe+Ez#ZFtx;Vq@xl|-liax5AJ{PXzAr_?#F@hcBhpjFq8gl#cfONeF z=sp1v`eO(Hz??$=YzC&lAtmy=iF}Kr{+GyiogU%SFCxE*H^1gQ%Fmr$CBNiZ4SxNn zD8F<-mHa*4{BR59?;YaHr_pRg#vBJp%2A8Dvxfto5z!C6EDLe=9t%h1g=__^u6%N{dHW=TqIF00h?+PX;iGOQT1 z9uPKe&oj&)TX|3_pan_xxKI+bdeT@pZBmGOs`VwG(-IljJ)0}p%NSzR%K{c$Bdd~2 zhx|b0+1HwuAy3Niq|{7^KH+XI+3n=HsXaa^$C4V(a3aArq<>C@uxT?Yqr~ufyy;Go zx!bS{jPAX@q&sXzxq3Gqp&#k7BR_I#8!A9h?nAVM3?uCnR9rak0WOZ(+gXmjFr>Gk zOnVK9RJu|^T_1oFa?wW zwW{8dB`Bkrg`mtOfT|%(lp*6K0Ct%MW_T$n>XH*>Sn}%)RIqkus4s67>NsTvRN%`H6Z700wZi9VN+3B8t$5qXYet7v&SOzcs0 z)yL%?dNDpLrjjG2E+{PX^q8be(;G5an4>_b`Ur*!boTyQY&{P(g!N1YP8VAaAo^l8 z=(G=vqh9y`8IVwT*J|VKK{{JmMamo)dqc+i$WX%sq*nx_*9a1Mm1+kg?C`nOWG~h{ zjf-n3r5*?IjsQZY{??w4&QNAmcPo?sk0?X?{bZ#^F z2j^P7Wh3`*##d0^u0yz}$pXZ0HMyz3hXI5_Am#>un1}?jD}z9^>(fBZFBDKE{UK59 zeowP5f>&6}FlP-?;3%#dM0dl7nFbqi8os#jr6|V#4OKdc?t`eeVP^|RI2zKgP`Qi~ zhV&otV9rSZpImr?sf|;A`mqf9F~@AnsU2^Si|KB5R@9Kk=12}nZ@U_VMQWX1qJ8%S z>J!wn#GW#|v=$HQ^DUXtWx?F)xBqCNTa!)c3&9QOHqkHRkm9~g?Jvr17G)<<*)OSV zQefE$NFf$QgKZ>k@`n(`68C*-sHi<()Q+awKTz#R)P^I*0zinU@*hZZ*9CrpP#X}0 zS0+`GSeoTxh^^&)Io4~G@i)s@8Oxe7o(f-qCL5o9M8lw5bwXE@#}z|AexM&D!>o>c zO70SG=Ew1K?3$_l6NOIbPO1n?0jP>aWW{AWdRbA)+B9SzLS4M6%mcwk+CGnYZUTnv zezoBbW$<%t&e{e!4_9Y-4a{ab>!4)s1dzNs{}OmpfQeRfd2Uz}2@h35%@H{Z)@v%b3@}O~4D4>Z{b;*IAyj% zrur?Y4ir1rF)IC}DDBd7Te?%}TL~0NDqI&F!6GYL%*tVI5p}! zushe5iykh+)#<4rjom9bdg;x!3JgI@LmGm4QtXA)7zyE;xabndtRbDuTy&=E4csJK zg+8?_8~J0<&%*+x-4%9!xkugHj9c1Ikhio55!|AwI%fFBBcS^MqPt-= zbRVY9f$+tsjXwZBJSD1U-i09DiLo7B;)=x0c9Qv>ikDh2wuu}#1n_r`fef?aQXlTl z&h08zt?qB78pE+Ig=i&zG>V2>j7ID0L@OE2rh?X%YJWlNW=~6+O12_eX;S7IK+_Og zMXl5ZVrVSRE%U@?r`}c#3FK2psj2;h@tJ~(uG?@?FE(X1JR?AFI1JEZ2=pJ7ZA_){ z!S>}$5|0l!@^qdg`SRe8WdDH;NdhcCykpuUesa%7RZm>+3!iXRI7AXT{t^s(Ct4q6px3{r@W7bLKZJ-IUV-6faU zNgaoh#K@W7Qm3apBvI*s6NuvgS&p6|pCxJqm9{G9&BEJBzl*D37K;8M=O#`j=LY_= zTn^I&@5T)8##JAs@XHlWrO88ucXtf^2#|jFmBuzQg+C@BvPc~DETA`1s5!rKk%6OO zad5hdddr)Fb{Y{#l9Wr7T2D(|h*Wjk0eEucY?5qI3o5=Ab>5|1^Fhv4m29%kIDY}8 z`P+haUj>nPZ_DdJ`x>5dpAEgDGrXRe0E55C8HINjh*88}Lxvjy!5qdkKvk!|!5A}k zBh`)bFV9ntgOWcmDrlH(e!JWg&(2wy}- zk~}lAo5uzCORE5_vcE+72(dK|TyB%Fa+FC3e}qIpu#B3&RANZKgk;Pn4e8(G!JJb| z=Ta#JELm+XM}K6SZQ0~4sdP6xufki(zSdg;AXb2}hc$1>(wJFPdYN1NvKzyc3aI12 ziK^a`QIy>z%8sP66pm420?ST73UQ|fSVq21^S3l_=~yGi z63fIrHtqYStHio9Y%w3NF#kh^aUJ&DCJ;DLGn8vC% z6A{p;aP*wNo+dirK(GrIr#3)8xkAVV`yq^_WmGxKMQPX1C}B~y$;xIk4211;Mapn;q9A5?Bb4)wsz-Xz$gXA{ zR3bTsz$@^O7uJouu#Hk|d1@@_hTSf5_K2Ja%DIDD+Z$Z)wl)a}Xv`bH0S=W_!mvS|vWSc@ zjJcCX*Kb4*tUA0cm+F3l6uV2UfzA`FC2AQD_;}MWdOtXhGEY$oq;3L15xyAWp)~6q z{*Ca%`olI^FX+}(A(nD%mM0n1NT8g(S|{L(1r)84YSa;s%^L*?Q+-T znk2+=>hn-1M8kuxL%LdFNU=la2};E>2VQB>9I~S{G|_n&Qsk6nN3&D*3lBf{%4m=4 z1erPbW}~5PKsyQTFj?8BzP&;;U?FLtb)N{rXSP(oZ@@HZT3WT?VQ%MWqg+(vwta0##~=O0u2aB|(dbM@IGlz(FkVVx;<=h7gn_ zd;8HZ?D2KHP8($qJ3CJJd=aSP+D?PkQ=e)6{7@J|6MCe~bre-8p#h~-mS|fN2 zO15c{x|BO{v;yMkk;u?dTpcwihW~kOK2Nhbej%>hC8()RJ(3xu^Dhg=P@t0xs!m-w zT)=bGPeCnL?G3&q4uNm6mR|`U;R(QZkHGf@S_6Dpdf=OMJ$!w9_<~UjuJe2sTm=GX zUVR^~uC87R1bt1k^2!5|9O(6jXhnX>BZ7V`ZH?_0Y!43c6%hG*M83sdM;!M5Ut(6jduRhfv+lNI^1iDs9?+|_JO=2Ag0cISp87qzLjqvd! zp|(q&l(Du;dP8~!2w#WRdop&U?qbJ-*LUaCtF0!H5>SF$V+^LQRwL?hH+Qe#Zmu&5 zO@9b=)hKkoSboxJGvo@SFi5wJZ^Z^@m)f=*k0B!&cqDth)@mVv&JD(@5_DCGYZ%fQ zw^mf|>Vp)&hb8!t^RVC-=FpubFb%2!bCZ5(Y<^0c5_3pUN^@zfYy{j8f-6y&O2=}g z3<~ixm-#0m>?wYTl4OsjNk*Jx?*)hm(3ZpjVze}NDhyh^`>@^;2anRcidWs=Lx)fbdE zLoN68Q-a%4vu=SthI2z}T1_T|>v}2E+lKVYjc;2I1$FMO2ysQLoo?c;P81IDn={bW z93V@m%T8N;lw9NO>@bK0ix{tA3>S;p0qZ4UqOJJS3Xl(z?JSy!SgMijGWT3O_uPG! zvCkNL*{0uBPnX)6!iZRsX@&)>e)h(9^&cC5SBN@ce7LS3JxTi7V%>xmQhM1fcZDbo zi0J`qC_-Kq4`Vjldxc0f^uDo0eZs5XF~pi@MH#|quW3EtTfmGe>7}n>NI!%!yd=KW zGZZ@^HrV5WAq-f<$xo?Y(pRrfZK5v2YOTAmWPc$N3n_*SVz_&VdXEDufAJ=0(t%jx z+kwp{4vVRyX9t)t6y~C%{r-rI{F&`gEfg`a5dY=`-d_;gw=Cr`q(|DEo>W+k7mR?N zIuP34sxAoO&iVclkHZ%tTItrbfbhX+PZl{#M9vt>sev4IejPS=+-)C$JTlr3ql;;c zi)=#_trq4F1+&5~)Zb?mGM=Daj!|%I*YG?AXYP z@$NPe`Y#GK)$fOB1Z|)XnAq#v_Yp;y<`P;g!4_a8o{&HC1uqq+JkM)dg(){n_Pc;S zwYB<;4P>ELhR{m8Qo~(e9Yra;?4Dxgxvsr|Ek77@5nFl9lva`r+ad5nw9P2mcCZJv z4OT2y7YefY5qoDs+&9rw?T$38&z5l8iPNKqW*#z8q?Jh?g}j~`%#C5ux7`z(ifuk< zQ$NU0eo6=$;>t!VqBBvWl7CBsLH%6Rr6W~f>anm%EEtV2*C1_W zackm;^_K0?g;(>Lo{4<~etBYowtUT7qvn?DI3u+KIJ1>_W!K_kpd}&GpP6`9W zMQ27A`h^}lzV4;M$xu$RVZ|0&a()#ABgse&nM(b+rVUWgqMxJYDm%h#kfmYji_w?J zS;m2%*qt-TyTgZg9T#s{S+AL7yIQ`+2&}6+;ezi*NsfBK(rjlMexY^7HNkiQ#IHhI z>C~?$ZRa9Z@?Cx^C>2*KdffV9h{P_JowpTu@|Smmzy3;5R68n&b=jRLFFDJ5XlEP*!JtZVK`!91Zly4Prd(c~Fc8E{yk6%}S=L*Ul^O3k1BCB0nDAm*FDukP;(+k<(#Toa&w{ zUU-$p3%Q)?^P(;W2+fmVYUl689aA${CTT1-w>hB$0FD;^(mzBNtbkeB6OS`1j1-)5 zw9JuOL6dsiZ4B^Ou?Xr24}S$R^zAK|@P)Z-y_o`WI&197lmVEPrL;2o2Tca#w^t6V^|rCb#*MI9#ALYw;Ags@k-f}(yOo?@0*pq1QNhfzjrqSrp|zX z>dR3bl>ke<2bEauKP;1RW>7!JBRYxkn>s~GojR*e=+c-$`-HakVxyYSV1Bk465qKB9B$ut`SeZb%xp#k9+wU&%vR*Olsw(6h-b7ptn9TDH_7A~7|Hof&c! zl}ltJ`qm)i#Uy}!OGDb(tDpNMGIuOv&5h&j{Tf5=;ydJ}&`^00`!nPS*N-E^#U`t3 zH`;?8Du(oQAWfj~AML}yPT^icJquL zW=C*GCD(brwLZ516K}I&MUFJK*rLZYct19unmv1!a*W{uc^ygar7m^04B8>EwxBh@1rR+JLp(j$Ia46Wl3k~}x_ zX4aBkl#;#zJ#v}^Upbp#SaC`oo5i0w169n@N}@;n@y%h*_h+`s$~M*X7EfzU7{oQE zL4FRj5aW|)ninP_| zQQX#1?R}n4dWQmd`BxA-yidH5I77t6Kw#5LY zlxKLw%&9)NmMaL$)QLBXnk7l}ccqDI`8zP@BC`=5ar*?zl`m>{@<^ zdDvtp5prQoKPq|pO1Y;p4}S#10SQVCj*-8iBe3 z+M^34`W8VIqerA1Mxz zecyt}RKu~?7OkBt7P+&_EV0;!aud7_eAz*tV{x?m5d}$q5CBmd)o8)35W9DwlgB`3 zmWjf#=Ti5(wnDaOO9VcOf?X^srs%mZvwlTD-C0&ciY=RQ2WZ36oSF|)Dl74wpa-!r zJAx=KfuI0j2uR*)(VufAfD*9cor8gFy0D-5@7f7e!kFP&$v$y!KOt+DA3JnE1q)%C zUJimSNJr<=t}t2IdT*!Ho7l5iOR6nFIsHpd+@lMlOBR?7>nvS$yjfOFeB%l&Vl|}w z^R{gG+7&2eOg-q1lN>WWv+3K=ttSU#a7S4krn{|}#P^WJrd&*x_N!(@HZci%8NH!N zU-Q`&cnhla1R`)0C%`~7O_I|V?5dlfR z@;(=Ovjne~q+k(4|EKeOFtZq%Xr zynt!les$~HpbzdgmiZ%SPckNZ_!Xt#q+7U~k*=F7#lol)EA=1ikI(!1`;XjJ1Tz_6~~-8>Ug7rtI??;UpcboBA`;J)K!_$b-Y zq6>9icdPwA+O%{NEj6Vhl8w&W@>-d;^CIPDt@Cb;#$mnaJ6$9Fds>@hiC{~PdHUyyP`yz8V+q}Rqlg-LEq-R1E~qh_)u57Yx>LUUkCTm zv^-j{H99lxo=%2EP#x>j?yVh_+VbUI`g+EDr()g@bx!(bFeWfS7#f}FZco3-K7kp< z?9Zq5?34;Wx&a4TYUi3mX;fC{yh2{8KVO5s#R)-+^uN1?qiDz##c2Z*H{zQ*IDZrZ zI?t4bH1p5&1>*R&EvKG>yFUA}_#ScFU|2@|f_NM#Iv0g_;|9LkgpCWCsEjwl@PQb7 z4GK&DeIn??IrM27N}!L|80p(k^eIeAfD_#(lC~w`dn=rPod$idkA**!!wK0)h|xZi z!wCr2@V930I+<0UNlWQ;PA@)43h4Bt1e+*;fx(3DxVDJR@sS8SCxz9aTacsWqI`Af*vfjhG`| zp3qm6kVT@Qu?rfK+0N;ROK%tK!WUIFcHvO&9r$#upItnuD3ZhQ#oV1B8EfiUjdEINaaYyDIfHNJ2IGTs9MrjbGHw!?bP|!s6|Q-Icn5zo3Siz8)A& zJurbq3g2+1QAEOI(W_F}0gIi^-?q?~kq6P4%)?C)WJ!#)z8Og`#6;1~1wP#w0>ojr zL8hr{H^eS2H4s3wG@@nNMzplq2n&{$Msx;uBb>o)X_SH&#v4sX469kbdJ2a*;%!BI zG%6l(6fyoQ%$__$##x*JYcg>Bb3f%yj^gJczPhNu%ou(y=Ihn0W0y-V!HoqU`TLNW zoSb`60=z9C09R5C)RQ4p_Fc4Edqm zl2guenPP?&<%K1PxlF}git#3T7y-Vt2nx9>M)G1pk8WgI^O-*JfhF+?raY$m5LevG zxra3Mu*gKSVnWbg5PO>Ga1r$Wf>xEQU7#}aiHVG}l#7*9vH3o7%ebVIp~Y~qTu#~I zr*#Wn3XAYEwU9~Kz(k+mlKV6UQ@5QKZ1yV(f}-&+fH6Y^|HMa*@Lf%miDKNM%Qg`; zLj*P37ze(E>E+yRP-FD6?OXucO1PvNz_wB^+q}Ah;EoiybD0^N2yTCYJCExZa2bJn zFQ?$En~jPBewz!j9oC{F~j0(+?m;7Ud(;1QI7GMA?4McOwY@Pq=&<l!+UlT8gB*77D{eEcb$w^8CjoOX?eMW!N193I38V|e(8k2u1^ zmwa3p9)2*<;s_qrnxgQ44+%|l;*6jALt)-}Yx{q{ruzCCBG!Xf36jpvyVBl+eWk z`rYg4vWYHPblF0e9JrX+fG|6I^iLXQop_Y`jn$^cA z4}cg)m2+Ib^tkZ7^JrN{_+?@)z#r1^I%fZc`2c-J6DuWqA(j}wt80TMcQ6SWcrYn} z7&FPr+#PJB=MjGHJ{Cu8V(vce=Wgm~N@ecO0C&Io&v2J{=;KQWzR(6f$@qQ^r_n?p zlsrf)zJQ0%Y_ij@&6pxYdO_e!$~T9x>JJ1?sC;u6tNwW4yvjF+!Ma}H^vXAfvFiT} zoMpj}uO-k{_?SB@#FKBxp!de;^K$fQIb|p&ETa&W8gIlWP7q$10=Ik?Ht=f_g6)kY zA%9o5eOVK`sfIw_G%Z0agP&Qw*kwI>yrh<-v@k_`$lCP}p@bo!s1s9?Nhz9d34h{v zNs^J%*|D4$$Ory&xR|i z<2kjIQfDHtHGFd%rx&va0=e<>)Z;W{{KAq%6A#RMC7Ke!d7+61rZ}T%jyHkkc*!&a zo9eY{|Eg#`fLX~uv2uEdqSwk9B9r$SBB!eO$X;9-E>Z|Ci4nLAi^OGg3@#HST#|?3 zG8Lzpr+ov4`~yeh8p#p~|HwWjDEV3(XMiJEJG-6&I`o|pEW3+v4&K}FY57lf`Luk> zV9DNE-2xu{bBCl9#?$>swJnhBP1T56fk3_8_;)qJTTljnx7&#HJv2d0z>17Tsbx{- zr%^X3i*+E;qP&~Nw@-8gQ~5fLZ!2|NcwHK9-v0jE9e3USspPf12_oz@V|LQQJQjXM zM_BCm{LlH!zz>Z2KhrfC6LrjP@s&msANm~1S0i7e_!`I8M81x0M?twBbV;eNl8f$c zq{)xwdaDDuAotaF5x<+F5x<+ zF2N{l(E)ljY%Y+{PnbP6oiKR`A}(fFQGBi0ONbJ6tD3z84iQx{d-$jmN3aqYzE$rv zePs41_*?ZTaqL;zjxU6XW;gUT+xGzekRpT&;-4(CHnvvL#>Yzr3&XX^tBsGBj1Zxb zonBRZyyRmewOrYb;0r5NF*C%B@py^$Q3Hhyz|+FO1t{nr{R-NzpJzZni*GCo?Oee? zE2y!(MPvWM%v;dqSn}fjEiI7SeME%ZW}{rX(_(+jD7V=pm+rFI zpN^EpWXLtG4G$~i|Ijsk8b|BQ(!A4JLPR9{2;u!vXcXSm|Ds1I zIi3!oZ{qP#yS`T#4T<^39VA@1TqjOu(R?^~3<%G$BZtq<)8N5}m%|VMBo_`}KcHNawR{MGtboFUD7)I6d42#r5Lc|&hVi&Tn_ zw|hMy`=Bjx*SJGG;5-ufLJu(iSY2F0?tD_KTsFXc;{YNj26%9li6)x=q5J074(lj- z#PBuWO(PZ^6lXyY75vnN0GjK7NZi3CFjd=k0CbowN62kafh6`PlF#XGgQ;!I;);W6UMpG3yY zo?zjp*ja_S?L?Zg*Pj6^hA`y*=r50>$3)8KKgG@_7_s)XoC9hP4)p{57;h; zfaivcMk{_)RVtIbN0iu@~$gou4@e^{YA4@;Nx%dC0;R zmk$|?abRXX8 z-A2UkC8-z5o8`CPv13RbjED%WruW9C4J?S^jJh)OYU&I1=dR({1S=Whk60YRhme9= zycu-|Rl(Bp^8^T0L$Jb3+wibs_=5K68i_pGmEiyLzAw<&M-JYuIKLc@uQ(nK9~z@q z!Lb22l5ly#>rQH=gDx?8!t019EZvrS!sGEna$uXbnfGDr>1f@D?w1t5d zwIgW2IdWzCIK!eGZksrF&&%Oe#5HxLsE*C@e|*-e()n!K`FutywLp}z@JX4Os*Y^hrvhivmI-ZJ^c5471UCpsV7;1z*gkf&CU#ebO0%!(;5V+Ki)84P zEyeDa41G39u};a*7b|%qv8YZf)cqp?IXAzh4%s(99zPJY)wLGg$c=o+f^y^+!8E;;7I~8cigwb(+DF!+TNa+)!%*2ZkrS1gv zB?j7ZumCy|K(U)<4Q;7c->wGkc)%4>ZC-UZ723nF%UFzQW3>MeO7HovCNY|@3;8xk zkZoEhc0}4{Hb$fqM`X~kwKezUwbZ3FGOvD;06vDpnA~9m4@btWewvbQSxF5K{}#o7 z+q^0niCn-n4A?98=c%>=9bRn{hbBt7X0TkR)AqyXc=OFsQg?IAb2acTh-7~NM-FC6 z-5-dM4ZY~y5c>1(H(4ZVU=R7jw9nLTj={LU9qaOj-g$`48dl(yhRoWMQ?0>)TUt`< zfR~E5Padgu*u_0omdib6cpvY81w(bKW-#5`V>3XTyMtP>lZ$QIZ4yxQ zaU{?t>tjfB$AWo~nY~hnZP?-lV#=i0jS#6a?w(6(CltoMmxkWcLUA`DiH?BeHM+Pu53NHZ z&FZNg%$FaL2T57*5`=y?WfQ-!@k&TNpZ+xiAm)2P6j7O*zoku?IVAVpWP4+i)sE$A z+VnRjDx3*#34Qt>}%>`k`Zu=#A^r;*ub~@#Qrm%&AGFU5L;{=W^UsR%nnXVtn<0s z)e{BGf$y`K1GPyqx{?rG0cn^n^RRyc7~N0dRX^J7-yNR1cKStT$|&uGjM%=BlgHrP z01MuBe@I;*V2%AQY>%ryK^2HovOh z;w2O?9S4CLq#6|2#sMY!RQDZdqkdF~ML#9Vjl+c9+CUS-%{SgmM!^aUIJ{3r{4h$l zLjq&BgBg3bGc!KPVrC4f2l6HodFMg4ml-^wB+1rOhqoWGp{}i<25-gPE!CJ3qP?H# z5RiroNc9NPa|G$*07%}3K0{K#Ee#kontn@dEy(3=dnQuqkR!$JdG0Q?lYoC^15>)? zMZjN3@Dl>y6Qwku!vNnMuD!~-Bc-mSM~u-7Mwz4hhD&?@TSI4?7IoeCzrV}pZU3~d z8ZWpE%W8u6zhAuG``@hu`HuS2QOi|3qw4$LT7D&bXh-1t-@ODr?fq|c1Mp3{9=<+4 zeDzQZqwa_8;0yuuzW?o|K-lLdT6yJxDBiK>ml^T?Qb|jV{d&nBhciTRk}+N*ArC83 z@)W6`Ze@9C4J5#IOE=&e4P5H4+ptd>!BnA5s1HaU*kSRW0;;|LF5p|m`&N#c-B5>C z)!}{B{9Jp6$*zgblALZ_1IG&RI|YgNE&cR*?Mb{wF-B?=7?f4;s6BXP+f1)Xr3Sg{ z%&bkslXr-7R3%4`vD-*3UNWze2yoI@Xb=vCo*0LhG41^$)!i(c@H0{3ejh5y3`YtBpVP;vOvJo&zdCIk9g~iwIm%y;e4JSb8`dBHGZ9$K z4WXC9!fPOM9$g5p58dK!iFZX~urc&9M8t1Rcz?GoH`G1=dbK=k%0+2xy_Y3@f#)cS z$LUfLDaF3!{&95R1&X<`t#PKLONA+;WbF4A$?#8SChAgu6v>I+z=Nlm_8CevM5LwA)Y?0+`v&f!`?9RUI zsb;)K!`ZiR_PR9i86`r!B{&H0D|8iWK9X(STv3RRgCYXvQvyRdC<-hP1vb(zEz5vEMaVl=()K>Fp~+2@Ir* zD8M3W$hgHGwsk0CAZ=8d-;HEev!|j zbR!4)SMTuc!^0P+|IQ3`w^)ttqA}gyi1(IK`_pQ)yI(Y&137Cx=GCumV#@rliM;xm z7}QIEfCse>>WTe0ykA0tTAhRvI10rIxU=p-%lk73%#OTzp}z)Vh!?~!r1@z6sp$o^ zBTA@mz|1$YK;iuvOtrWb<4mD?8N4w}_n(@EPf_3`80fzxD3 zCmrank#DYw`x}%{n+x2pp)PR$LKe3UMzwlZ0B*9lu2I09wBR{hYh&|vH~d@iKMwzx zD|!~<|1U_y__x0_^(sO`Iu8e$;L;KH z7cmuD9SI(oR}CiecJbk$>_v7|-(Z=+L=A=phoIxkMtU=#*w!}!_NTVRJ_XxFr$)Kv zAQ8Q^9H6{?FqB{|{}g;^l*jV<)=Zjfm9H5_8Fzv=@LzoLi5bCJc%bO2abjln6iSQJ z5Q77X9snLPUfYSa!a*TdOq?Kr*Qw~040~OReQb>S)%~PT)@uNgr_2u`95<0$L3uDY)0uDXZEdW0yDfDrIR zyv2Jxe#403!L82!^X>lqCKJGYec$KL^N^Y8v%0Ffy1Kf$S}}2)YZyQ)sOo>B`X@Sg z>cnzB3Y>C-CdFP3G+dx6j~XOycd&#gR>?&8I3vDANsm@qGzoP?FpLjApRSXq@mNG1 zNhGoyng)aXaL!PJ@LbJMm*-w)hJ$NU!c3Xrro`9+=ieqrft~{m^#bM~<*>h=%s#~9 z@i9HM{O+CHJKf(rc31?KP`u}{d8vuz!In{Z77j%{2tDA{G&A2xQJ!gLlS7r32 zYMI{@n;sL`SGT$*Fzcx%1K4y*XUTYk_nOueect1K3SRxLHJ|d+j{E<84O1^pcmq9y zFx|QIIW8@;H=Gj0JI0jniXVlqD<*ll!L`;De!)J*w2M=V9-_g(GTnF&EE#|erSOL~tM!95uHT@0F-NahJK`y$nH+r-o#1=_9?$#7$8?NV4m=EA)(!Bi)6%}>!u z0EDxPzc-ma zKeD!)iR!aJeXanO#imCY)RKod*Vuwj+k%Irfx9DJ&;xC{z->|&D-1uM*f3ovyu<>5 zl)~!|5mlwQE7nR6?-B>1o>I~-V*Wmj9ws}_^Dx;-X*?cYjOaAT*YLZ%xNQ0s2j z&UZIf*9j`7kVk|T6-PN!Oz+aE!e=<`Z12a|-fuaW*#kEpY6sQB3`&=!;<;UR8`L}G zCVN?6zoMD8pf;MiI@QRGYvswL*7e4H2j%bi21%K=0nK^*wzewx^vIKpDp~mV#Z@vH@;N|+rDq<3+AqIqCaoa+aeae zFFF})_?9k@G_B2?j^=!`SpBWAMF>DWeZYWnPC)wWU_cl5>M}tomOWqhdl}o#nX3MeM zdGIB38T@QLU!-;Mb5=K3Cu|ChH|NS=ce(TO8qPrpse*mrg-5k*Wa~dn2n;ELtEo)N zaHX7WqP)t|9%v|2!U-e@S_7XsS%xJ9U%7H$&+{8y4%5JT9wZAPe`EL5*glQ$xG&7x9n4#_>TUtrR8Vy^VWOMACHG56%bXM369S`Ir* z`inVp?1eD?lg1bKA0(_S(W;{;N{~C(5X4~5o9!9rf3`hO7vdOUA5OSJ9i3rV5E%5) zhH8534Xn`|&tPfB1L6J@ScYs)CHFFiW+m&G)Et~uw%7t~rf2Kn4_d!lp)c(p+MvMyo04Sz}WLjJDtXttaD!YR5239%al8Tq6Ck1ju92u zv6D3EpNbMBbB2>mWMQ}9$@+b-(fp|x(K~k1KO~6T9;dtVsmATz+^FxO;CtL9ChXpa zXija%tMxZMhmOpw)>(}keS$|FiMmi7yMmqncBkkcKl+OE+HwmctXB!<-}^fj zr=GMGXwWf&v(GKSM&1AfU+u80(BUOb#7C>0gQbp#ZL>q|>zl6lRsj;`VN9-`y3NMx zI<4tP7VYG0_kfmt2+cB*Ktr<{U2?Mq+(GS@Dy&WPJ?ucv?|uVZE%rT08Gx$99!RO< zG&^AKd?h&Dpg)Ko1j3xn%Ot13i~nHS4>YV*<7)+R+~189L_nb7H6=bvBD2s;t28;& z=yCaFNPB~rg!G}-Jbm-#b9Y-J7%5Bqs}JFnIy*TxYlY8U&rdq-h!@1-iBu0Xm`Fl} z8YrWj3ih0Hp`vT)vJt~rhwfKB_uaVbMt5~qGzc@JgH-a>-9tlW*Ex|jgE%fai zcGOp|VM)Y&!e?SRf1EK-YT)_xi;Y6DekyBvZ>694l+mF2@jdO6aTsn4i5ryL&D1KJ zU~4@{Eil`7u(JA7adDe!KafJF#@wofIXT98jk9IUi#}Wb9Jl^qs{aK|x2^xA>PvVG zTPj@GhU6XK><^|q0=3FLj|%Czd2@~-k-KQIOzfaCa@8|{1-i;~mXqw@yYXXkwv6fC zo+y2azi;2#Bh-xhHksn$_hBRK`3E)JCz#Fr8j8= z6Ut4>@0BverUae0pAmBGQh2&@Mw62~&3W_*VJLs&Glnip#zL2ql-&a6&}M( zT4#$cw?%JKQAtPVu8gAT{U6v(^K)Bq=Giw2RCh|YH{G@Vpa~<5Pv$VFG%IvoVChVX zEV*9g@@(IuZQo<(6I5@5bFeyIZ(wwvuKbo18Jp|xrji(N3bt{=YTd|Btai?8sAWRv z39x*0I-yVKDE!C3(u>s59jZepYr>>M`}Z8Zz(Uz+rj)O4Hgd!=JPX*m(CL&i5q78H zzfTzBn#T*VgU5&O~6&we4x2vlHy7Jgm+ya|^@TeK&0k-Jo5ft65qMK>LE2?1%u4bwmHr1>~ z2m9hlO{dr7t=M3;9FJ|FF~*E)kJWfQQ_jDfGJ%F1n8V4rQFNSJYG!=$Y3FR)^*xke z7k;I>PO0lFGrHEkDH%x*o-gU~|7^y;RKP7WkF$dbR~I){=AMG2M{2ELVaW+Bm8oG_ zqlVqd@XNBN)!Y03G6>k;LNNn}3Z53*3TT{LX+GOmURMdD#@@ZFM053{l|QA)f$Dil zDLj6SZMo^+W_*7yV|*Lb;~i?;6&4<9YfSsHDeR*Dxcpkv+WaA+_CZ?$-Lu1E@nXR~ z))ek+_k{W$8X~R{!{yli%WeOIN6~+0(P4l3Z{=SR))7SR#?Zs>4?m^{bheaKHo@qq zm#}DcGg9Vto03K zBCBy4_i@0S7pMH|EWpJ;aD$$iiMA-r9prB6IzP1ykY@GbyN_VRo1C9_7^+uAzX-H+ zcb<_>!F)`kwLbVa@S>ecigf#B|yD-V8=snM?1W7 zjZMN6*qaU3;{FxaOZ<%{(Ybj6Tq;h|5Wy@k@c{T&>8|X4xI!_EKij>J2Q85F5xP7u ze}u3e?q>(`ZOWgjwBxO;`z(A^avyxiQ*NQqdZqQ5KW~$%;%EF~ngZ!a{?%@^OrAV# zyX8r(xX}!r6s+3MeH+t!emBritw9|@Ga8f}Yd-VfiYKPBD7!-2+K>La%}SGa?)`7| zZsQMa*W+t#zBBXooGj;EZj?Ih|H7Yd9^lXZ%q-^|;;S#Br=r9GCwitS(}Qh)w5`85 z>vGX1v2}5J$Aml!3erLsf>>_Ol_1L+56cl@L01E9l1+jGVuJmNC)eoR-?;%e zq-R+?zxh5Iu={=4yx;qMj_dPYE;G^rbYxb^pxI2jf92s z+_M#4_x}VR%nM-oeJy;~Km@^eY+LxQqNCm5>tK5)wrxEB(|e5v-OG2wx{JlSiEXXm zVh<=5I^W!42NcWw_v|#d*kJ2?fc# zPv1)2Ky*b3wnz*}$!=+X0KyRlhwkJ(Eg6d|Vx1dMT{+#*NKV$+J~SXXbyE?J>-62+ zbm⋘?GiJV93znJ^DVd*+bYhv(g9?|4sZ~^Iz3HZTRohQ`+#~opiJtefzt;$MZW= zIWY^CaObB7U;8wCpI-idh0o=`hyP;W^Z2iEavS(|{;&u5t|bv|*6`o?REIZSx>Ni& z5yB>Bw}R{O-$}ND?7ywxivK45cmCVRtFZ9jMKn$yOS0g<)5JTuIB&3sx8c9nNK5ly zg*yE2{MY=wN8-PP(CoEq54qD&u=b1}Q_fCiCfyz%DeF!uuI#+!;p3Y#(q>HK_cMPswn8l;LM_Dch~S-=RP&a2 za(^sPXh1SF#qw{ugU&Zgjv!MYIdLf@cklnsBhbW50pY2CC!J^oS1%P~-xSN;aao#3 zMODVI*gH+_lPM*1TJIL?_{8J9oNNTN^M1{#f7%!gUr}^&BwKZ?-}~X_J_YD6n z+1b25Q_`egf8xOfZfbNj=Xl{*FnWY-4DYMNF|CU)bKI`4{l9fbonLqt*gN+Tu-B5? zo$vYwQgfC3u^7H3ceCiMWNkkU{eut$CJ%!Rm!>hu)&sNF15%%?r%ycpY!qTA z|4~LRjVDT$ZcRj5Qkf37S-zzcG?p{ z|0*(!&UD}{rvrfXciW_XrC7(Z`fqKS62D|XU+MNT#hY;^K6CZb%W#VJ!Kwn*FsJrmMy7i%Q zr|&(CHJ*Q|T}yvDmT7bbJv&41HJ*U8{>)*$Zc{aCudioqvv|`OBqqD+Pl&{xMsAn97e=`B}F7Q|a=ulxYMb7#hGE zk5#*Xx%%JNE>9w3I>k@{hh_WzvfLO^eB+j9EXOuJv*l)qE2p!qiBGzDd7G2A+)s41 zQ3{ynBDcS9>E4YHH_PcVT=WRdRcvNnwdqx}XjR3af+G}AJ6&eOFjMAKPRDbSyn!HR z@t@D0)x)=R?9a|mWGn z8UG4*{=9Om<>xA5LKy{7Ckg1N9sZG%6^M4QrlH$h)x>;c$nOGXl5^TuhSHVMS1rk8 z3613*F*8lFo|Fp4c&LoUVPS`O@Wa~eU3}L~hJ3!K*TB&&x#Xw)`W3sH3-Lt@&&aOh zE#KKtqjZI@?qWVm)|9LZM!(J+4oK$JpD$=@*1ii)>t3v|HsZk(<{Gg?YK{#a|C^yAov*Gt}k2i@*rS~kJ}a^@{^dH)-4%YL+@w?T=3-K#LMAHVDX+MAwzbr=Hx zn0)4cbamm+B^!sA`s%)A?X%_HKgOc%BT^RbjpMX$DG}3$BRpHy8srLO=V?m$o*thg ziXmU})Y9xUtOS#L+U z&HB}>(J$K0*FNd?7@_cM+q>MhmvAtyFYMr3y4sF)6HfI&KTwjwOhY6^=Y-aZPvB5I zIyHk^rA1<57tyGT6|Mg^m3?(j(nVFQS0VU>0?d^MNvPe<(svhMI28b0*STdkGCCuO z4{;tKX+gz6rsPUk*_An_@TqR$=P4{9Z3bRx6YC$fi4nDbKi|1ft?7OFqgE6>wQy9n z0`-5vK!;Q0Lq45{^`3P+f5&ZR-fkWZOVv}=X~tgyZc5$iaZ?SctXhfp`CK^KG&Ig_ zs8S70u?;n zqo>(MKRpepvR>&2)2O>PoStr2lEq1ole}_aySk>(lfwsw9&=Xfpgx|zpKanVYT`{5 z{FtU0V4rk#?bP-1?B8gCOI8_V_kYO(;p|3ojtyETcFDtbVk0F|Y5({%?F^aj9%Sb? zoUnxp_bW9Cy=k7jaU?-$uH!C7=Ko#OcDiK>2ruiEtKz%2#(3+C_x|VSW_i(zNB2GQ zqu#Fz@VWw*#=!4{)xmiCDjsWDS-`kiO19l1>t0`R$BIH!2uu=@ z6+MGPe-5v$j1D3|_Q%rA(+1;A{l@A;L+mhsagu>?A7D((4`UVEU7zpdmW|~)HM{8Z z3lLhBtQQ3TXPJ%X-?#;?J@$TAr9Yosjs~kD&u5G&z;3m?9(cgjdA2$n^T7RAME`$BFZ0u(`Sn28TfrXt5YquZHDh>MCDQ!P4 ze6V@nB(uL6nf*1;Tvlvc=g22NcWjQ1(+d~I<;-z5Z`NI6U!6*rr$AWiYL6=mU))yz z?qgfqP|}q67fqqB?K-|6OESjSQke(HS+(bMmeYva)k51<{e9#lqz<={-yDvS&Qb>s zM!(aWfEza**@tZeKF=+cS{v9p%)WuGLnUnSQF4lyp|t_M50bfGm}#NeUw6DHfTQm0`wLjV5RQ^@NvTxg(r%nP3dlhPjGBmSz1*_#Q z^!}6AjN~QnFKIF>_8BY8ZYuemCpg_tD{oePZ1-Pr|7SO99w$IuqT40A z%9|o#`3_sw3YCgjNkzM(cnF3o9}y=zpcp2%BWBAp#OxnTle((wjK9bg4d2pI?UeTH z>3LCPd0v$eoCO9_RR$o+YOQ3Ki#iaD_qx0)KIkKj$M~N`kMHX1+QHW?CpI3@kliuZ zeq45QKqVu~dj`-w0>h5HdPs2U=Aj>5y$noT9-R6Sjr=|ktA0N(FtmDW-drBp8T7sy zWI84vY2|@MR2e{ViEL_krIOcG#Y0>3e77{S3CVtyt`e65Y(=!Fa_HBg0aeZ03&zGf zv$k}9xd6~vWbfj}3};z^#od^rP7lV7H)E3#`)yu|#C~hATJe#k07j~~|J1+sf(Sx_ z^N+jj?b6l!Am`#W@|(n$%4FW?X8zq~jtysKUg~DvU^7RJ&CEQ>WQM{tIlE29aGP-& z8Jjz62wqtxn_ed#1Isq=19h>& zSc*@w?_&c43o6-|0Sj{tj%#TW*IL6PMT!Wf5sY@)QrS5>To}1JFDHDuEPLi`y+}=l z`aE`ca$qaNrE_=h5Nl1c*JH=2L(`S%)ay{&#$a}K>?C$KpvZXu@|Z>UM)<83kD~sZ z=&|07tewO4mT~cEh1sFp%KFc)_&PBEGa#NP>jm5^~tKr{vbGj)rjKNL;rJG-=LnHJ|M8F5>-+NG3{U_G$r8q z6wyh+r^fE9pUYaJP8ck;^!k#AT|3ym|8=lw7=NjH*?+AbtK^1g zHhG*jfW#i?bZH;&y)(0iUtP8J3*OXfM-~oHiKs`qzew+O6;f)cO_lLA#F$(9Qyrb z&x!~#!k7B$KWEMT1sW!BLC0YO;iaMp+yxB@k1)mUOvq%)g4RtM`;7e zR$Sh|-I4#pCu*~9tLB@k;dix7u{+6~hp4g8(cb+TZ||9>o<200+K=~X-95V(qLZV{ z<}&m$d#*Lkq=iN?dT@bJj7HvvVk9G}Gb4s#6zc+6|8||H8|gxabM*D>x$J_F86Ycj z{O_9ao9mG2i>@7MDQjMjtVYa5*xM&aMJmse#OjZ%mUHU{-ZrzYyqq3$$IG62*}G2o z746bzMt{xk{6c?==6d~&F#So;q`SN7f7M;9|CjA7?LW`32Bf=rd%PKzaRKWs-8$Oh z{Ko6&J57s-TSlG`c*D%{BZmoNwR(gEi zxwPy5XuGnJ2=}7}JH5b48{n8xo__J!TLvBGvb2DDZjOhD^92+>UD0p34Dd4E8Nt}R zVtzUI>O1Z@Z>Jk)>{egA&2262pL{KT)}`=bpy5FNCZ0Pd?Yd#+vn^kfE?-3XK*Rnj z`gjdR)ASl;i`bihZ*|F$5_Kd%0zuMf>?k)3>caB!z}$) zy>7a%Et>((vcp|g< zyON&m;hjX0>(7&$2IoJM(%=}Kxqu!pvYK;!#mv-w0A^9H@#s;b=9*qV)i=m#>z-rm zP3G))HUx0)UT5%xh#Q9+A`vQdP4E)0$GXCcRp=BTp)6B(9cf{~i%=6w~Lg0aJYn#*W75co|E2Q$t`EXw8_ zKKFCdQ9+Bv4&{a&%niN*TGB|TaHLCrh;`gq-;A!5Q{ln`;Es)I4=mg?Y3eR%KNfQqhzEQ0>o$-#< zqp$lT2=2bnW`Vse%InIN(5mQOiaez3d2V)jpkbS`zvU)rRkVX@cc@aFyW@;>pt!H@ zZVKp?SYN&7Dn)#C|5OrjpW^-@G{hVNZm0KIw9%qd%&cSqSqG3+vc}EKYRwc3z05@@ z|F#*by|-$AG)1*LsJ1g|zQOW)OE&VCVfTV5Me%cyku1&5O*Ym$pRe)m(tx`BLu!Y}YUF$g4qhQW=y^ZQ`c-xI$#7~r3N)ixhp zojLclpxOM}Y<^8-XwG6pPgX-O{`e}ISGf6?yY<;kGSLD>D~Km*mP0rHjSME$DqGD>1LK|CTzO+GEoBh2}lTNiwo8#ZN ztD-M2+^=@$@55)Z{b@WMSe;HE0Gn)=XTvULOBr2RJzyh&-0C<@I*7#D?Z^7+EBInu za98Bi&dy=a_Ar%8QnhnR+cBT!W!v~o^{Fd1nz&09_=eKMvFf4WeGz|E`uFYnJ#!sSR)~3eL`e#>>e(6P1GRI?wBd9sY_b(F14fg{<8%&v zI^!D`JcZj`n8J9^;Y|(c*q;Z?b7mml}Qb<3We>%p5bs@#;99 zyRU|Lv<(k|{wHZmSx{XkL@=kmu+--PnPCMM7+ShGhIs% z?svG%n#BTiIZ|3RAxd|j57vLBR;8+KOH8ms$E5`s0spq|>$^@upNm`#xmmTW7Wc3A zMLr{?Qac(GFJluY6D8}7Vdal-MXavSAs;#VF(0mDV0mj08cxeAu%rppYOB&it8TDJ zJ}{SSIWvoNfcMqI?I~2Qa73GEs+u?q{OA|#&b^BDt;}l98)AoFB8OO->4tUz??MaA zcDcV-sB_7J5u|yw2w9`Vb^Z`S6P6QxWB$0mzqZKUBn69qhQ)S9dIvz{HjznAxj@Uch-^9t$}QO^ z`05p`HSs5)hQ+SHYXWqn{FhSE=qC!liV{D&r(1)KdGD*!AvgL%O-oh|ub@O{=TtaioG3W}L9o5ZcF*kh3JW6NLITXVnDL;f=$CYP%xH<*bwn7yPWUnzdBa-ka)xlmGhq1Xl{W_ zZy!6Ob1OE8jwAs$3klSU05|azGi+pbc}+BEMjkXa50N4mozOmdCZiwYj($u5?c+#1 zhW~T%WuL_n%9d~}IRhZWJ@GKb+7Av61fb-2!@u46T15;uFjf|f4bD|guhu0Mc+aG* z?I{-ck2DW^a>X4x=$cRtNwEn9`no>#R`P9E2@*2^tci*?Wd#e2gUT_vjdoVDzKVc( z(aAmefUH4@l7R1)=L5~3wM+a9Ci+ErR?3*2N0xi?Y=JIQT6tDn0|O5ntm#}dWbj~O zU!t2&fhEC5F-ovLN&fMBWc&z~E+kt|W z;eCy0G+Wd>YMJdTMji!4e2kCi+O2Dih&1wz{=Xz0|z z+xndI8h!{vH(hoCUKu#W;gZ2Uy!<{MbST+%8$&0?#{7X{^R`4__$Ue$ za8B2W@v&gy+Vv7G+4=zeES!ZxX3p|{1JE$&Ln;4xlrGs=wnK6hAFcT-C63Cp03nW& z*JW~t?8$Ciw40kOTfmPMW`|fdlopc(t-NggC1Pj5&jer$M$MTN{5;;}=ePOl^7Gq@ z#cf}|Y1>^}f2Zk)FK2(-F@mYOa71OaB_Qu6f|2JY0xo;j6;7dGF#eCiiBQd&o}eAp zr@OWHls4-n$}Souu~FtCYty0RI8k-~&Pb7^KIq>%B%L3B9gO{>@Evu4h-GY2fuf0u z=%+Zl!Z`rtXeDH;OzhSRA>+pIpl5|mb06Y5!%rN_>~OlVi4R1+&i38Z45=^S-B4mj z*R2WNl{_c9B6(Kixx%&dlEuTJtY7W5<44!wIMEfE-=+M^#75AWD09pz!9!hc2(?%V3dafV;#68V3dekRy6;o9cCy(D_w69g`(4MnX8)$#cia=f1^hc;5iGI z8b&bmIH-cPvoHY5$L=b;T|;FKF4AWUY6iw>tuq7i%5-514cF4 zl>dW{%&DeiU*2EBXwDl*P4l;Fg0XVtYI@DzZs%7sY+uv{-)5Egn(8jf8pH$1cRVuK z^SU5uPPZin@g^KU)9b}*-aTDF22#?na(r~nc0 zEi;=m|MdcQReh5=c=83Y!Lkxc6>djwX6qkpqXwttLF73D4e`O`YUn@zob)+Je|m5p z7|Tuw{)N*IB6}>^aH{*1(J@>_bK~)Z+DAtu34c^Jz?9HL2{eE(;!9U^;;?as@iY6V zuavRvjWT9C9cQ-P#mi9fCTZ5b*LoCWQ0SeXyz#6B7x8tp5nsFx5ifgkn!uvR!D_fG zJ<7VJgdrq|Rs`_c&QP?mH>Vk1dv&%wyjH5_Z$mMhN3exFA#ol)QB7R;N}b#jt*!|# zRC8+Qn?STwF74Y^JnDM(#UU^MiZLADzyhxNE)ua{9aZ@%&zU0bbny zU#Rlj-eKvnl21J;5h;t2m*uP5n;%@syM|xR z68KOB{(~kH-PSKP59bNpHb6`kP-doS4XcrJ%PinmN!<{LVE6Xb{hMz!Y(o}2!WZjT zT*0J`<(H;wo#EA*m#%g7E`|3_7cTJ%|2AEC;x2_hJWrz&Jng;03dn2WQAm$WFdk1A ze*1D4_-$a)7MATQvv6Iy@FQN~7t@8;>{9r5>B3igg>`^oTb2ONZ28c1;fc0zD6E2L z_r?|5qAWLE=rFg?L{sRunT0-{?v1{KTj(%T=Y5xw&7RqaWn zRP&-mtOr^hf$ zdR<4cC0;RY?VNcO(?z!Y{wntP1?jL`t#8bw=kxbZ8D!dd8_Rv^7+|-17u}HlK(6wf zuwiy|-lZLj*!$@uQuaJ~AoezX(ABg2a(Ylilk*zVu*j!pu}%qN`45@KG;6v@A^|#@ zybyMsG7Q4}S7spWVl{a%DZ3)<)d?=b_JhLCA0VNzf}^Zy&?Tz&-1!+CbrECSg`paZ zYOv)=Pnf+iBt7nO6lU~Q{22APEYXVdzd6TXK_XVCrbW&Ou0f<%{BE?abuO4`p|tY; zHrswIdst3*?_g{s8!5tlr#^07pF*~TjF3>`NLA$Oo|60n&vg_B)&K8O*VL3LY7#(|$D&(I@2jY`~&c<0TpY&xU zL6V)Fq#%F5@bCCfZ908+(vH-U8E}HFC@ou=RFcf zJiFmML(|yAO0(t;-2FU4)nlEBe)AohPB{Ti6|>{U|Cu-&j{$!t)1$7AIfswjo{}j{ zreU>wIxVLsKvza=>?uKM=yn#k{QtN?FXydH0DpBXyE=A!UU2HjuJK?t-s$6_?KuiI z|E#_)tT<=cN=9GD(5pk$AxEbX$e7n~gu-1V=XOfFy7J|QCbMdcKs*8zsQ`>ddPa(N(eX zp8PWRSBV7HnIcGl9LdhO|5-FUV{lVs=_8QOq(!VBF$w_=H5A49qAj zRUn6I9aROZ3&*Q0>HsJ=!F%QBGC2OkiZ=0Wov~S5Cy3<#8hnm^$oJp(T zubN-_6x-{wLZ8|!iHuMN4BM-x3P`YnP+jt?xu3VrI1a2Py4xeNfo6vb=kJi4dieU1 zHI4d$RYBc$qFWVAT!Id{$iOpy$1UI~+?j&youegvJ%FRRFT9xB0(CU8)(UBFy5s8LPXRf{=g z;T`QYpCcYd#^KE$&EY!wm3djAYy{=;c^UHweW_fd>}qSV=c;GSluW#fYVhuR4t~h^mHk5I-v*2r~=Z9;JptB{pn7ZtP$ieUG|Rs zK93W^`g>Imj-`b00tqw_i#N+zJlr-w^A~`VyT}Fr?IIWJ{;ji^+m^v-JB{=hZWnS! zZdUnC6~5-ndj>G@R75Y+bnL3QoG%%HM6})eS@5WV#c*%XshjYl3}hpOiJCH+jQdMW zSL~{fQ{o9EW(3e4`A??uI|%l2V{bTP1*1rQ(q=wSSGC4}7~ZK^P+d3*bl!YK8l6+j zBEEnz5FI=a#d(AHdr10i_yuWpzskm^79XR!4^5G2g*anZ6pjMd6+~~n<#^#?Yx2le z>y7T?MZc-e{-z4;1OI`~g1*Xan8_$zLuvQod2~C|rNa4+J)nC5%W_}h3yplh1oAq4v{Vsj98VZ{X#b#%}aIu<^d%;@0*=Dp&% zNOn$ienOAX5u=%b3O#G&=GI|y(jwG-AT~2^;a<97{1A1IWw6%OienyM9Z>{Rx`N(| z&x)Zx`Rabd7w53kM5_F2tIWyv0h6#X>)L@c>U06bt$^ehs(L{xo2i;GVI2QvYvtHle^f069dMRW z%hn0go$M&_baxIZ5ZCk_KC&W880_l8JZB&-Ux|0fbN%N7(Nr*-`~RZ|qq-h)ZlX@; z4R!&BhDT_VZ48!r<&ujeo0CUplCNB@Ek}4~f$}e?or6DJ+P&plOH3}L^ZF2rfubsG zK+;58bg&kw7@7)mNjH_aLYQto)!?RA&|v!U-!jT`HOhAxeT6aG#&f$4^bo8!<`g>M zPHkstpL`7&@UnY}X>TiC?_8<569>WL=AP^>R=BIN& zDOiEy&qKp-J~T`>I!_4-g+G^a$dTXi;XDClG0Mq7K0IPehwSh%R;BP?q1*>NxwM|i zZDw29TdTKyJgc ziQ$w0nSo;?QSW2p@|+u(x)u%&Ty%~lKoVOe08IYf#=d->xaZnjegR_?f$GAr5%`t#F)b#f=B8En}qvFD&AV;SF55qA9 z@{O$d#Hj}ey0;Vy&2WL(uViEGoWpjPZvcS9Uz#ine(=bmp6Q-ujJy z^6))~=+Wf{3J3=s-ZtHb#@eIEqd*36v~ZHaAgSE#@1_!)%Be2L7o2%h(VwMev;!18lUg&#@Yt$?&?nw>~rhK1Pks4&hZ`%-*s+>+%7|nqg29A5l>{anXg*+oQ3xr*zd#G>;huL4UNicgpM9kPK?y$tKH&p@z$k#DufvSJ{iKY25xIg5@ zf}O2hvb?7%&IT7)gC_avZlY*Jo!p=Di04PY z*ASN8%XMFVE?&e5&Dawq`SKq=(X6_?+p+4JC?!)TnUkB_PvFT=7hL;Vs98?>ouqYw z-S7IufiNYJ;$jaJPUf2q5@n?hM1F#tp~8#h-7{U=uPHiuogAum&f9kzOrCK1zPSVL z`p_JJE@xNf)ZVTOJOlY31Qrr~|BcK5S#tp!<5f7RAq*SbPE&lndn^EtzyI&_%AXOs){Qn1hc z72Ted3KB#unxZ}6WJRi-o_aIc_rDk5kxBF?wvoSKzll=otQTK!?kFykn)3U5jha9I zc`;`jO@(3n{JI_fZ4BRO259H56AZa_V5()WGBX=;-AF~}$=@0Ul8tv(@U4|HZpd=JTr+jpEA@VD_ek-b*D<&3(=YAmtb zz6YgOyCAn;xb3Y;W9&{VWWqF;Y-0H%Pp}iFw>0%4=l-+IY#B%@vt^~`4vNbW%oa&G zZ?^Ovt=U2&ZKldIAKIxBkYvh@1@od9HSJNZWxs#$Q}*abl~jUos>;-V-Tc&znm6?mAvg%H78M+lg()`|l}cyu(Rl zyl+lz9j`e}fs2qN{=+NxG~SjE?09);>zBvtj?D~KF>ScD>yB;P-FqIJ#%J}Z%PC<7 zmw(p;ppNsfEhMfz8z5V9>q*i-qhMKmZ?s!J?!UxXr+PxN#2=18Q?vQ~ZN9OdX0PDJ z#9<@bTY~`Sp+U9X-j*=WR^eEZ`~HDxq{{A%NicsufdcUi!xTH0o@o%+j#LnMi(qpZ zW-^+q$1vBAu?Q>PaD*VApJ?4d{V}^VQkP!E?HYS9qEWcEOBZx5mmG*1?fBh@V{Zd=1@8P$N{v`#$>2!yo^*Fs#jd$r=Sf$VzR+FV? zm7DlP9K6hhJ9GS3XS9jP&QxeRbJs?CF!y4(PaL)?nmsX9+}~I4vpQsxq(QMfm35RL zHS%-t0-GGoNy6gC_>OcCEK7^bwDQ$$APu{beb#^_gdOVmDF`25orH_if9F=zkCnP% zQun4HWx3pE4IxxkXl9pF1PFcgr!w%wQT$>R^3|=R;Fi(Vgb!k?HRvTNJ8UxM;!Q`< zVHD*DyLEmX!4JxOe6N&V8axXAK}|{YC*66Z%1qGN2wHe%`A-LAHGT`U&hq0;q$ay! z_lLU@U(zAY3InUrw#vuJJ?;C)vJDOb%XA9ARF8k)L%9Dgg#ycT%VO!#Zi~`FTrICr~~-JDe{`i`&w6=lswP8Q*+$ zGUjL(A%CJrICf+I5NIe@r(bfPu*#Y)e0A)vvc^Sg^2SBwPzD{zDF{T*2vtRw)*3R+ zoXu+)e1l?7FW^IKK3#q5q+ZI=RZb#;)Jp^hV<%xLK_A&cA+$mB{MU^eRk3CCY(A9e z9MAMLyKB2$3}5*ySWg9>&Mw*bSaw3FhBV<94Egvr>>lx$G*n;xwHV%!GPOa8cz9*8 z3ZI5^udzioPc~sM7TixNrB0<3qI$g%_$$q;Pq99RZ4Op5Am&)n>&F>dzqJ!u zzeR25tzZVNMM|l6_d=?qka@a_>)d{%klE=F+F&IscA9v}L2=(ujf~?6+UCaFY*?=O z&ucEWQ8#5+-3S4x!7Vqq@F~0)49EY2FD9ns2CfDyC8G&f*>VPcS(M|s>-J|{u3FY(f2}g8Q}%X6z$}YS zu{I}Qg2P$(Y)zNQ$}X%+*6;hx9Z=+2Uf_C8vNTa66Vc}xK~EE2~tCY?*Yh*ZtE zhUVI{-n_*wkkMCS*<~p(KCe6;{5i6{kMBC&3kF;rWe{g~0RfFtpAf6O$Z-BjWm>mg zg74{v4vG`2i1AvM!ZmB&FuTUgvTMxVX0h_sn;2BF@x{2W($1O?>a}@uupO`0W4m)k zb5AQ-W0$lG?5ebvS(U=SqqkqIcI4YsJCao^w3k`o!dQt~OLuWQ>5y9VkO8}kGnWeH zZ5a1}JF|EOOcMpu^3N5azXOQ5jf;$Gg$LvNxp02j!J5qDON~$C2TD1N6oZn{ zc!Z2EaMs_?bTo%g)VV{mrK72HgCwKV`x)ilut?v#+3&Ub{^m?rH+F;Y>_k7@$}F&M zllO{?P>^t&Tz*i7+oXnO{#kuz^iAIALLIgQ+GmG3^Nai6G`f=%%S1huZTV7l(ZiOn ziZa?4Jcic^Z2`I1*TwkjFvDdqzS|%8G%dXG2X_J}{Pd&TZSpQQ1Y^-!+7Op^biSTp zOk>jkCI~R@Qva5)@_GT71nJoRuz}ua*Nruyi<}o$2}Rc_Avx6mb~TG%@+L#gFVt}yo#<<(E%AnwJu9oZ6+=82Q&jICD*F2*j{>F zVGQdQ=KRQx(w(*#!*Z2frii2aMK(~JY(TUN1(CTm{4?dD7KfC z2)#Us(2>GOgnB6=o}{ltul^y^MCKrSyFGno zb8JuZGkbc0AE`x;Daq^1x@dt7g4hH#&O6x_d!cYE=E7Go zfs34O8LcHxrrd0LoDFN%n$E8^AF&%Xm(@}ir|a{EbCtmMRg7iwR1rq^**T4CUwzZO z{gSyvlfWwPDn1fACvXsFofXVIm322T!^|eGRK89z?n<#d0q^f{p^$7L3x((cYB4 zRYZ^HiW$>WnmGe-OiE+bj9hUs)9P*&i2aV}>6_d}{|>-*^6zVxdUm1+hR{)VUwT+_ zrrk~+WS8*+%%UDX#bZ;J*77N!Abbp7{x0hdCrvCCKA3o)t&4d-FTYnXR=Zk&fI0XE z#`lJZ8ytLU)|VAEi;ja2HuK;6)c0TWf9kD2fuR8rV?TJ43 z82U*47JZN{^Gf5vO_A-zIOTFlfCT;refPg`X&1YJM<*gNe4&RJ-tMUtLncQXip(vv zs6Wi2o?pnr@Nr)0A)xuy-$y2CU2qd=eX z3!v{~K}Qp$l&}6>(Glrn80{tfh%9d6172IoUuVua_<^GFcgLP)GJ@^K7bWB{XopyC zhj@k^;_u85ef1IY-Bv?aV)U3zOaZ6FZE6h~gvpFF&rTlSX87a1N|jW)I4d;5`EeSS zeC^ha+~s8uB_HB^PO`wdy0xu~ar>hj%t#1ZW@?qoi*4tuH~*uV_yXE_zxAf#M}j?` zD|e2Jsc3G-`Rax$RpW7wDNc9c*t|qzI=MiAR>wxpPH@WWPRo(*nyh&9ZP(*9U}Rn~EWAx$xks_}06wrHNHLjW zAFy-&c9eEE9Mb7cA#tUzD^@9AS2pmz0oNBcDFu)oGfhy}sTq_}K`m!g=ytsc)8gw^ zSNM*pVDB}H`pckM$$D0~6IkWAkH*=cbLYO;6i9+SI+$1|$wAJSgUzPmsbOZPa7lul z!hB48PHG7LTV_k4$*+CF%XphSYq$WTjj*XB!qR&8h){^v@s!hl$JUhd2ixkIw$lZ@FZ+G z-TA8BDlSKD{<+v`BHJmjtuRG?Zl9ha4de)4rxM``qd-M{N8LVoT@mo>{zzU0H-pUt z8@WyN;_jLD$;IRtm*xWVg&2;8au#t507!_t&>2si z2yKhF(?O*{oG5NUgWpzP1I6{2NE4kI8zK7vP99H`FgpM+GIwuR3UhY>pJs+?>r;b=PIP5rFOb-VdY*l*X1?WXj;;wa0Oj3@Vvue; zE}id@w7Z)tOgtd(eprERr6Rh*^)i`LcrlTpe4$JEn+hK$n=hdZ{6BqdZpS*0E$Xgq z)FhI=bd&yUlLF2IC5EJz9%D#)M};@ppF3(-k}ib!@{6;9zY1LX$vYyfp^wGAR$Gs9 zB4J30_tl*O`^55`vw89tKO{~zChObKRiR4f2;1wALrkxQ>h<5uKocZ}C=8Lg1xVr< z)Yo~)6WSS}2Vqzpa=YF6&J<^Ic9U1%$J?98$kqGrG>EsfYvh{Tm9ZHk7b(WbHJTL9 z$OV%62IFVwb^DO1#;L(_i2>awbN#aC1ras1qA;Zo(WZ1Zj8$ohO%0oRP2zrRTXn> z#n%TLMC}|&#pkJ*9PA{IG)*qHIiD!!QF0O^RqYj@35BK=%JZ28;J^!T#S% zu>Xt9*j`MHQzJj8Li77>^3@xU;TvuF({1_7R6dXL&fma6E4h+mef4JcOZ#`EcnVcU=X+Q`PP8V^Pv)c-*HhA$ket{j=p1GSpW)8n(9B@0VYSc-^PkN$ zsr?#Bim0A)z2pgA`O1@l_q}nL1v0J&u^o5D9&T{HpB+G!1~8TZBum0$TkB}yRMhjt zxb*kEQD)39|3H}}sJXja%DAgcQ3#75^*a<;%Pe#E3-Bq?JU}gTY-DSS? zVJ*(i(Y6Z$D=q89=8N$&S={Mrd$;jy!Sv~Z?i@ZtZ?q}m zN~A)gHPl0T;6Ji?r})5E_Za0c&sQwi-28d_$jeCQ=EkgA>eg(f1blHm1qkLVuic|_ zeoQ5nsNv|L25X}aHCUTUG~&ilO1~Furu^%hwbY!ZxOchTALdK|Et9i9-RznIT2uZV z&-c!Xl;(u?PWitvUps_Lox!%v?*^DQeQI;8+8jrlUIfZo5|TrZWhws}2D7?S(#F)M}o34SX!rOBKopm$ys%Iwfr_o-=EJ6>O%PM}#A zY~KfB3oaiwwaEmxA-@e;?{4JK67d zQz+VOc$fJz-FaGm8S%lF6-hHJ>$y$(QES-p)gYPiHW}Kiek4s(=SHaJT&cR0zKsrv zlH%M!>BR;7FcF`H_~{fg@>}|Q+v$_$|5+~nxs4|~S61i%?|qS@Fk$TSzK9`Gxkb?a zqCj+0TiVntXRN#rwR_IAcE3m(LnD_T-n`tvQxR=qHgaHP`|4rj7h5RE|GM}bn{1Ed zQ5mV5b7bx&2*13(_bX}QegTg#@4=s6E20?CozLYA*_q;Xy3$@BF5N~KB_B(WarSsg zxxEJ$AIWO8_pIT}<{Q~l1*f*3AIx5z`nV=?75nrB_rar5$V^CunQGBoqbav~DR)V7 z06en%w#beHeAmg^$n`+XYa!EA=SAi?J95=P^vtiMOkPC*#kx<`rd&$i<%`>ojvWm@ zJ_27PUqFn3$|>%>@G! z6Zy>SP19l+g-q1LqsdIDO*|M9{rQ3H^VRj%m*_DGKc9AZp98WT-WwW~Op z+I?{u!@FOrM{)R7ROlKlQ zoo@ZiBz>jOOj1A3X;P?hG@xnP9o%1&^z`SBJA-@Mh3O z%cx>W{iwr>jL}g8gVC$XTSgU8g0q&C{~B!YsoLLoAhGh8 z)uZU!ohd_}*?o<)zL*lxW&*Hv4HugdDgQYnnUhMTHfX(S4DrWTv6gG*2&JVm& zOmG9 z=-`~l)g}#wYTeiiP^|!GE;ZF!W|_G%rV}V<(21P=@c9+ zg7^O5AT@V*;e{gjyNv@FF;|S7qabO@KckUM{N}aXK6r1M>RPeCrGWE3jsdDQ(5>|& zZOXN$fDxnxbc?vd|3Fl?*e?I^&r0D5Ytql-b1{nQt(3(#I~C$To?8Fk{=Z1OySDcuX;xp`aPZ@qrr){rHM3G%9>UsmLbftaNu^UxSdw5}?%$X#=J z=ki&tw`eK7QvMD4w)rup`Ia%3Io)$ZPiOS}FDBXkjI|bn*5TjLfL#iTWqvJ6 z`43{MORX!~GNx;J_@LMrUhnD|8pC@Ego5ST1X})fq^{K6Kfci}EBTvU5`Q~G`XZ)B zd4FSt8mS%)-R*gFn--pWlNMfMg#^AJLwBCM$Y2oAZu&M2S~(}Rr3N)(?cv(oVkIus zqmpY`UYHar#lr%<**L`0>7BmiOnod8II=J5}J5Aq*TEX?`TW>2s->F}x(JA^OH2xcXd-pcZ{pV$y~b&VgoOkW#{8x-S*=7Kk`aXVk;ze>1F%7c2E5Y{Tcl& zrzZXVooPmYlhxmuztrE5bbr^|SAi})7`vMq3@jeO-$Xwqse!=4*TF5{8Yj>N&-D-e z;(QMuJ%14&hg$etd_30^_?Er$3;4LrZ7-hx1CI5HFJY-R__%ye{q2H}M!Dpt@lmb* zCj3%={nP!`Q44OI!&04z55Gqdi;pem+3`kC=xLWZ4;?N(-g5KJ!Rs&Z$3Yf87ax!9 z3w)2h{0sQF)@?7I{}rCSiMOF^8+_F4slQ$D@r3aS_V}Y*{T=s9{q;!qH-}o_Bfd{t zeB^o*vH196x`z)huA_$#cR#q&EfCL75|J9RRJ^eh|7m6+(|1^;>>X0^@xj&kb-pWLgLWX25u*$R;M_O17|ZaUZER31}ii^sj+$WV5Z zI(6eHBag4w>uW6kFFwFn*Gr(fju)}sFZLAPFmbzy^(*?Ae&NO^CPj#HE{KTiZ9=YNsQOJ3sU=*N3ZYvBodd zLRU&!6+ozv3h*H*N%&*%g>)TkF#{(HhR!#(_qlL&nopyHC)iR=D#Aaz^2hQwAjg^KRtfD<(yXP|BbrFA3OhP)l9%NqSXo|$MZk!kr}W!h|>@Zi@V^0Cgl&^ z6#j|?U)`VdD|W3(qySFcH;8P(=mPT*Gh13wTu3 z^*5YI!bJin2q-E_&_Ri!5(P>sN(LryMkbop8?~rd_*1MXR1%=|LST}R<1oP6QnW21 zDpGAJQpJFXkZ=oX5f!BfUV_~AI9!522y)5$`>lP>nMoKf|9;=|eLQ5&Is3Zy+H0@9 z_F8N2{giy@JxsjO@uN?V>%DmqDzMao_Cf%u$D~((gF8u={lM@wz^_99POr{D4@Ggy zZ1gczDA{iN7@9MT;tXU%XDxe@IiHKm`6vR|5BDLb@*)osH?)+_1~8CmDoV zJ)H*3ee04DMw){RD|IfeD?!U%#=|J#9wtO-+@yvP7U8)%PA^e^p23<94U2WqufS^2 zL2CMA$Qy3e9Cd-kJ+cuj?kiMD^#2A)g0~&SiP=EqgG?W4rSPRLw-dnA2w}IiBP>{z zT;q<{OSn4D9ji?vT9LA{0uUfoAvR95dWEH=q7+nlLWxpCU2G@6mW75}6~nNuF2XHC z=wB>|=Z%91W#n!_%ty$eNQFWpL?Kn+;r5x>(gJNZ+%jq1{>iZ>VwjO$$Bo{4zsoNt zG7Z*9AE=A%1n;tdaQnL?_c0t8!N_BEIybquT{ODwx#!&6TO>ijL_~JhInl?=xl-pW zc?CJ6LsfoTp{+M+g-FJJ2HK*FgBsZmv}JcYX$#Dyi%$BiPHHmgHB5S2TvBINXaPEk z_QQ5s@bDMeXX+xU$E!29WM(-GDG8YY`Auzb(XOlvsB)oVzDi2fmMS?9x^~J7h)OBE zuqHS~Caq{EHF<+=H85eef$tU2OHR!BpLa2YT7B699odIt;vI+D9Qm;H5lw8N3ry~l z#(9{Qw>h5ac*0|~AH4Cs9q`P+GXqboUA>Tw`@wn8+a1r|ctQo*F9**YJWV`JJmGNU z9gOEtJcr^r63>x%j=^&bo)hq#V1_0a36PWI5t>ZZxM@w|BQ%)}wI=abmf(6e-8@v_ zg)cSQ<-Ld`jeXv#6BG#1eb9`=yij!d`8xf(!;^}V)t@)QkpvSGcv!0!TSH9QvgYU+ zFQB#kU)=mj7pPyv<$vEm{uj=f{~A00#H6AN)TMFx|Je@t2cI+l?uVtl?@mujYNEdU zI0k>-#mL{`ocZVL{2=++ahYp6BJ;M@4H~c8JI=0e1c*#M8kc1Qv%KCgON^NCrt|l* z^8?>0DzkC^k>|```;cxg7<8(NY>2h@pI4&(uIJ2OZs&g@sVG&w5tsk*PROrTHE41I z_{Z7#|B+PGR6QD(e_v;||D5@Ix%DSCQKX{5#hE2d`uM z&zZm6&Oa=vsGWKvF8|}#v;CFlY`>jAUpS+#zKWF}4cK%UGMOUiK`20C+|2gy5KA_wGKS@PxRpf(M`(NzG z_MbCtpSIW)Rzd&iv(eek_G9Rd2-Q59hP}@1L{%cK*kbk}g+|#^rzf zXKepD^Y^mzqyH{dnQ{3yF#pJN=C2*B+mHG$R*|Y$`@d%XuIJ2OZs*7Nxm>*wm;Z*l z+5Y9{Y`>j0~6c0&@y*WNGf zNB+GD<2SqUYaUFLzub+V{ZOL#S#Es6!wut|^hSmDShGFL4T&mnz{Z{IShSrE;v|wd|HORe{pwyRSk4TFh>z1k zK8DhI;eW8$(dH+d9{szn{z=E!uqU6AqoiZUDUO)I)-AxfCCQ!ij)sZDYD;yEFXi&!Bz__|x0!TxzqB6LmM?4m0t z9$ilmU43Hc!uo;@C?lx2Vv+A1@qT6ueXIeNlot`;vtei+F_gnXuZ^qVy`IPhoWMGl z5GRktRq-bB5hr5kx(ZEoaAMLs(LqTZHO27b_+(GLTM(3I{TnWoetPL`R2>Myu)!&T z>>cC#_%pxCs*Rz9L%-}s(hT2PBc(sw?kE0mS}z{R2)E}!oiNIhzX@dT9=FK>C!;|Z zoes~vUhRQ9ffWAZ&jlh-^_I`wZW;qWLZkRPbh#KUAGImGIR|%hztOLf*8b5vNBxQT z1DZkaLNeI;^e0!C$*cU?mCPX}ZbAM45RC;Zl9=I?wFtm!vZ7!e4$`CF<51o> zfpsiS!Z14J;3Sq){>uGL`Wxjb-E(m(SJBR9K5IwOp=L(;HsjST#vdvws+*Ph01MFb z@C`tu71g-<4(G`T!CPPRKpHHeVP+K$3NLTcvhCXW8+0&D!8lP5wJ)8B(S#!~H`Vns z%F}M{jH`f#c%BIEC}LQ&1++{6tY2BjoBcQnK9F5!AG(u@T%?RK94*a(QKu^=*j=G7 zzSi^*+rRSc&+OsCn+~xH1KcHkeB7`3M;V+IG{0?aw~0g7X%O5e&>%88+(!q7p#FSC z9N)DWuFZZm-~9*qZVwKG8{Lu@!>vqnql6_RZ9f$2v?|7bNf7XV&DS&(PM!LThVz?_ zpXJ66e;`r(I5$4={C%GrpO$d`?d8VrOc;N$8^1i^{9JpN3;)nW@NIVEhd+`izTAy3 zNGN~Ia^rh6n*TL^#<}q)8kO(J2lv=1;0xOVlO?r!kPk@4_-D%n)v5_t+4DsV3FA=V zPw;J|4;*8(nKA|`@j4W*L-lJuUi0;94qkKQwV9EASGtjYe}<9%aCf84OPD>ddF6R( z<%~Ad)8#QG2@i)58oMtxynoRIBJ)YB=-QQo$3 z_Be3850Ka`LJ=t~ z4#K%?E|`8knSR?qjOh`vJ#A?wWGe(98t5j?qYvI$FQYc+fj|(gS&w9=(HoXk7YsWgkhUX3maq)jEKO56W0cSTJcPl_)e_i9kFWhlv)HfIpy*VB(L~86AmGK~_yNL`o6UV*oHYOB5=$xNB@QF^Ku$jZ&fJjLvaGw1p7rtw85QGOQ_8!j@ z4Mf5{9l_csf~2i4wH$am~sn# zt22_t=DGy=a0;bS+zch=g~vAs8b$50MP*<`Ne9s!a1!3-Q`99TN#Len^j8k49KYr3 zgpK~7>1FLukb3-3UA-TlUA3m;)L$q)7}H?4hGlb5b3gLnEFRq3xE+T*VE3IGf}LKt zyEoXeC%;{U_18!ei4T+?p63TNB}t zg@N!w&0mk0!F`yua34Y^8Tt_MaN`Y08H|N;B1=O#x-p9HHSQkzO&H*-;0$MJT>0#n78o3_Ssc(w7n$p zah)>_`^X7$Esm*dh;0XCw|E?Ur?K7F46kT}-D~4PCW~~ii`>ZPfP7G$YL=oY6i|{v zzqtxcK;PmNP_o%r-zwih4wYbC@};#$H@EmEnyQJ1&DEs(b`8gvOUU!=&!IZ@PxzEK zHoFWT*m!G{%!J@)*Ia-wDKatW(Fo|L&f0aSFngtFuMJSR%GjtH-?y@ojIqb=qN(G41`TJkIXXo$5D5@cUf1#tx zRId=gD88~rBBP~*xcvPD<^uBfIM&YLkpXx`FMwQN8kOqv&S&TA<4Y+?|AIAK8moQ| zO4rkiQ~gv4ajSpktkv5x^I+8NAgeJi*Znmv$LX)V_cZJ;9eOfC8&I_#4yI-{tsp;xMzrRTWy7&P@r)%zzue=y6d$aeFY@ZBXe3-f6#?svkJW4 z$ubUN5$iO|y)|6JJ6~spJz))YMM6#L{&UL@+DT-iuIed;uOqM*3h2hP1?W1^B)5vz zsV-D*VxpK-38n#i$2GX{IlYwcM*1}9+3}bK@feRez4>TUI$R#pxR1?fGpkr0b9h^z zkv`v)ule)jYvDcewQ!LbejC;5YA~$q9Y%@vxWti1-(jy%#FQfJFD~Z0 zV6hpOpT{B>|5De3iIXL*Cac4`D+9tMH!K}O$wvAVL38?aL38@^g68y>1kG)l8g1@O zH`?5nVYGRuJCX{z+YHFTtH$+!fOl!S?q$>ZUOmDZbm6m5xIg(D9gQ)`=pUkWoZCxY zLk>B;Igov%ptX)9fS+L<8Av_^5+Zo_{J#v|R<#PF*e?92KuYvMq#*isb2(Nu#i0nV zzT5y(k?gvx>%$#Xm~`zOR9aDvP7;}(F#38NH3xv9Z70=2sve~_A#$H0I~4& z)hzCP)5e#f>ckbINqUcj7(8`x!QkMjOK_S4x=zl4q82$5aua{L^Cu&C(rZkuLE--J z5{@5$q4uCX*w(EfQeS4UrvvQiA5Bi(Shz_p9F6R-I70pL@tpP1*x)QW-+L}~J;`E~bSuvi|IdbuQoZKiV+;`{k({d|ezkoh@)nJx(fgx?&({N~8?F6BW=K&?tgL*65SMgGH(VQZyT zjG2$sq|r5XG_1QuSG3f)f4z~(CqBB{AKm$pV}IajqcT4ZZ*P($aD!@wcWDl4kYV?u z`ZH7{DEzAbs_;ow98tiQ`Z)^M<;s-*qw=EMWxAeYszBA%%kfsS7NY{@4?l(M&HE}x ziuMI(ERpYOeta?Fcuo{<*Mvbno*(P@+H5Dj08pPEzuAos{5(;7xf}0+!8$%a`8}mA z^+!92L&c9_JDgB)v8N8W%f)TJ*O8!FUQSL_sorjvjJ>WYxCsD(wb)+Bz&R4wr0^C7 zCQC}O2#-Kdo1v*v5^+(A%JkF(1sZDS6laEU2!V9bR@IB?QN#)<7Yt*S+5~hj+akUA zrMh?zPBmP}5$TS{-iT5k_ZB?4lVT%##a-H7v865bB@`tdLivzJ9DQspcy5yw6p zztn_&7o$1eK3wXSn3kYKZ6BVD;+*MNt5iq=r^{m(>NbFpsCQjPk|WlsFxrT=z;^<+iL=Xu{0UqN{Cdz552y zuSur+*$tVe{K;5?95=I1nMS`;u+bFaKwFB{=+Jk5;&f=nod z!f#~0K7f0_Yz}%p_Sm`(vGz^H`V+FY$jRTD*+<7!FU4pRzl<|jgf+h@)z#A9XBmkt@OYUmR4mu^aDe z#1C}+wcP~^sJ~-l`DJMaC|`YHCjh^A4l|t1FFK*E3HXKA(Z}{?*1zbiGOo1!H{Qp= zm^>AU0^u$@Aj1a2H|#}`(E%}rn~>I^v8z;94U1zR3Cy|UdT|g2TC%VB_E{T9>iY^7 z+VinUt*AOj)-+OL{zIsH0i7d;Fhd(R>+?FR2Rh*ZyBC%ON2CGeBd;)^Hp~u9SgQ_b z${Er;{;L?qG?QvHiW|r)gNeTfZqj`dO6~nGci83&zxpdtcOLvI5RcD;sw zZWp@z>R+($a}^YB^=rRwK<_j1uJ*rB zF{C}6W8JpMZF&QTyWfIx-oYapwd=>!<;^)@`1Uf2 ziDPaUJD;h%fuQkE;v`POBkOgc)rH+`Xlxl;Yj=tkx}BG={((|iKKwE8RB3=G0SM;vzLa_3Y-63ug4gUXiLa{H@oqP<+XA*KC!$u%Z*PguZ?r#8_R1N zzWdzxKx6)VKPaIq`NYk38d+efotUFS9oUV_Y=j?EpCHT(0W0-`$Cxwz?@0`wsrYB}cd%eOB;OI32r&hh;BflFOWvD+_h>ryx?cC*T)w8h_ftGbg3B%dmV@hv zd&;>$bfqdUO0myKk;6vzWZLxD`1C}qxAkZmio(=TB*WMS$hXHp9*iR*`XCl|m(L1@ z&^`}!_W;xxtZQQgPa*=VR}2}Phc=3qL>Q_RL3kS=h`}Vgt}qjA>JzN{X~9o#n~Mwu zeS>uqj1sPXy(@C?TT(xI0)_pTrXmefCs z^Q}h3ZWP1J%A;;%>*wdEL|OYL!j?5b;#|EGHSZ!p@(W)0cp?gyw5tcw88k5%4XT^I z6g%vDJ7k0Cz8(qJGZ$mLy1=|bH< z_PApra`o>`x@zQF9|eDGQgB7^#|sK>5clzt&kA8o`BF{h^uMlhcavyyoP3D$v7gOk zaHs{k7x!Ja;yH2nehS~A?$%EnLKI6tRz>u%ro-6$hH+{VnCGr_dUYg@gLIbRGM`UF z{C}Z;7z_}=8f76d1eBo1;&))=_E>Z#zjz&l`VIu)EI&50Q3(_fFT2up4?yiS@oWkR zXMVC9`{Fe=1KAkwd^Y;^Si|GjAEOidgHPa;WVbKPZdFV!L?>YNp(l0q;G|filG&&R zUQ3$!h6#&DE) z1>)exnZKXzA_!_Qe>VpN&idoF1peR?pwiP*^eG&s7zpR2IMXy8!nXni_5$QH2bwcG zgxm9+FmnF;65S#JEOM(Y=MJdU-KXoi*wyuvNgw%=WX zX1eyf-=KZcu7oBhS03rjv38{{^b9icleWI1Ydx5z%wG#R?IfDJxZwEoxpPckvHdKY zv28||I3z)dw!RX@IsFrxy-w#B4_xi=i{7urhRy#IePvg|WRItAE~Ibazz?)N5wBqD z-www{*56P-B6bneSEd4Jr*UUv7p{HK<`<7&m55*Tofm6(Jij=DzQSZn?Dl=t1S16U zf82U4&foO*X|YCqhFoW57qR^#dr+uT!BcoosIRc~yxZdNHkwsnMirIrcb(%=l$cvI zu%Bj-q65g)Sl-`4iQ_E$N6ZtXM#_R8Bc;PD?ENF}0R{hSeZ{&AhhNN0$S>x*@uL#* zi&8hmeWQ-^jXj=DV56Jn zY?B#!W`5E3mDtc4&;CdtUc~st7gxp)hBIn6W8=Z*7q=nb>DmHgZmi+w!7n<*wQm9D zf;01rIdP481-Z_XUo7DSVVLs6W_1)OYExIge?<~bmu=M(YysNFUf>=$61FmCG1cR5xZO()ryOk29-+h_g-nI_>*j9z~wak_P{&FY%4p_C>-gmIg3F*tR z+bU7|QYZb}cKSkWdm{YUeU&KveNOtIoxbNVN#E;NiPK->q`%uve`L6%uVuCb@Yh~y zxA$rX{t=SC{C^UsU+1JhgyV+U-olZRe%!AUr=R7dUu~!F`5Q^!>(RvNM>y&KXs18& zn53^|wgm9^a?%g8)4%h$q%R+pIDJzm{Vfjsqb2>gClaUM+`*xL2maqm`d(uar=Rbn z-vkr8(EpQ?zLwb%z(3AO|Eit-odQW;J~nas0Z#g{cKX6`l73ua;`A3g>HFL1drpw_ zy~Za_zxxu0{vG&#C+TaMEdl)HPWm0NtPA}=CF#q5pE!M~lm2Zxec>~be%#ZE)8FT$ z58CN_PL%Y$o=u$o8l4^^Dy1mVdabD&|5d{H-4{#yk$>@|METdb@lz)!il6Vs4-Y1a zFLmSl6eo%wq2p^)|F}fsGqqIO+q5KcdwV(QhuP`ZPLcHMm@PqnHFeV8Vy7>$BzzNsyW%Zy+J*5luAGNZAR43<+W1%2@4k-jaM8lOum2j#zJX>ZnIBc>s{u2dxNyYE7)W>T@!S=tY)nXcVW4?X9Z1xHB8Qwi4aNPjBX1*iP`1=lIY-t*SmeAkqYn znt-MyfZaiqm}Pqx8U|kgQR7#to6!Vk$=Gl`96LkvPF=pw+90l@AE}c^AfRX`-_P($ zAF1E7zN+xR`cTiiVc>@g`uP4O`*5u-lywWyN9c~D>yn`5EqILB8v*@~wF;XZ)NhXn zcK?15zL(Z?-5IUAbxTc(e1Ntw`{)MQ?;@VDjnmHl^B*Mpd(rspw`C#wbI9%_v#a*0 z{y$4~`~T?AQhea-1a@J|p-?BGs^2;x-kqQ8K@ zuYHHqix}23JEHUt4e~1Z7YMeAVD;Bim4DvJJ(+4Y zPWxnMVdx&jD>}z#hNfsTgq;rGFm^IfcUxoI+&)+rp=3rh{Ivl*S2JSl+=;3k5XIOT z$4_`V#obE>lc78q2cf1PZjQYv24TMX@t_OgT^hoG8G1<|B)^FwmKH@&6c5@LXkku_ zSrTJJruxEu%k9U6b~PIgXvKitD-PPh4LrMq-W`7j0r~HSO*K$?V`88mAwYim|F}ra zR|5euxE2QdvZM^O@+kNU$XXQmA;bZ?fIsvm+kiBx4A*P;r487^m!J)iuaGb=`~(_V zXI82{MlpG|e>r~mFFj`cA2GvRssod1%w*ZcefjD2bwGVIqg5MdPi`u6>xs(ll1 zjoxbD6+pi({1$e6m^BWn3)n7)suCI5!EhY@`=vSzpO&Z>_Rc%a&_76+EMg^UIR7GE z&X0|*|3T$41Gy;Jl$Ov&PkX3s;Qkp>h$@4l3R?kG605h?1u!AC*kTlKBUCv2`q$)? zYfwSB-C7bmI5GvU$(tz+9kTas{wJ)mA}CA6x+~EK>Ijgvyud3JPC} z;;ztX{IZK+#tX4_&8m2JJwmHWnFj?XSt+d@`iQ%n+o5NN|MW&mbzMsvO*ml)bU@R# z+Kg%HeGJ9=;8KCvDE<|Uupo@!Qd~Wmlr#a`x(}*FNEOIFV3gz{(jPp5eV(`4$(~Ke zF~?sbr5OxvL5^gjrhRMzksgH?M4ftiI0Upi zk!M*4)E_l);~m>Nvm)HCD7dOE(4u!g=0TXBdk79v(gdiQI|qgZi90j=Ebjh(Pj@@S z*&mg4Un;rz_^O~Z5ye(XO5)#V9f@#rid0gZ!G;Cb(cElPjGqBIE3O~*&q)(P88*^>qH>3!wth~T-0?5{k}myHN5!|>FW1GF z3Jikm&ZA!r{~8sPqg-D|kih$?%eXdah%XM_cSjN5*qnpd$R=zZXn-4ZGN?cm+>hb- zs8G?Hgq9f4(B9!d3k*CPjKjcXXT`wX05CcsQB`jYAv}dmeAX^_)8Sr_1|Dxfp!66n zk&LKsgj9@@PjPduKY5?C--{{KMUSV#OJaBA9mF-rKtY zX^q{KgJ_D^+8?X8(VT(MbDundXeDlSH34zxH_46i#;-w3vxs% zskAxAGR_a`!MDMWL(@5>V4!r>`|z?47F-Oy!hx6Q5R*doJmOE--*W*Re7Gs^^!>|v zzh(x;iTWzV5s>WrnLkq2?xduZWM;vWU5qI>m65Wn_>De>clZ89f0Zl~PzX4AOEng` zyw((%PlKy*D8WkvQe;GIkt3h;R!t#8Wyz7{;NKJ$uRtzL@&3>hQXaROUxZ`gceBkm*I-_A>+#9z>0~-*$ z)m|(RBXbZO%ls#NN#>GJEV(VSypQCllEp1h4ZNtW9l#P>J3TjGi!o% z&5fx85n?yhYaNC_!pUVEieVK68Q#$IL`UWic}@PthRJ@I!TQvRG!|Ahg-rl0;@Ttp zX4RuL3_;uW;u~MK1o92OY{c8$fPrfhv+9XEp#8(7s_Y-E8Y8d@opR@@z_5xy<|=;^ z={3^F*I1Z=I0%W|=|#TedPpaIP&!^u;WbOYp2RB-rC{g@ykhelUytJzPPlyi5wCD; z;OjBG!X%5==5@(vk|TyiJcuXiuov`7y!t~;{h>QkO=}P?5$WICrr=h{3F_a%R^y+s zq<{a)>U3j0@jDrVHz{n^5u1grgU7II^b3RpE0k3^qO!922y0OPm7MV-EASJKy@mLW zVQ((Jbf?e4o89)nuyp~Py4Xa{z zV$+0O8Vi2}g(I2M-O-kQaGZc8(ycHa;V$v6N9v(}iMaSbe-&}D=W&ROWf&>Z4$7xv z2I-i~8FLlK(2%#FgtDFeoC_Yhg2_c+88KrHt{<2SZgHl+ZxPQs7S9x4fw+_Sy9L~K z9{!wkt9FTl7U`Q)9c$dT*2JFUD%tmg4u;cT*3S{bpOc8 z@0w)QA5EPKQ+5&vDGl*g{3@8O6(hM(`3<0qj~;l?OT{SAAOV3w!7s%Pk-S%wY#0zFaRDRO-(Q zw1_khu?X_#}TNnFBK2(#+wRa`jFnpp!S^*WXs`S&hfN= z{s!m_l@T@^+}QA1KR3vQkSmTG{!{}6_%u2{N=3NQl;I*7`0Y{4;Q{83hFRX(d)!g*?QdO)2wACTl(56c1yE$DBT1uG4_#^1MeX zbva%(eLe#HNS{$#iE9w?t7t~i`W(8p2dijlJ?U34; z_LoYQ;F`qd30GeVf8~6(`QLTYX2-$@?b1ikZqNC>t1vayqgxv*n_ht6i{TPnn$F55 z+)HHiN2)+J4s|Nhk>;=>K|rvgc_6#SnDVFuABBgNU;{ZOZK9UkD7N{u@}dMYgvCd* z$V?Oq=2O@*YadleCEbwOR=63(m*K;29mbKBQW8~6u*w3UFV&4wWMQkIoQMSxbw5Zo zxB_mPlvm+(EUB6!74&q(GVAZcU>M|Bd__8)dhGW)%UBq2%R4R>n_4P1mwvO(3K{Z%F#NFC^uAJp5gI{Hws zQVvG`)1dPWio)olcBNF#cSl+3rN4u3A^LqSD@5KC>PqN zKYOv%E1VboKMh)Rr-tE1(I@?&ZbDkY2DR{FmdiWR7Wczrl8Ha%KOPJehx-&X81LE< za^NXJPrcuT9_*5{Gz-HlYpPU2TFXC!B+C zBU(leW=3#$nIpWIi_kYD;>bF@Q1;u2Rh2(9&BUixf{+#$;*08ogGR}#c*U&D_{bmd zBRn#*##$F1a0&~&x~9gIA_RM_at_tVX#8--Xp(~K#t@MYBmtFinPg)M3fp+Inbp5*WG-IqW$a(Ww@SVZ3)7HuG<6*>|QDv(m;+_$4^DGC*~R!25+&QE>l z1Cf@ly%B^JPOZ5q^^DSkBQ`9UhNM+;(mQI4lM=WnJd>0_{uIPo^*p46hU0r-jkH+r z^`eRHK}=B?&+ltO-GXKpJjo{CL_V?z%Ltq$&e!%J2B+N+&T~QIZB-H*U9zpPEkt@e z(xN|68$Okwf(&NL|A3@6p%VpsuT_B{YREpsdXc(Kza{oLyzUc@7Na7kulRJUVELSiQ0}#IAc^ zh8kpB!;@z_zXCn}zh!hr)NoWy&d}43H3q^2gxkS5hJ#S9Hjsh~X3qAch;o?;?<-^BTG|UKFoU!sP?|SN<0*#9dT-+X#cODtYL;9Y<)S0!7^T%93!VbGPFl^dh zk@!j#Cp=mqaG&#nRC=xzK2ueC7zN{hQZy&HqJ}R8X5%cJbXY%gzK5LAKEV~tmQ;`y z2(5S>#iFLdSyi%-WVj$FngQXFk(kC=HFJltc#Kskoe}fJbH|HgZx;Q`vb{E!+nC2u zAdGGVCM|6#4K5WxAWVxqUlm*`ZNMqv0@Y4j#Z6rS)$Zo7>D34{lVOGW4rjir{7*S5 zjN)mCR9C+!BjY;U4h-9U*MDJTVYoDJyA@~?Ng9fFK z@syRGKA%!GMIOP+0UPX+bp%4-^}fq$2rFEhAphUP^Y_bL1%?u0r^R&0v#j45w)Y*8Mah0S(dcj$9R1RKn4KgfZ2CywnZ`#bL zCcG=eT5wB*rh8ODdKQB%P6i>i8|G54FV*Nhgz~GV2z}tqYt=@!vqrm_`e;lxPwgR1 zw{n}hYla7;17F^%Tm+#ABezC6gJ))E2&yOl2@^e%2$l<|yHi>4 z4hdMt&ESq2tTOfZg=kLgAC7lLgS)7O3sLdai4$#6`2;If`30PUuE}5flfOc5N~PZ5 z9)rXCB9X~(PG+6T!qPd{dIq9>4Xthstimpb(`Px1h{%{yS7L_5&EJGgtpe7T;sw)5 z+CWe&|P_(rUr@f&6w0w<|GvnlI%^U=Ku-2_bE=x0{ zAnaY%9P){(Qg$q<4@(w@q9SkMn-cev$PxgojD}i^g=@%#^;Nh{h8!0efQiRzTd-P> zpWw5~$$+B2$*T8dm$Q<>;q0O5tA+3eqdbyO9tCVpnQC_}=J-BYShLm__?;|S%rZo> z6n2SafhM03zFFm%$@1)I>bG6miQ+TN1MT! z^8YvCn^2xhEO@ccixAnR`yQJUI}TFcQd+U4FUQ943_Avj%Tb{ldX!Pbi&x!E$ zrMRZsLgzE8Xf5Zth0c`tTO@PmOxPZj>MJ_(X}Ygdf4jz>^-LU5L2?8+v|6wN66;g; z%%$hwGtXW>sFM%|B7a9#)&FYBxNhZ#A?r1)`~uN4I+ZW(g5(r><163DB%FAaN_mw$ zqo6m%-d~&zf~NpM);`X?_r)f{=&f1%q5~RFmp`b7u%3-e<%yei%Q<3DTj3vK)2@iY zB8*S3+Zrd`uG0^QrT?v+e$c3d>Erz8+UQI^ z>m%Ejxq_1leN>OBKY>y}0$9Ha34lJDsh_=xu1i&07vk&^Isk*|A11JmZpVb0UdneI zKFyUZ4xer(k2xMb+LYcSk7>i@F{?lxbBg&$pWj2i=FgO`g+BROI8Pp>i{&wGjXY+3 zE{{1o0Kl&foceERW*pgy93H2xu`QDLNwh;UacM7iKu7KD8 z4dz$>1N3E8n^v*s6Q-Jn$>IGRyj}l#I;skU7O_U$x#d)xRa?8-FiM6u<=Oyp&k)`) zB6l6Vz*fMUM12DJ0W^0jU87FPB@u&pG1bt5XZ_&;9y3^711~9~q%(@hs`XA>4;QfPri+5jmd-$ON=hYEe6vpxt|N6kzuM+@NPj0TuS>2Z|hrR{|! z;x;JiLrrV3r-&geIS}sA-5V{*YXv1wjh$LB-@03VCvf!>w-o95wenh;tS2o-p83PgEP zN_z-&UL(!qh(8QGL2!oxZ3vA-;2~7wK=2p}@aa2Nc8XveGlSbJ%;XIwK=In3cx_O^ zDbRWVbIHXToR)ybi9@^1kj5Y($dCMmT@YLg73L^?Rt~cgnjFkiqNm)hy1=#3L9-|r zuYzk$49Pelb|4gyUt`9TJT+=D><)e?Co_*megsvZ_u3nitMC@EYV*R2g|yC9>7uzQ zNx1dU*SFsLemel^%{&>NP9f7@xvv8*#SOQLNuW>%h;mKN;6mqb7#Oa!CG&hh+W*{N>Z4SUhL5$zkj}b^gwk+oBlcSLC%7g zsh$)9VWiLW2sB3eY>!9M{StT6-Hl__2jO`2CUIyIA~vlBo+6ZB6b~UHtRpeBY1(u) zXs99>5d6utX66xG8X3t#Y92NZct--00qaO0^JJh2hICvF1zj4KM$M_+2i>JS^a1D} z!`#{*+}#%?L%@WvPK6^TBA31?lZ<{7%6uFgTe%MuwLpm53+PGYl{-M(vxS;SHG{{t z46X%9=}3}uK7`qVbbFwIY--fMU?>P=A1ds|e&%2u33~&){Dri*C~@cw#RD%tijjhJ zo?~YJSa6Btz;#Z|cx9)x1@XGGnkPMBci&9?p}X%Md`R~V z*Do_Y1^Q*Sr&!XRsr!s#`m(qP4xVcFemjC{^hXDM35eOskHEwO;XXf+UfWM5Qs8s& zDx*PN3Zb1t0TXSd0%D78#Oh}}*Jv5608Vu{)B}ZAMQ;?;3dXtqDwz!tF!DG22qbE*MNPJEztu-s7=!@BQU+8CiU!)^#a5cI^M@1ov{15=+qW?H8s964Cq*0Xn z02Tv(aCfyCzH6^J=~xbFO5Ccjid%rLGJ|_6oFVI7nS=b^%qpC%%{d2TJd`sErUZ=h zoPrx=jyE&cV#V$^27*)55W;iYlzIg9=&=J6I5TJNw~hw)pt-n~XjetSARxnSDGcpu zE=)o!oU9y9xR`s;;>C80^%G`rZ}I|Xl30i4>TnvdFS1)$r}jV#ETsgJfzm)GhM&(f zA&`k{N2;Q!0Wg;dCbmF^A*-PAoU#V@3#Xk#OOw1!rb_ARmhap03Iwl_+0KE2F)SZt8q|(vBjeYVncJs`O&`m36$( z%Myv5t1*ptRtLa%n&BPthqGZ|n6g4f99m>3W;$%p%7nU(c@JAdm% z$YS85vv8}CEm|^wWpqB)Vo(Y{=pVw5=KQFzKhnHcFZZm$M~kP@p*xCjRVQtQhc7_u zOI|Lr!^rV{eAa6CJYNk-n26W66ZkW?;OeFV1y+e+pMwL#;dz@AJl@+V{iomq5EFFi zwt#h#j0*-rv+`u$&Z<#cA&LJI`chx4sa30&NOv4J!zs6zm37SHw6oINqP5#beB2pp zB)?*<6ZhjPIsG6%*tKeO#_?E-{f)+Nd~l#6&D=Xxki(P*OQlK zZNnZCWAddaCAhZ6QtquU0muoHs>q8#kW+l}Y%_%`SAO@fzYpVi$ZL$ALV1@j(^GVg z=iBM$OCFu_rJefu{O5Qg;l$}{?3XEv855c!OF&VTw`w3jwR#WV6rc_{R9d5++X#eE z>P48$+XXq!l&w7zzMMxX0h-b+nn2+Ds0aZmhDDnc4MQDfc&dlsa@_n8 zXp2n10vkK0jHzAFPJd`Xb7RREA)Z|eHSx4^!*evn*lYA7@LZ5PgD6h5z(>|LTw;w` z*_gZ)Ut;S&MFPaP6T6Ssm)Co${Xq$k;{{zAM?Mll(Ya71%;^23Ez2Q6Svp*c0iXP5{vgm7naC>!d7c>bAJVmIC3V{p^Ha>@< zcr!FpulT}uw5QZC5@ANyVnH3Im(&r)X#H`>m{{x~4z0CfXAC|-5(rje!A+h65c1(> zu4Nj#8AG;1TBgaoz%!T{V~8tVB1PU`lWt{o(HX&5T zX&-N6LS>xu@s<-R;|RiA@8`<4;H@m+>GoXNmweMw#dAel`Qh_i>%*ijtEs`kx(mb! z)9An6tlW;ur+Qu>Hf&Z)jvDTf0>yIqyxq|xQ6MSNo?&(%meByO^*Xj;@ z(&>m%*W#U8)UxF#{=vuW6ASPFqm#HV-$BWTs=7|k> zhVpN+W}f(p8G{wKfYKy$aP@}DXj5x;pRPgySl>_WGZ)=hRov%wJb*PMuS)mhHcZHc z$&f2>k;-zxlu>eu;(2(chZ!gjq0cdykHQt0kheQu)-=hNy~Fy3=dX?N8Uqgp*KGg} zC!NY^jzXcGnP5yFhYG}c8N@}tDaPbySs!Ms!NFq}h`ea@ujCjQgFfKCs><(DvbM!! z)){7Kc}GpV%R6eYI}+>+6jPO4B!w;S*j<{oyyGHiSI4DJ0&iDI5nP=C%|^bNy|r+W zwHg{GvvLPJo68kxS$Lj|i^xQvm{kTI?7~~j>{kR;pfEd?evQpZ zpCBqPyo6H^#3J=P7A|oUk6#vz&g>Jc^I`%$D$UU8xd`^HXUjVFzy{E{2!-BFFT8_$ zDhfM92g38?0-(tZ&-O4nt5&Z=g+fIT+p3;?k+R@%)XS*}ryqEeSK4zDwGt94bCOQa zN;g&sJ*8IdXivJ*tsh|=SP$XI`9LUf;fCrDgjU3d+8-Jt$gN+r+;R`V;eQ);Hql&O&L-J!df<=h?|2R*^gBI_7aGoUXcx%3 z)nBWac?`$l3!s!O)Eu~ZCfz`|TZ(_uT28tS3yH&@Hx%euq^>~;Ruj~o9A4>aZvwe$GVXIP8Z`L2f;qGtUEQWh$0?zd7rzVPVw{X&8A zB6R@4{Vd-NN&K9vs_oAdepaYY?a$`?tX8Y+&$ND_v~soB{z`}Nazi@;k{t1cC${rh zKh}O$^qm3G?R^NeNtq{uyNp0MH(A|`9NYsR+*5Uh2Zm~d&2`WOiqhN z`Ew$$wZx0aYlD*UCc@gF3(AHyDeIqdN4VSd(6F>OLufkeLR|vM$KiGlHPxD~#6)kS zWVgs6Q*G%L#cJw(I2@bKmT@*yt;XRV>bV4;G_Fn+!mlV39b|9(#93NhK%ukn|p#9I59UKb>X87L~RNcI*c|@HxqhM!rRqH}q_W@W2znx@N`{EHRQG+j~Rf zpbe#yjV$l}KUhNgL-#e;_l7EZVue#E2PQ#$vwpCSZTjB&V$-hV&6)eNH;=n0{KUmj z`G(E<>Kka~hiBML(&Y2K;>zZ{K zT3d{+dL0k_^?F1!&|i<3F|wwnr8{kfxBMWFp%>^q~jivhcsF^ilIykSY(OQD$q+@``WX$c>h!sWZ7zRoltH0lT1b0&^NJz*P7Tt z=A(z(jcq2X@rfNc6J6?-3*Q!JCu8J#R52qippilPzNI<9tSVV6kk_WZH3H`T!W3K> zag!+JV11+qLw6(sBhC82J;|W|YfWKHh8p96JR16axPp64&O@v1@_IVu{px(nJ2cNO z?}9ty%hUb>y$R#?H3FlNS>VZtISqv7lbpqAVCd_dj2b!(bet|u1FvC^QUj-fdHb8f z_Y@|Z)_ zi_vC&J9#Yh$fLXm9`GbE@c^+oo&-i$G#9w-t=$P4=%eb0mJe48TJo$7vcHaW2mgWD zxvn0nhN*_X=K2rxK?Q-(T-F2s0jIjGZQk0oa3E;cocsg$glX6eFCzG$D>ZWt6$%W^ zDfnsDHk^5bZA;&x2%f$X<~e2%Z35ZKDA|NTgu*7R$VaSr1pIL|7bKCz_{I_p5(k`q z9hV$%BH^Rh&&S<6?@z{Qg;I8FibB>dcoCqfUQ#7HcG9%!f+NpNYeU@CvPA}5s;dy0@#n~}d|*<)g6dE(o(WLrThZVSScSAPGS^W;0WUc?eo5VNfT^)sQ%o&t-_klj=hZuV z#RgVRI<+NTi6mArV}m9*t#e+W&laZVE$YK)Q*_zR3ivHlo44h#os*BC^B@Dw#kA&! zY04X#Catgz$+9DJV*vL`!hl7?*9T%Qat&V_%Z ziciobe4rk=A}J$)c^@t{Ad&F}@`)<4d_7ujA6;n(WV914_>^M5WqoMnP1Cd%|mBKdLz!+UkZ33U`D;>r2Xr=*u6~YiJ>dj%KO@Wq2({iP(WI z!)u;?m*F)W?;KtQ`YD{7!)vCbX=GAy@GBi_6wk(Ba}f=V$om;DNd&a-N5LH+YR6EFK|PAOzE?L3*l^*&Hymu> z-XvY0Yso_djyW7FV_U*>l57VP!s4$GYC{0gl{hNj(=;ivR)Vn#!#WVgD~)M@V0{R7 zM*e9Qba&bj=cfgT?xA75MvXx!EEigyc5tSRV)^dH#(KDGIhKN<0Y~2)GxCF8;3~yJ zhN_1^Hu5H{jt2iZn%L#hfYs5|FQHN+UAD^5^#N;loXb`Ytp2d>ZQ1R>MQ^QvSQ$0& z)^esaG=WpvWwWV7tH%6TVJ5HjSMCL7=M#!tOCB=t!zpV~8yvP|dj(Ov1}L`UqcXN5 z+i`q|rbnSOjA?bJ4arE3kx7CsGO4hcfoYKrAKz!f;p5vtGPcGl?eI~pe(@n4J{pWu zcS;6W02aH4;UO*)a<`{aw0@Iv{P&`F8UQ&h^?*nV-sdE zCuV|-Rb5(^8SVoEx8pCc54^JX(JwlmoV2S5sakBX{RMK$x@m`ja)0=7I1FebdTDPA z;h4Wbj`{+9gT@P4!g$Y1?0`=Kh-$RIzyZvXpQ8<(k9k|9dGN8t{s?$rvYy64#wae6 zL({6=FYH$o;Znz?<4d`5?eMkN{p#SoS~l!>TRiRQNY4U_nKIbmdrHp&`V*|hCE_Tj{0S7)C7$(=G9eiaHZ$QswmK(x68;3=2)-~{b!+`hS!O_nd9V}+nYzJQJuf)4otI%NNmw}sEsQPw(*91vFL;Ou4_w(CJ|Ld=3M z4SVv}xmH~3)Im}>2mnYIz$4z4=tqlvSuViu_qHqm@DL8rf;rNHsJG=1>OJt28(PR_ zcsuXQZ8^#V)ee5ZHKj4i+Y;o@F`~5(cB>9}TR!b+=Y>3trj<(5BxMJV2&P0E7uK*) zl-N-%!t&H>rFiH~3OQcQm2aOX1w3u0{SJ4Ah?-)5q=8UP^*j439m{~n?e}(w8LA$( zzd9h#-S&IO*fg!B;KiulXrrZIJxvcdk7@c$QtW$YexWvfg)5KO*e7U zhW$Z@DOhV!BZP!H^-i_sb|b~%0dpYDC@>s+4e+2%vb`M!i?@R;fW~;m+u>VbKLOX< z;XmHY1N3%q^=DYl^gs!YzeAs#?uUF?`+e5Q%9EWsZ-BqULGADGJ^USZ!QbIP%-;b{ zQNg;5IDZF7nw>XAuKv+=cHsUE+@Bk}FB^ib`prACz7&6l39cyJ8+HH(BEMwN(m@}NF{0^Kq zTp0()v?cO?_y!bvHvfk|lceMQA1FM)Aj5})KZ2rNRkWgcr!4Wus}gJLxqhkH<@#GeqInO|>(`5Gnp_&JfFx^xV!6YcN1yUa_4a zI-pc9oLlhFmX>ySvlk!|%4J8KGsJzU!whT75=Ea<;4s4725dt<+Dxa2$pi5c?-cP# zSX0Zh|w! zz1kT9x=z~}VguzM2!FFZMJSeT5#Hc;U`vZVGoTUS^Apqn$5ioy*n)TZK@bEioU|VV zKVUv8_9(=9t##TFqCY+Y)=V*zI}Q;OC?!H!Kb^7x9xCVRL$aC9Kw3{)8sKL=Y#3zcy)gcJ?K3+F(})1fjBD!knjD&rK6xBO5U z=QzCC$A96?e6Eb6u}qsApDRn{N5FH_b45-0=JVW$nF?0~0D-i9iSIIOUvQM&okF|C z3xEaO7#`d(+EiiBDtM3frFb1NIQ}cEHeO)FIl_xQjIpAI5Bw7)ur>6Kc7>w|c5%K?&Rrb&Bv-nIRTCkumr1-pd)6;2cXTmOmypwZyk z?yi4Ds}=-%p1nYsDcl6jAh<07zQcN57mKFL#l>PMTr6gx&z$DkS+)Sdr)AlMHDu%h za5<3{B7X(aU~HU;2OK$^)bzD@4hi9FarbKNYaxT-?2Z*(_0|O2*Wwd&fpLGar`+oT7icouw)Y^L_NkWgFeMn`-c>xwcW zKgUn7qPw>Eb3r^fcv4&ajs9Q6jQ$Hebx0TpYsbWR-yreN32@?H+)-0(A*!SUK%qDQ zbci_sbii27g#*A=qj&(sIXD359HBfo0IY@sz}l|ScN`|@*lxjed}QNufe_8~sK%gL z9ZbA8PA&`2mKIYLH8NluyE%x;X52E?nZ*t{s4Lu(jPz_-Qo@R zXsR6outP%l8udGi-i2f-4go~rx)fD}T?0o(f^VzTWoPmTfU%D7CLgqX5qJd1=;m1F zh(J5hRvNcQdAb`VJ}7?i%Q`-Jr`BN(?Wa( z-04Sr1~~o14}TwE3r+3GsYM-Hiu`mLfO9~x1Tl*9jM~B|aU8%TQ!HVOWAoK=iEEC0 zn0SgMjQ#>e>Q#wILj-0b*9Rdiq3H<4yyW^IjF-@M2<7_I@j-Y*LOaCyAlxM(9doUx z>4T6_WIZM>2<)1w;>BcAKnEubaY68BA2v!lp0ST!@ie2`*3cC3LMTQ~)DMCM1{zbc zR=~w(bBq%DJrbP`&lWEP`m157Jyfjj?1~ux$7lFJ-~P}g{?J{`A&g-L5eoNaZc(k5 z&<|lC>>SO!;ZYvAAxtfo-uNFxSSdI0Te{$0e*12MC&JWn!Mjffh%Z8s>ZQNnia@`G zuKFuoyDeO%zuIZ9g*5%uL7W!q%cazg(2U)L87U*z+6AkJ0kvMy4>;84h>=G}1q-RX z!JRdMfY?au_%(J%%-;s^rZ{y)VavXnrf2%fL z+oJU??wP*zuZpTg?o!hJBO}l5KXT{C_FDU_{v(4iNMxcl zl-PeHx}oXW{71ML!F;L#hmz;3OA_6md5s&txI?1&rf&SyOB2QKenk+2@`qoRD1MzA z-{E+f4&<(Gc!?qsT)7)x-JV;Tz z3AT}65?qZF!7mtVU_Hj;RB*oEqGmY2%KhB)O0&58H~sl?ve`9*IzLxIH)J?;z@2rIT3H1Qs-*xp*I)>OByTevRr&$Bq`vRCUTBZn`#Ycl z3bTXi$s(!)3l^Ta#F~FMNs2BY>${g3{^e%<39{lqy?g+L60V?l*;P!W4m5I^=D(O- z-|l&OWII5+6w@H2UHd3&eSBa8{rX_Us|)97+76|vPPQ%`GPxXUPv1wuoIW=_9&=RS zZL)LuF1{KXe{v~#HIMg)CKxgWH#{U1=6pqJS^uVqK7XG#CE`Mdzn9O3kE zFCqvB4qqx%&DYWw$^H;E_;>)ljbxhj4V;OlU#`z+e57mhXh{72~XZVdtk$>eBLWx3JVeFcJsBRK zdQ_}^H}P+np5dWY@N*rCfuMn(w2t-g^Zz6NCIb-`emWM0`1e^zdCkA?Le27@`M2$X zaPPgSu7Tc1@b6DgoVDldZ#drP7-U3WEpPj{CoG825r(=QkuVF*A3cvjq&YWM@Tm$rr2=A=+S zTb~3VFXZR#IOopw5CWWRYcUz6AS=9oUMzi)>+B1@iM5p-EEFm8^Cvr7O^ z4?9N>#lpp8M-;>Vc;c3pPk|NqmbON3%@=nY@awmcu|Q zB=bM*DAKYl*Q>}Ven>+mb>-tGk|E%M=g*1f!<>c0v%6=l^bsec010;6p0#vth|pro z@RtaG4_RRYYA;)g3~-f+pr7;kM3ZFrqn-ja@pjxSUs~Z$@=8F={W>5fV^SzH|VB$vgk3T>$bN&r*5|J|uBkp$w-O$y<=lbtdtnfsEuE6ry#mbkWh-zjwe;&Hy ziuuW^TFA9OY9HkND^N$(PGlG8xFPR*!d=)Z*i4N-B9D!cHyi4C++2y;@WSQ>0G8Di zeNRX-ABgOVpt@Q;Un4bC1Q@_kwUzqpWpmjsio~Hv1YjH0%G0s9XWZvvCUQQr5-Vs$ z@xVaF6o9uWUe)_vRbLC`Qh(G0{aD~~{D#H#juC59h=UiFY9Wnqg*S6{#PK5lh9(jh zOpi59cD!t`)j1ChM}ozxyqGjYU$ zS&e5JE#a{_?BSHRbI=<9V?4qiM9^zlf%|%hM@R^*c!WeRVw;7i5^D@u3i(kAT;%l4 zUBUhUDONnfa^ye+C;80!`bm8VUW@Mt&vuB}{cRmC_S(ZnKU~k+JFxcB3(a_hJLTKC{ANRo&_RV- z%txIF4#?^Pf8tL#q~J-skrG#TYxtUt-W_j3-k-gQ8*i#1HK~lJy zm(7fo_ewQowDx7Sb9F&+jeA0MKtX`P$^(V?17C@_SBVE5w_w2pJVHlFFwGc+!|;f6 zadspeqi_%Rbg2AhX-py&N)bjXWKLKFsc(Kd&EO3S+puKiTnbu}M4%RbCDxvZUVACS zprF5|==>3H=8%{J#Q9PS4&}pjWkf1uQw>Hc)J-2(y#XG`hOZ_$AkH8o6%Iu0cx0r) zO#R46g_r6_Mk=J^cw}It!uI-+kqQ&^BO?{Y$YZ=jDzs*iKzDpJLp)rBM`9*%D$&*o zTD!PVhzB8Ysv?=;S4eyNnu#qcevIeqA$cuGX!eVz7Lg0TK`#G&)l8|Hr0JpxpWwdw z6n-MnC*nPXzEx)J6}nK_V|X`F5DHkRv2;SU#P;>9s7^u1D2$rbMG06)yjuYaLkU*E zLe^^5#*@w0vGa=<^-#bB#2}16*WoMVCWi#vWD|egYr1e3~V@*g7pDNvI&cC{G+a}5Km|O z_XyGx5WDbf>29RGhkt+I-xS2mu<{LpxaRX4=CAerfBC%#<*fCcC>u5`46pCL#M)Hd zCv)Pai1l5yy|j|!k4Zcq)2-?OnhLG&aI(8k9aw_(T_wUirvWX#M|x;t1$&Vju>ZKy zulFA*Fh5pbcZVC_x}rfwrH2QC{IAkOHsuS}S#T%MIcA(x77<`c7K5*2Ls;nvQ;FI+ zKt}iF92wnDzQfus5wxIwx~74(-J2Lly&_AGh*}siKji@g^|IeYiH=ABtS~;-EPH6h zZYbwuoOlD32*z#-m{5GIbaq3>$GQ-?>cz(r)OgN_k0p2lZf={XnUbn_GU@rK;XlU5 zA|`B+@v)?)$oN>Tq)NueO3`_Be5_RcV#UW&ce0^{91u=qB5uweAM4yJ;Np21@e{nYSjFT z1@I^ok$svhz!1Gq0(!l{JVTf?ZccRTSbvE9>jB<}IFc3-@=C|Y3PG_3Ey(o)ETN^& zPtYTCL2ob9@v%7a0uEP#eWZVX)$3*^yVU5z;ALIv+b+5l>fl}ahd`5gMQ*I2yTPQvj#SHhoyZQ^BLh-SzQ3kxi zco^oue~phd2`ysC&y0^{(Jf^}JM=ajA8YkvTyU=8(ERW5vG(3Ygw>0WMYLLrr#F!y zB0kph@_J@`toLpwyjFZH!Vr#+H5p|>3yItDV#dc}u5f%TBK#~74F10#A8Rd{SIwCt zbdK3QyKVIOi%y>qO7GaNq4bR^rsp;AI%A+yBj606VcWRANo51;!zjSoEcySy(HiYsP8pK!PH zI;&%cnxZ3gY9v+*)cw<{D@89H=-Jv^Q};!1py69Yh#-vh3Eml*)UU6HPVml5yz!mz z37$A#IBWbb2lPSac};%{{vEkyxVD{Y`>m(yjB)!o6MuurfyXuot&A(rUu9G+4xi6d z*F7!>)CV*_tp(^jJgj#>W9@fjalf-9j4d7!YIXdxjUyKK_n<{04-2mfJOWuTh8M+? z=;84$M<`-f2Q=2#y??M#pPjM1gX3TwX1$H3EfD1}%T&5s`w#PQZ8;Cvr{_+Mi92o3h_G9lsXF}YiR zTm^4Cqdj#qF?vQ@;?4Z>sGe`%CU`Wp4G55leeg1SP3bldDON&V;ZH{mxKtWRczIUr15%WV<# zb$33oAsny)j~tr_`g;FwNc~^wYlDd>azpP(&{qKriyO|QukiU<=McDJ<3$|>XzN&A zM;+@0=l<$E!YJowZ;XZTI$Cr+Zb{4enQyhoKUgHds`j|rgJcakM!KOxG;41J*4VE$ zZ-JDr3m)W=so{XgJ*C;XsRm`c}D7wYk2-{5=#HG zNv8(vlMc_+<$aC<#;Nb=YwnHrT^H*$Vtv=edks7%c#Q<#b%|ah(RW>v*GTeRm+Uo? zeb;sL8XXaE#eJT`hm0u>pMg@ik!#>B4L>N?qcRI>BIw#<@TcDN6n);Ci45vy4GUtL ziG29d<-5PY;mZMFx9-)k-Mo(N4gbwC zM!(P932>TccuvEo!GS5?#q4u-`xa;Ph;jjcRk%VA)$@)<*!sgIeG>fU3!n~9PD)H+ z9DY(`aG6y6AwZ5hyH!Gnq~OdfYsyTA?_Sqeeyrj62G@S7MVf1qE9 z9H~THrrj6wwb3sq;|518Tj2NfXQLZE_`v_`J*g?6DhXk=k9+x9nCbyAe5j{jLGipm z6nu(~1Je;Q)n3Xiq0^7Myb>Il_kraiaBJQGZk;1G8!7shIi__v@jb3CL7fgyT|CUb z%LAh@CfE66uK_j($hOt9G2Q65(_w$O+~~IzKNUv5FXith`@A&?4>xcXJw%P5rt*P{+8c+BwU z9W%VUjfw!Q*Wo|hyAAjC8e`Il(1U=vv>e0HM9dh%Kd^wn=b7lrx>r^a;nPE6GP!y)q zKzJD-kLqNEP>EV^6oI%@bKOsu_mf=jX4oZY|6O0zn0+oyEkG{AHyy4xUt?!H8)RW% zFMSCwxn=*hm-2iZOJRHIb9nG?2;_h{h~yh=App2QlO#!h z+tbhhgk&aHy*01VKJHx(s~mkEP;*bRk6bN;x(Xlot)+{Vb4fl-;`rK2*W)k-AyJ74 z?QxN7ysfAp0wocE`6Rp0kaS?5dJ=h~XkdFaYK&PoqDqd}r-INpC+c$Q8)S(XXOr{=FKz(M z_QKRo7eZ)gJ{$4^iQsvqo8b|<~a4W12>=0`#+#yLrttE1=W}>vqPY&;GhV2Be}Eel6aKb6j382 zy#-xvH|6voFd-pkc>{uh1cqR40`n2I<`|JH19?JuQ>Z5qs?ijL{ggc$c1{5`OXJfL zq8gEHK^(lX2ETs5id^8^8qBc9pEyygP$kF+p4KsixR}S)tpYF|!-Ss*?>nowC-p7R zQvqg}zEj$WHCD-HP9Y7=qn5pjW&d)&o=iQ{3vBLWY&+DzU)iO_WDLVMq(HsZr9q_dGUI8jOXYFRp$vx&b++tO3m>b-Lb)LU@~=5q?nGY6dJJVeel-!-0Pww|^kMd8%93=Z?Ue!@Hc2YLEtnlkN7oSK!Sv zi}NOr_26!Rf(IVWAaQy8@GLfmx>&2IVieX0+=|&2*`5AHp;9N*eJFwk%R>!P)&iJ6 z66|Hv-8p*pY{5geirf1d0()$fxZlwF<-vY73;v_zb=q<0F<^AA*mp z@tK3-R8`K$SOzyt$Gr8^fB1JCX}4fee6~CfB8@f*!FqYRg5F4UXDj!gzQ2xgvh-3- z?$hk)kL%c`-uXfrx}gr=E9q+C15hPm*$%Z}VB@gHwjDc0jMNdjH|rdFOR6TA`6NUk zbB`Spc=+OY<`*HY9@W{g+*DUeUYbl1#bW~NdorK(pN4OOnH~49>cMBVpB>fy;b|Jq z$o9{w+*+yUh;<9zvL$7v;NcVAHK)J&yITS>)vnG>FPeSUE=3W>4yd1N}a>R zPfi2yZPfWSe1&F%z|Xn$;5!n9pU~aCfmQ>E22}~~qdyc13cLs4)Z7WBn@dgQY|bC|S^fTD_%Z&Ito}chRF6I+eQPNFjt0-KR)x}gGKaOS)jPj3=`qgC;B&+q;|kzB?r>xEtXnYnW)rODHEG(@BS}+8#2Vkt zcMFCr`qxF}<`B!0{?EyXq<@`V=wI@N>FAK`W_Ev^yB_>{{BlC+S2uY6)-{wK z+u-?IlTiAOdg&&9e!NQ)KqP)jrfU3j3E}70dc=iJX!us?;u+!l7_vrk({XyaOkK9ijgxfzNa9R+U{WshHQ1WVdevS}#sQ-6H z_20}F+5a2+M9}jVon880ul*~d=~?EJ$8>fzAfo*fm$5z#-!wD5b3}SC4WI7s@Nj#l z3!?k`Wba5O)%A5X>-$vl>i%9U^_lX|tj-_zRDJ%@^WOx)hFTa={&wAeYyQ(YEdDq1 zMdHiMui<}GXBYmj2mikiT*w+Ni+?7V+5K_bez)p$b)VTT`Dw}!)?>l9Qs=PdzXsrYSm)RHDKs18 z{8taY<~N=RUrq?Vr47Q@)r9ZU`!rqzz8R75#YVxm%FGuzejBA-6MvuIp%DW39+bRc z{9W?kneeSLv*UhxefVVloD)i4Sx??+{7%s6us;vOe`6H<**b@b-^2HwIsUyo(qE0= zwq}EXf4$@l!++qJDEKw~k2C4ttXJ?S{j|n+v(91Be*^8m`CLT%kLv6~|Ml9xa&1)m zf}aUyc3<4EUphvek(l$JnU3?Fdg)R6&QV;;oNGb4Gnf0m&mD*y>~p56Jug+&^^eK2 z*=F`oTieL`erOX}-|wL*!c8puO;UB;x>!7|zYBi?8Q2*azM=3qJ~Cb1_{L=as<8StU}HSotfESKRaJw~+XZ{$p>U(B zCeOZb8ivP8RJX&mhKv>c?|>qprVpt8>q!(vqm;X~;O4K!(!Ju((LFDkQVY;+jT>&O z107LT89MK7op%`X^5jJ2hw@Hi-ecGbR?S_B$42f8T|W2$_dTu^9E(=tBg3UX^G~3L z^s@tbO@+M6H!|4;Qb7sP>6#Z;ab{-7+1e3D-Hquivy5fLz7->_+Pn?I+8V%Rl*sE% z(dUx}pLuUc)U`&vV3Q;eD{IW zhk(>`EK;8d(FGN2Ci>^{9;q8Hls+DG;CopdyLtYKE9{0uwRd54oe%M%?VjoccYEFM z(`&6oG(pBA!MW@D!4t{r%t>s1UfGn-*K<>uQ>MwaJc8b%kx)E;VdefA+r!9 zg^E}Q>i4__(bDeB?~``L0Pkz&PLBlo*0(*O1bCa@Jq1;f-~CfeRYkaZ&(0LtIr31l^8Gv5J;8s!$N zyvD%+yg!+p*suhE$V}80h0%s%-DH-0_h)>EYCWH)v!`#S?wqteF^aY$L0a<(=bgwe zN%&G$PSVYqwZKYHzhJf(oN^{Ie8a(;$JO??%sI!VJL3III&$(;bmV-3lN{=Z%sIVK zR(;l7I(@6oTgkjMHCI2Q4&7;?kS55R?R$k>h;U7f+?Tj~gTRQ7YDV;v5ut2SU7tsX z$t2%^qRjk*N{<$G8kzOoN;)#mB!FAcHj@UR zjIWH{;rL|Iq%U)IN?~))Ly4f2x$=EbGB_7&0;~W~v@S1=1>;bVJ?exsihxXEY*u~b zGmezK^ap5kJ#g5HI^qg}BN-eVQ>*D~v;f(N47z4D4Qq$8@?87S!)A5xiMUU#c)1Ep zT^V=+84b@vSvGeHehizt1Ag*sxHjy0Xq3&}0zdgSd)bW~$DhG2f_Vk5`mYQc&ko4d|FE8 zG11APEo^3y-CF>W)5ky%0jYZu00_)QS-nS&l25YQymF)|pFY+$ZVvCKB8VC-pp@A9 z0kEVo#8U>!p)3;3Z;P0ETqQiLCwn}`;1N@&;k!Ofy|WxrsM1r3Wpy?FR@7lxy%Wpo zO98lJt(PFb`J)zEu#X!+QPeLRAujzqk5CUZ75vzAFSmb;UeddOT7=$f=HhH0?3qzV zlc_#xu+G1$MzHb(^ZS^8Y$(4afi4A}bdLaVzKB}*H}M1TabH4sEo2Se?;PvotSen% zQsrfxXzNDI$JzQYbvafyARhNChVLes=4WBv$MUs@dZ&r>V1S0GDIt2A5Pbqjbq`2y zfxARJyhVN*R9Xes8$KWDU$q0Lk|+L-GA`eH<@m;PxpT#a<%AjxmXSj^4^s$%lZ%91 zd&o)d3$0ox&nO>e49eim!;I}@*Sx@dltHE2bfxYtR`%VXnBY~`+a|mP8lio&&AnL2 z>o!@?%JnpI$;sV5egsbc1YJ;=5c)lYo(tk5lp}d$_J>+&IZ{M9;$goEIpheJ*tY}y(vIe%fl5mR`)60VW3}ovos3oj_jFPY96m{SRk*|pNh${wSpT2|Zzr7)Qv2x^cdAF-) z5u=oQg>@o$;S(*OUX@3kdFA|-;IV|q<4ao!8Yvu92 ztX$s^13f{7P(PS_MVk?>TId1}TdSFz!-tXeP4IEBUwu%wil7w$dPtVYFy3%gAFk9X znsu&Y(&`^tZK_s~j_i*|XE9_sBe+;vRU;{P={7N&FsK(7 zNfVV?p-biJQt7ftJ7#7S$8$g61(7q7amN1f+ZdyMMmnrV@e$RiepmqjKa>gxaFL82 zr2#3}^I-j=O@w`Krxqg>@*8bpj~BQTMb++qRAKaFy!Xa2bqhQ{0C|>VdIOVPm_`Ks z^rI6{qo`nFH^-tF$`}wmrZjsfs*H@2E|Y4N!3BLljery|NS<}e8UEExLWX_TjcxwW zn;xwDt?D~RRu4l_EI|oSC3epO&^|2YJcu0E@7uyPD5Zux$u0~5-r7F$7HAbl$~VAq z@)~4A^K_)1Z!dcVZ^&e*LyvK5g-65(!(+cqFf$7vUad{7LOs&JYP@j`gfo9kQ$=R4 z=e}B1M_LqYW7o8uiK76V%0;({smobQzEw+x)Pj~qfSE0HUyDjGjKo>4Zv=-CcnuGs z3*3j@4hmr5E#jT^#-k>|CIx-|69NY4845`OLl)_38_)xvgG5MwWEn&o)$hRBYA4?G z_~T8FKiJ3u=#5tdAP&3<5QrlZpnR*Fda{uQzFKBBP>d>RbG|T{qINu5!VXs5$`(-x zt@B|hOYuTzO7Orvpcy?dql#j1$y|&o!fmV|=`#Cdfl|i^G3kUMRTd*1-a9U6{RgsR zdFn##5rcCCy9Vqw;<@hAi~W80f)zdxM?7suOhHltq=@iifm(*`Hrs23_Q-Y2i@kRW z<%-2o3nWqrieTqfmj|th)BBsf^b?3zIQ!QIcZg)i=p>ZO_Ok9=WfL^D{&BPpwtW-7 z9MOERB?eWCf_tH$WYgEOA}oNu#)cM4zV={fAfNfvl`rTlU62KzF$gZ93Nk|NaV=#U zBTXG=1FVb@U#LFNL_5mF0`>b^`h}^5+p#AhcIg1{JC%qEI16akV8>OS0E3Qdc#sAC zSN}3)<|4ELcnnSupi->yU|SealT46kHH{Jw{JN)8&0<`w37s^;JgfR!FpFWXcdHNu zWF+oj`ELCQy@O|Y(-ZkG5S-<^mn>FRe!>O_0l`J@mG`IC1%HvBw&BK>qZ#)Gr z)*@?-vZnM#_Oc9^_XdX2#ur&{MOKP};rPusNG`g?11^H8VmZi=>K309YB0u%+=dU) z3Kt>I^VmEFzYQDxP?Ky^{Vlj!zg910BgcbJn6pl#REV5uRF_wY8xBj6z4Tt)Ka%iV z=p~7>o6RnN`G}zP9JV{@sj9m7fEin008$Y{ne$n+L_4OcuoYzm=dptD?519ooq>=i zrhQ?T7SHLdJRKl-?}(=a36&URl~* zkiZyKvoaBUuBR34gYk7_;fJ*c(+!`IqH3^wL>PKAMICAJM_s?lnP1?YO9XX*t4qCf zQJ?B(0;M+MSs|o2rfSJ2)OoLeDiQC?b*>Z)HZ4U_O}+l=I`;QX00#6)G^B z!yu~a{pu^c@Nft+Xt((tEQR}yd|^S_?P4;@=E|I>t|`3QgOv`L6(YArO+q^J{UlOz zNTRyVk5yTPrvj>`ZTMSJ)7a5(=PX34Mp{p#VY}mi3Mw0#DE>O2i-KM(+k2XFCrcfK z?2?8V2#NfHVy6~V)%o)P^yt(y;5CJKg{*X}1=B*KvRyTCYW>XZC_^FIcM%0t;YK{K z$BGO3xED|>ocz7oV0roSjMEB`MGwUX1WiH>)c#5KQ&fiyJwZ~>wnTafoNf|j<7v>;l z8gJH6BAIe-)Zk4LY4MsoB4ss$16_Pu?Tk z)aU(9LaR}k5FgSjhysw--$0X`Io<#>!!M(MUch0LR9DI7vN(^vWk-z_hhx; zgmlgLQM$IGB`l-&v1=>QWmrbDbL$K6C)iEhrn7d^Sqqs}#N70VtYm(oVh}LXK<@w{ zd7dWP>I6W5LQfr$->n&1k4K%|2dG3_uydJ-SlC#D7oL-G6oh}Y&}7pz;cqfE=u2(& zf(H?@NM4#vfp%t#XGXXgwBLhlQlR?rY0~1HWo*iql?J?_xgqhWD4EPPmRpH* zu9UObT?j%KS(8VwxRPJDD|m~#>6j3X{RbhOJ6nJnzCvwSPlpMI3=}vDtt5QUJ}9+G z*Ms-TP9;=5unF#l`?G50msPhrSIZW2`-3(S4;**^WVoh?vGBt4o>De!)d?+Ta~RXK zP?s26cM4gMYXy}+wkthc}XG#0qzvfNVd7h zVb1k_uHKj6biD|sJawWJ+|9E+MTY~UXXdv-N2GVL<@C7 zmQ=yM7{dwSg-vPQ=fNJ#pGnPwp)mdCDK99I zH7)_$a9;P~mqRs5&BjxpeUyMO?N5haX-(S2&H`MLuZUM0D*?Vo^l35HzNWtSXv@C3 zomt%HEK=*SQnm7@N&FP zQFErAsuPdMNbTAxBQ@WTk&@oV zU<5O&)?pH`t*20;{Qd;r93SXqvdHe2AwUS*Z&OeSes@d;WY>~L)e^Q*hcTa>qsnv} z_vkjdvW?3L+|w{sf#-DH2#xyK$V4=vy_!c`JAnzvBFcb;tOkO(A1Ju5P}hQN2*J(3 zji_jktD$@(3k?OtSPOxS;lg9PyM5KbqL{+|oO-i(!d#G173hI1>KmL0VD7k|3Qv)tv|f@134|dG|I+Iy zMNS0|y1DSdtYr~Lb=~k}LeBGYoIXQ5$iaM#`UeCgyPdxtm<3(cC}&T(tU6RB&L+b^ z)wNMwjujY`1y{-IY1LI;-92PnYLLy^P-Xia*fCXO$0Vzp0a#Aq)G%-XfK|JH5@f91 zEXbHb4u<0-n+gC)8X3iS#Zyf@D2k#6s z0;h5I(&;NEs_OC*y<35lg*>irFFyFXc$H>=Cm>H8;l24eABYgnV4_6Tf?x{;Kr~UUqZ}kEk>{=iO^Rp zx!jU2*n32Dc@qY5fb6oanT3&yvbykAciT7XLWBv2Lzccgam5m~W+3X(ch--oKk*W5 z!W0~BVrKxm^|AKXpE0NSZsHero%9EnslOxlTQiq!Q1 z9JwOLcJ~cxi+`xIQ}(pEMUZLeYcKj5kXGeURE?55J_x33UpSJZa0!PBTv zmgv%?r#i;nS2g*!u=3GzVdbmmfkUaTSDj&_Bdn}9*uUbH?X(9bsAl7C46Dge=%*ki zT^5s;YEUpj`bFfOEC8i;^%8u*4UBA&5z&QMdG75HR>Unv^#|!;3*Ezm zL$L*UmOZ>0J;y#VZRlg=*Db9TNSmiXR75LfG@GaGH`tO z<_2xRxb~l{^^0=UfWf@VJa9rEHbNk&S`2ns`Sb}WP$C7OQ(D6UZ8u_C9O?A+ z^!s*NPj&zI0`e$iglSS5L4JjKz37SsGVPiI2Bs$IG7eAJ$}(xCDBkJq8R)}l>tbC~ zD_zq!Lr~MZtm&GFnn-_vTF7;aGxcwnKQW4#9L44tn%BKMssB(PPq$VI-PMNg1maa6 z3d9vG|@ktQu`f_nvhab0!iM0Y$_t~)`YuB&Dj@U2Ew@D1Gc&k4}o@D^;e!zE7T zL%2*o1CwDA-Ufc`=T-UG|2w$C)vKi`4aa| zI(Q)H0`LI`Nr%3H+(S)FLd~j!{8IRQcv#z4oy%-!Lw)@KVZ>@wSK{!NJJ#g^A*#<2 z$yxRxC@8>7_W%K;3`6i1YBdT068~rXJTkv*Pa%|GEz}Zx#>Bi46=HQ`ZOMTNQvf{M zTkCS3Mlm#=EDL2dLo|u@Qrcdl7X5LKES>FTS@?(wFrWU0bti_ltS7z%=qSJ>YfZyB zJx$5H4RV-U!hAUJ!XaC0tOHE1QXcHuqBH}#0&_nRrz?O2D|Qy?{lpa5HDNy?C<#N5Ep2H3eCRqhwG=U8quZ7Nk z)q0#!i$Uklv+h&hfXjGz~L6MFlo{ z=^dm_f7_m2A%fxbCB>wliv8H+V}+H)HC_P+7;f4yG1$9tyt_ejnCJ9)wZE$taIpnAs)9_95CNfk`MT)|00)wrF~N4n3tf){s-N4dC8`p2o3uF<#WV z^}r&y_#tfm2wFaYEa1~b!!Nt;<0=kynfiDP!wIzo?zhY)v7wjdHg1+>{RP=l8lNx? zVdbDM8C6pU3lqThXobH|GtXAy=4v#Lt%GCjKM28#?_tweH9J$a`bq}0vsNO_r4s1{ zX6TB6)&u$=UOAw@;ftrX*1oVSc**cB&_sjz@s{qga4p$PD#OBPlc%PMyMw2uDK1fA zA+O0uA*7Wp@CM!~pbxs|@T{$55a`0;%S)n?zK^G|EwC6FT)wP)q!u>!#j6ds)<8y= zW7Pk3`dq{j17H7{Z91|f?%f=C@Ba0iy$NKOy?@mO#^Pg`0W^P%xJ3AkbW@K%^!i${ z(-}IG90rm@n!Oz2kV8l}e9dqXKZ_%0F~=^GIsO7Is*MK(mwUdIN#IBBa*nXt1F$BG zW{%asx(sH(hP}Vfk@!#Kiz)1V5m}?Z<_umC>@SS5O6HpW!W5-D(NcfmlSP5g2$VPo z@^DKu3a_$Y8by3&j}SwKdAFi~!&||f#wgH1^9wbPq^sw@&{PkKE=?^)QBr+686=Ag zQa}dDC#9vU_oAE|;K@_n#GUV{2Fa`;b(I~&eF4DDdS1payw*H$rFW|f%z?VOILExB8qIEO>vyd1 zd^sdU|I;w;B)5z`CjUQMD+}Rqg6d$tro7=W!X)wKnoG<5$n02`NN_S>WrVF%PpE3p z3vk&?X^zMDm}c7`Rc;Jn9erB8fZeFWJ1sF7C$$SRRqPW2*!{H-z`wm9fS+0K{{0pK zSj_YyNJW^biW~xFI)r>&Y_>`>nTZ6c&#ZCT^Mx=uoBKh7^_E<0P9kw*{~20BgFg4T zZ_qTXsQ-^Z9#)|i^0qwmUyyglUxK`58hK{$#w@~kH=32@i4i(fkAM&M31m6-@2(m+ znQJzz4sp`!NaQp#t{?%kKCZSv;PG~8x6;%VNa8PrB!7$Lm!eyfV%xm7nt0QwRL)b5 zoti9-ND59P1!qQ3u+NTbo-0t5Xrk3qz=@~2&fPMj3R|BJRV~myRGz-3Ravd$aV-=C z51?w+UxX4ADL+FSAUHto!dNIkM{~TS7r@0B^b)uz*~~tRO@!=r$Rq0ei>%=(e=*ek z6-bfmCGqsf)}Slp!CD?Zbh$U6H>7lnpA_2_*# zc!Mn*jKRreIkmWZQ{8&%m0;PUhCLgG4?J%tn{}Vj*yi1;!*?!_F(MI+N9UyD>?(JC zo1hlE7BMct;kyoy!>>bv>RfO-LKc%uh$x%dnku+~=nJo8G!g0aRXTkw1>%KKnauT9 zna)5AijU!PcMIPQ5PTIFvmnf`9OfL-OwAZ6a|kC7Z0WJ8hRdXP2ZYv0Tpy(nq~0b- z86~x=A#-M*w}B+!sm-6=!&93&`zlXuoO_I?wzK=H;7tFVX<%GWb+ZR`3yL^H!Z@(MVa0f6*^Ls^X8ybi!2IQRLr7&a_KVvC2giNR@T zU6y@0G{p;2$zc?+jZ#bzOMaBOg*-}q3}qJJfc;AsVN&?za7uh+uue z6Lnu&b7p#A9&`ALQnIPUGj(M{36HEXpgy6r4XaC`DmBEpq&p+q`w?^tXjC`1JJcMs z3z*~%q3QWDG+sfJDu)xB-Beb=d-F#x_-YSWJq9^A0fC_pPLy(Oj=f;8NFzF3QErK(g%?h7HDY*f?EO@Wlkq`SV~+I z{9 zoiO1aj?b3SR$O{C{Kh!P#Q%K(wi(td;RTp_AG`4XqGYHy0_Tp890BND!Xsd>2Rvv< z)8Av~Y4EYGq0ETc|{nqz@AN(2?T2JK!V?V8yw`ajo#Z(=Z_! z)(`cyVSS^|sPB@f`Ye0uKDtJ@U)ibRPn{OgoZ!v$7Q|L&Q6gifEmqG~{#X+NGPD!> z+WrT2Qm*5a$-!m^{R`dm4VkE1*w53tOTD!dUnmlhB^z0EeSwb1XyKB-R{bkGXp915 z2q^NYXL@3zLL8e9VN;W%nmX`Lv#IZ{Hk&dUZt4wWq((Dryp8ru`KMchzrpEQHjtP1 zhAhtK@Em2zzi>LNo|qmVm0p!B(6W5CPS1`?|00~8pwkng($`q&&}YDaDh6xY8c@pDjGolNuGq1w2^JpedbR)h;ytI7%RWBu0(z7vc1Dot|!{N1ZP%gO*VJOXr0@ zB(bA`pKeCLow+PD=PX7pnJlr0lWj5oWZ^VpeVG;+Jh}Mn8Aj@!ex(ZHQasmNCzl9Q zp44~!)Mt`MV5XHrLjFOqw4gMeW<`b=t~OyEL!DssL_WajEEo~)!fti zO9H-R!-_Q#8#$Nn{nU8;^7;u4B~>rjP8gM1y#YA6P>O8wLIK!5?;z@cB?``1psvBW zNH+eY0F2vCaNf}yAc#fwF)_Xw$IxE-qI3yrtG2myFnz}AD2rXrw>86bse8;DGMVaC zoRjF28?_0AG5+@$@VT*=@VIYNJ88g)`H1hnrP2)*?4m3A9N*~Qh;Ev!zTs9ATgLS_ z`#dW(w*6*PET{#3h$_goF?+Q z2)es|)w2eRJwO>)54Ab$&QH#-D5*(^iMJJAs#>B4Y8%de%wB%ov>BZGpr4P2`sqF8 zo*!t9vML=T5|jy;9ae8-q6-lNq>gQtIqGlE${h990VwotV8-~Zn4_BEN!*Naj`{*$ z1UVLF(7gKY(q_PzzU+siVtd&D5H34bjG_*>N5c?a`jiBepSK8jWZPU-^FKZp^8x>H z@m|CS^(!nVeJ{(nWL0f};{rz5#S?=9PE~}mWiy`5Fqoev0<4pfcEw-hfo$5E(3%B= zTtpS8+vV3Ss&-#!0%jm~)j_C$2OijH72^v@n1F%4V&ohF{$MU>Bc%0j{dwdePJZym zAfFA8K1tqQdLg^zZ~L*Cyy&Dfv+zxR27$5CjP2Aiy|WWvNUJx37z~f-5K%rGZ#0;8 z`*1=kSG9Tc2&juul6NLLUDnJNIEQ5-`#bp0$o>K=v!Il3pxc)n6nM&-ks9{Ea0-Mf zBefJYs@5PQQtko)B0JS>`1Q6DTW6Q zF&y^Jx!yuSQUGP8m18IqxD9`JgyTk25e*FMW%4$NQYtoCp9;#X^)^rv>2Ay)(t&wn zkjcBri-ST&hTv(pYE&QQXD_#44g>}R zLnJhN8Ciug!X$`4aT`Isdt8s}qe$~^Rhii`x7bS;;vHzPh8oDBc|r|D1&8`U0FCRj z@i2hLQ5inmWud~t1E8(j#gw5)5%+Msnji>`HGvDfgfD`>aG`)2@kk?$e{kJ=WKe@( z0{|%y2P6k)s`eGa*u_r^V}CXYj9tlGmu;lAVU_0XmNspYei|QxhWddf9sZUsU1kN# z$XVJSs4G-vB5L8dY(*I&{ZrsHn1E!Ccd2$dWufl!7*vI`J;&PZZ2OUyQDhvLlAGBg z%rlO4AMyLacW8Hw()U2+s4k_o5J^D&0Gz{;!3|^l@6-f{5&rh{VK&3yaEhc3;X^w@ zBgvkQQ|m3XU$NXek`gxbj)5ym=zgu)9`ku78|{j6*e-FF<&ikG2Xpy-2<+<{I1~>T z|F2IpWsT>=2A#+uYQMc-;J_X3FHYJ!pEjCR6(-P<5j24uCpX>X80Hx6^q$0h8Ylc` zWb83;vgw~4pFMwM_57iKdN$ZV7zm2f6}`*$#VwaJ?SqcmjjnEcbG!X`tuOX*=c!G{ zK)moqgGIN)zWz{W``<^Rl;QcIsnb6t#@*l9?Vzo~m$b+myXabP(&M&WDDO*}%fgOc zNvQ+pCH2Vl-;o+))O}p|1&aimFAn`@mGpId*IBVGUMkp_JwNtw+kRHragi_k-B_J6 z?D1IJ0ZB4j^NoHt-b#wM)i0G`B_*6uD$zSm6UWwsbniDS(nNx#(lpqlz&>u z9d$`Z@kW8p=?TW?_Kr;*>WvjV!~>%sgF1T|<8(Paf8eZs8boAZ2?k*?5)TQw$5AMI zez3bQ|3BfEtG$fv^Y&lo@AJ6ec7pxaa*T{^mv2n0eNDNyoo)XF`{vlWw&0Z<1S`+3 z3HGVS!D1<&>&?yhR3!S63D*hpm#*9tpXJ%EVpS=V1o&4a0HMf zEym~pWr;*7kzkgP1Px-6lt?s7GzynUmJ&&32}#f;I!cLTvqa-?i4-Z((JUbex}&Q&?ejX?UHx{?OS*zt z_kp3TtpD%TngCe0|8KCGS^pPU|IfAN`fsTogbe~kYWyH?Zfvgq-mpk{2%|5f+%z~y zhQwB3U$dQ~0PGX>wv8M?pqZ$~_x6Hr%4w0&j(NjT^Sv=3s+ht}!5ED95yadaS?wMA zr_0wPmDfTsxs1m251UdAa{aUahp&&}A52`M+@ASRx! zIDqHek~C##u%$&x^6QPA8yhpVMlzS+u|K(%c+~ zBS&#X%+!NHXQ+;Ou}!1P&rh;PmY?6TDM{?poOwx2b3k=c!GX%TGBH9BXOw#fS9=Hl z*+5sN4c)W2i!i(U+K$CJy?y=nNmCN=xAWBI2m{&S0sXa&vpQc`9__HwLqpDrL{9`WPL^!8h#>H%|cf~&WS$zmUIMM)p z6u>Kply`bxUyZL$uc+~Zmo09feaRRu7g@mRn~o!mt?Qmd9et%oN2#*P+an^GhoK4}-+ zZs=%uYOi^qli`o;jewh3Q`?9s{4w=*n>MzX2Sx`pQt#~O*pP)nh$V}3?jDvRUE7Vq zo`7y&_I?&?Gfo|J#(d-SY|V0YtHojxL1eL#l)owR7lBn<$EG^4zGQ7!FVtpD>4LSd zvqE8ZKs_gvkM&>)>p4{JjM?frd=0G=+j<`uZFnj>>O54+B%ys#DNzAO!xS@O_*&Mh zM#KtQG=PCjRB0_#L;A7aI*V5L8UC9)`mP;q_>GvY*^N3D0~MK5x*9Pspt}b3AucCO z(JIb~=rXJ&)SM$H%`_0nj&u#-{4sM;M8((Jb&YTgA@3hfbfR=)vC7oiaWD| zX8#+9*Yl1%duvBF{`JAXRQyY_r^O9=zb#HrV=5hq$G+RMNkG$j#4vqv!>3xB6?Jpd ziBjC*($Hz+#0}uHI?w4&_A+{fgGi!X0!%l5$87EJRO4EI5Kp+*;mb+#jTzm)82LS8 zlI!A%fe3F-l2GCtcHW=RnUv`H!)~9mL_W+qgWghGhmlXEx*E49)|MM{3gLB+$+l%M z!uJhM!M{X%>!y{1vrrG{>kpx?eVV@H9(Q+?!xXLQtJ3oW=<8r%^I*UC(orDSd&=++ z>F6CZT5ja+Q?DfAx=beo`fY-LLqNWXBhbbT_~%4BF`k^!>Bic==6_VV3Gi5yBxo4D zJJ#lm`N`?2%mOOZ{|OZ-D0>Df5`veyU0Ly-yyVp^yc&csrF}h0-@6t$ObxPE_67j4>F~DbFu+q52UP5l3Hrixo~PE z_iMP3pU7Q!rVVG3)%G;7oHwl%Zj98ouJytN_oA>UJ5G1Em_O1d}nm zE~+o(cV%ikhub$5_j#%c$5B;=+cFUON7TG~YU_dyb^mLkak=eD(bCk+gvR9x)U3R( z%l(%ocwSF=5x5!SORWdjJfh4xq=w(cV;ZRA;4hVP;RV>6QXz9MzVrS^tjjkcQ7xZJ zeRocZWO<)Z1Gl*P%;|Xuu8fQ8B&yRhRZ@zn(;6s^REo)gdIr{ktUg~pQbFk(lA_}A zl_Zo25=sF+ma%5w!@e#7Pe2Mn_d&@GON0?vkciiZM70+l&FBgV`8r80-|v&F_`r$! zl8?ASQMyXnsZm{#*+Qj-TGbl8fu^VsLgc6$R*yrVXE~xiUUVAgz0kqYb8J?Tbd^}E z%8HFGyn^|AQJVB&JC~(PcNQK(I*7NA8jwy)vkLXaLR|^_x(SK6CyLq}ML0VEUa0B) zLB6go<_~p?j|ij(>OM!2g)3!%2BtyDhR*@o^lso)(O|k-@QMub%w;mjNi#6WT(_u| z;36wB~70XQGpi4eWrrK^JxCqWd2_*M@1}9+VSnYuw|fChlqy5eD9UXKh_2 zNe1CI1ML8hI!708q>Dd^;_G>4p)QGN`5Am*EmimyIK(dbCPG(<{!wR#nOpa7v!t+p z#rPc9fj`P~KS!g^-Pq~9LLV50ko9PJ#i6DiRqSoTYb`F{>~!@g_!o%A!pTtwj?78d zeU#CPLG4cOQBeXi(x}VZo4U}lfIP#0I}|Q-sl>tgoRIk2z6>L4UXkzILe%tSG$9WZ zGtIMhAE3j4y@S7%F)8>POv2Y)c)(ZUzA`2ee{pP(4qJI}P&!B3<^7$jv8NxX#jtQp z_aG1Z*OZjy^vVwjOhg}v%U<>xtDzGuaUiniqtkWbjh1jOWfBT{R;<8Nt)@<+$ndPI z#ybF^M!R?(6Jx{&^9Gg(#AD7>-s^=*7cmM&&_%&9nWri!0Dm6kr*Zx>nQRTdV#9~j z{?B>DH!w>OM4heS#g1uUeBgPOxMh&8tvxqOT6xMW5ZfiE&*8PklFj7bDBYqJWblG7T_V)txCtiY9o7yXl0B$5r=)jXso9?6In65UWh2o7Zmn8mP}mo@)L^P{ zw6NcXPtZc3I~2Sew7aOp1$+b((qNj(M1QPO?vD)LYn(#JBt2kny$&$~r0)Ig#orMTa)IF|g1m0}XhE8Px(u!oQ8cbRFE(*DPlZyn< zivS)Nrw6{70E=gs{0~o4M4>--TLug#)Jq@B33BN+B7!s14m>ykrA*MeBe=4;8fW76VN}nR4Obsr;0bDZyS|8=S);tSMW8i-tsDz3gAN9RY~uRVZ=iutQ9UZq z2?VE0hs)3)&_Xwi95lr2kXd;V*O`I-7(c@|J6zXGQWx?DdvWTRg>oT~P!Qomn%6H< zq>F^I(X6#(b1|BYKm$Ys=Nxs*ez_Mo?ocn*^pmtN^y zVZ!UqadpiWn4MGxyYVRR`S(xP1!hNO;pdK$g-?G+2KK+~W@IAu-H2bTI#Cna7nt6NMW(yUfEHi9OqPYA1hjnojUQH-6P)&2;jMzNYpPn{a5 zXOTy>d=HBfjJI$Vu8fNlWUOEW3~wTUEpcZd&uJx)I0n4)bEIv_i<3Et?-94|&Dl4BSA zo{bz2WB&q9`x69cEdf_QmGS43qI)nmK&)&Sj2$hpVChEQ=nL=?raY0z7@QVyz z9#ZzWv3LLb3icaP66HY|!Pi&WMenw-%GZJ&DhR{>tVx-{txDlTi=T*SV|=u@KAR0xTcNpk8c5 zsdv>n12G;zgVYlc&ob%J_cYc)aoS!4Yf7mGliVyeE3`r=*wL`OfIo;Ld{n1@k-kE) zH^{q%j(>S~_LG8;?7*Nkga3-|tJ~V33cY7M7q{SiyE-4iqzL9j)PnJviIOotVtTzX;YipdO|iP4K+F z35>%662PsK&i#_kedI6XrVUl#LR5to3)`^8R-k8Yc2cgXr85NsI*UW7h7b*P_y^VX zuZ0zxeeRbi=pTqTq}k_g#~YkdRUC@H2n*@i_BDH(ZDbs&H}IXpcp?NmohH7Q*-T%I zE&CPaXfIZcU&FUF^_cwP28;Z*Jh7@Ra4)V2;=aJMR=Om{tv8>uykiksbaH~r`v7by zCMO%-!4UW(p&y!^ReWa}_Eb>KiMbTWLHn9nel^X3c+VW%F^TWSJ!3v?+x(QI{G z%}0lQL(@*}_Iww^4auYBXxRSf#`uji@|akB8g^_n>j4GW%ihKV%=))y!AV@nr!?wA zk;2x1GTSRYUcUnJq0Uglo)((;{xRXK^WhX={mlSe-2;DU*cuuBPJL-~W*_u5Nv-M* zQr+lOgh;!Gi~y5LkmSqfcC-nB(A!UiYE9Z8NWl#C`%_ZAM_0e`4^*GR>RY0Ei>es& zhpfAwO``t=To$^1)*R!h=UEXOzd#klot6(7w>XHWc*kbv5WU2II`y=QHIWItQu6|`DV*G}VaIMuUK^7eD2WSN6Z~Hb% z0l}uvos8+I3QgWDVXZKkf(jrP18QFjUFsI8>$FPi3hMN?oyIm1qi`)fQ>bwda@1R> z?#*hkYwwNaDXf#YT@xokj8Owr37N#QyNMfFK_@FWUoc&Qr5_|E|*n zI3~b}^FH`QQ`dCYNQZ2yG(4F$_gFNEeXU7CwphV~y_CuW4vDWlm4SbQ7KR!{P(e>T zlBY{~eJl#|zd@NHi%pK*BBa9JEZA4;#UtBzsT^FPOv))|(mT%H=Fj-~mls!7-=AM{oj}mpGflg~%V^Me-&?z_5Ot3$+jd7w zLWp#js#Z@ELWK7NWDs3#8fPIvWtvEOu$C$4L^P3P zk(7vR*ce{QEU!Hvwu*sb0^)DkI~9|(zwI-qOvmCC`lXMnK-;|_#ba(z_Gh!o0W!@F|x*)+EX0pW$mcej?Wyh#ImWbp<;G$v%o}3a(7>W-WdADMdbE zCLU!{SO#nbFdbxZVlX&W=IO;URzj_TmfMk^JF_g6Mni6Y++E8#As@#g=Ec#FT>iFM zRt|DSV`K`Ik5j3n2&|-5AP?t-KIqM2thw1WiU>K#43uzTdyjk&?Xi{asB6hX(xXr&#dRWYNe64QN?tU^OkATS>vuXtNTK9V*hmnTe4R& z&B*?T&@Szd{icu~xf}BL2JWtntY;#0>EqYp*(Lir+V$nGWklXFkDlNTeF2g3 z=7~v>GDq{NO)w=os0|!^Ml8f@CJXjCxl9K+XyUPv^$Yep+3YD&8U(X=>orp6I7!UxDJj@&ZO7<+5TUKc3!^F*$|3r`2j4#zVm1%a^v!!yS;V0TyR zwTRF|b&7tR8U?jE|7-XAsTT{el1RcmWw@3~!NIP-M(x#KN2>A?f3yd#)6MK2m*>nZ zHrisN{cXV)cI*xI&&Vc2P<#Py)&O!zv^g&Ia=-}w^DI-*&kmxKMkQJe)?b9UyjIlM zDp)?OCwH)=>HLxnc=#RlzUxRhM7)xdkQuV7Jyu&k|SA<-hOcx9$GReE+`L zzg-EpF1X_tfsvw-2CH?$#?&cxEi`RYvXt_bOX&a`>Zr*A?kg^jFMjNo@izSSFZf0K zz5Mp;M5^~TZr_?RDwU$W0}%E$bZ{fdO)%iC?>*Zb9mdiw{c~1|JBwK`n)zn3+1k|2;%`1xb}V@sP8UvKurzVM@-gfXdZZv+~eYO$CVbnyujL* zztoTX%k%JR0qywqd@@2QvG(P%uG93kJJ~&#RccNip3D%m(c5b;6{`@Ghd^VO7sTz} z{m2qK!e^;Ky-`Q|BV5MFbGJ4fVHKkmgyCN1I#+F$t9G8MO;vBVQmvs9??VJ#!Qrl8 zR~5WL1ut&Yelt&J=}!A`i5t0F@96|8} zht?!Thvng*5(z~|XA?MXeRd$TRUmUXE+8fA`KJXN%8?F`h;%&fb8J=IggZLL;f(h;BOaYDtR z`m%z=0MlWyosb^RO!*p7P~1|>VLFhNn=CcG=gUT+j{d2Y$Q*c%7Hm*gd;-d?F(WKvA zrEAZ6n6;pD80XH4YxRtv%2C_NaBRXuH zX&to3#gkyAN8ce)_-P5Q6*qvSbwei68WUlk#-hM(EP26dy`*HWqJTZ_v@dIx_aOex zF7uDSQ;XGM3b$}Uj0?lC@)gvvd{ll&=2W$-gE?{5TBUS!DIuC4wx9Fu4NNvYYUeO> z9o46sus%F~yq;Pf!~f*^pX&YLwQ^~zDCSgAHiR3~oEVnm*}nXMl%1^(25j|KjeDK; zeZnoXOzyqgUnVb)bo>%scokL{=@J;RZo>j9)ZU`K7MGfOvpNcVv5d`&>>?jsfc$@? z2KVtDR-OHVXlRbGgiuV^=Fu3`R2P>VZ*F&$?{Ss$+xi6g5S4$)`>&^7TsNa+sY@sN zYWz?9Kow|r{xQf^8^ZYc4m^YJiGePKqKn<816BxtMZ6E-G>VycLz%&+A=~mFEjAlR zFnT?nOxweK?IA|Rx(;Y9x}O>yZq&xRDATloViI+0NlooVpi*qKmSAW5_$ z1^41}ZR59!xmja`9QFDNNcEMDj0d|ZiHsXzUOk@(s6)m%xg*AhNN$OKVP53%9Rm(hLA2?9L#b%sM7?Q4E=To~owOOyhYMmXL z6W6G?I|x6W+;$zL|D?TymL#_I&fO3~gy$Qn+}4_yZr58lGYS&Wu5&1lb&Hh# ze`Db|x{UxEbMNvO#-prNa1p5dt7cZl5hR?;Axrqr`bbMa$e%=fP>q+#BES*7~C;!+VUv9MNP0QT>SY zVAr)3Yf|%flzm>LH=d}CP8B(%XFjX;!%fP^BOpc&#dq*6;%X(fc-8F|{3*9t@J|-_ zH;cpE%&`1j%O|cOB1En5gHTxlj}6ezIKIDEP>}pJ$(DZzLr>yHqFqg-B(*E@emkaQ z8_K%q&s&v#u=k{V*_={dNTD0qe5_^5$-T2%%S<|JblGV5yoe@rr%yCDl~|%dJ@N=9 z13+E{tKuyj*q@)h=+Y3p$@3tsTh*+LVCKU{z}Y43#8R{Alew^^iIT4j+^ zpYG4;)ga49iZ3IK|u>&uT%l!Rq&f zCU=)I7lKxQa((n&07@I%G<^&?{9W{pkG~G_*H~13uH-3y{=Fh{*^dHzWFu5vBxY~N z;xnK!--K&vU3bi7T!9Z9N;#lN8VPEg^0QT#^G zVP@d{w8*;L?Ka!VJ7mFc`&mvmA{!9lw_R6>-F{#8g&IA=0r#cW;P#3GsU5c$w%!w; z*@6Y(Th)^0(qLUt3L>>s-{R)ahQdtm@;m2`4y%}+r2jU||Dio;v zuDQ*-X;BuWi>*#ZGz*OUfh*rqu257QiAlkdvot=WRUhiRq9zmZHFkPSAWC#()7aav zrSLu{+bIHAO=fh=&e-!QNF5c2TFq=98b9_0UQ|icf2kW@#gWUVB;LaUwJ9;*`#mdx zaZWi8l+#I~+WT>ze*E40@vwe8=>1rzA9r~_q~#?l&1dCyL2jRKY44t4@Uwh|Vt!iC zSQXbH+L~M0K09leIs=w0P`??TA59nH(k$WzXXb97xP-%0dzic%l0dFZjizgGNT^@O zx6!GatKUJ#KIy%J{K$sCj`H%da8JB?bU1$>@vr|uj_LmTLC$E3m?+S#CtjH~ZAGsv z^cUurMRs8ZDtihVyB&2f^H@ZxF+tGC$6MfbbMySuJZ%P}?Rb~#u?5u6^E;UTp-M^Q zfSkap>=CKn$AW9Er#Kx>DFsQFsM2PT=VT1=toZw|q1^4Vf!2QBg6lrnkkZ|@3sHyp z>N0gvG|3LYdNqTbV6=1`*FkGPKQ@AWyr?IGn#A=J1lsN40ahl>-OxZRc0EMfAIXZD z6uF>nCSJNnQmu?C7qlf%Q;p*GXNL04jE{<=7h=*Z9cMnt#l4(WnTj~?V?0n5fXw12 zNy7bPo*i6bt{vQ+sUG); zH^FM~%%STBbI+U1SM);m&CO*U_|VYu%tITss#fYq2u7T%G`$|wLCGX={cV-f8V=14 zG414(9iM2;L#AoDTj|NR^ulYjzph9Ok2QPxsld;st@lEzX9?Z+fNpzKM8a$(RN91n zw#3a!n63#?XOKiy8o2NnLZGU>5VLx&9rxxr7BM%U&#d3BY9E8J(r76RAnj9}`yBzK zK>+42(1|7Hq`f=yP5itJvQE>7FP5B62Lt(kDF0u;|5x(=&xGB&iNCk-f5UF({9}FH zcHA45vj=bx$am&3b*+&&fdC9~H?_v;vdu+;iZ9t_i*;G zT+8)!+Z>C%yKk}B%c}=_Ry2PQtTvU)ZvdZ(;eL1JCm*yu9^-nojM3eorr^KURg6oH!xG`t4e z**O>p?D_M1^bGx)2SBW+fbAoxSaz!?AvDdTaS7>WOr^*A{zi(VK#Uk)al^$qz$}*M zruCdDjC{{K>51MAEJgc|9=72%lQlyjl3AofU&KQ(1pDRK2+t4VzG9&+n|bJ~NdNL29#z;*2BF*=cl%-gYFt zNlkQfNp|Vj)Q;+ZO=@BO_Bpt&?J`>!p7fUr&l$mw_+sfnTr_`sn6pAb*w#YGnV@c| z*0Pj#R!jzlj}jl+mS2U~8uf-=IqzOs0LO9xu1h6D6{2>bSgnm|q&ivQ`U(@20p5c7 z2g^Q>k7wyc4lkl6Af8M@2E>n z>W@%_Cq@P;=5s$36HWf*^-MHBTTEau-gnCmz=@B?a>U*;vtLalFiUHKQetF z5jgQf6f#2-+*E0Yt~>8YoqbqPIllmtjN}qV;%Rh;U4QqkbC?3a^pLj*T?|{dK4&z| z{}2RtF>YP{MGg6B_)v~$2!QMI`+ND(1@{Se441H<5i6SEO_~gro8$*AljR@&@A8e= zgBPzDj%<0uTWcL}h^6%{_gTUv1HIL_kfyBn1BR46um-JJtx!mj^<&W)SS&`$i|*Dk z!WP#d6unbJFzw+PksTGQQX_kU`JaVvQ6*;KbgJpZGrG{0T0yfbC@yvT9jZRTr)GUw zN%Z#T_+}L(&>|C0njhtp5zA;c!}6xb_xZf3%`AxY7spW_l9p&kWU01RGqy{olAb*4 zRuc!EM)thgH6Hov;6Adi{PR}Nr*1}mb(Y_wEe66fL%dEK%D3^QXO@=4W@ByRH)XM(K1rN4s>l!)~afL4iL$EX9?+J_JBx!br3;zH`kQgyiEN%)@*Tk z+4|Kw6gj7S7>b*Hz*Z+u$zynD1@g};JO&b5>5wYDCq{>rp?dHk`jJTY2qlK5|4aRNR9VX%59 ziSelbkUW2D;WsGy_;^av%yv-KPaXkAnB5uHq8$*2J&s~8Pz;qvLw(_C(T6pv^^ydu zBIi-$54K1v75RfLVhO6OH)b@5mDm~sr%=M0(lS)&6ZPa7&g3C#-XNhd3AyoHRO9dy zOZ#|ER+IJGh#%#;LM~nkuGI4e``8{;DN2S~d$uHI`++D)Y4P?{nRyvw}r6(M-t z=$^{|&y6;}4oP15OTGMKEkcIpnLV}A>~ODGX_l*Kba+T8C^cgd-mvNvvbVJty|JSy z(-e}PziRGj`oX?-p+s0#V3Z@6U1qe`{AOKUSN9WXGqKug#6A&uT-f1b$g3W`APW2% zf9v7pM>e|G@Zwb*N!Ws}eoL1he{1qD+(A5j;p-Ky{m3TVOiLr{FOIwx z*;E@p78}XZ+GWy(1I6n_ELEF>%VihQEenNsUDpfu1XYR$ZZ6sbeiFegxevrIBmWOCysBL^QcWabzf( zC9>k7X-+Kqh(_Q~)*11-_nd&*T^kBAI5!EFkX`>CJr1Tv)XiY^y|%wnF)DQRH_9Ph19;aj2#gwlx{A!{d2_1k}V7@{hfJ$gkas;W?{PC z=XV{fe;3L3h5ST#SmG-4)Q$8S%c#D=Zc1Z!`kT^9{7Zgmuw4v2C1wNoB9#{4cM9;I zKvd$YNCJqU_xN|X45Z%@d4*;2KA%-5O^CYSlU`aNgb*_QC-e4Q?! zIXK`XfoyJP|AQ13-v(CE%(pgRT<~UsdiSPtMC^ck>SjaORMU?A>34%;?S)Q;C4wR!v`*?)7te}yTx z@2zbz5ACJnWaBVbIwQT zRAi$tG0Fd$@Vh)Jn&TX7z_RzOBrG``71-;^(Lw1|HNh3fP_i&kw9{D&BCCRd!jB8{ ze+b<9B^ktc#pLF)6icXQqASiB&VC@ve^pIkpm1AkFeZ#2BC7(G)*+1`o3PJt7hG`J zQTIhYD&^?d1_O@KBr;mh6v8yUrXvgSTU$hfJk9jl<;TMapHW;fm&oQPmqd`J%PGc% zt6@DAF8DMmVBD-H0!zi*Y;2-8W|w{G^<&I2G!cs4goujGP9}_GHP1^Gc-K7u`YY|V>qc|~(|@eNAx+;rzkL1x&kL%nt;+MtA8OqQ96y3PLb z;utMs2@Szz3mA*}jC-YGI4rbWGiZ|K7k{7-W$*h5eo>)9mtbE)_-Ey(EcX?(9@nYj zPEleL?Tqt4=N8o6gOAM^mzUYRA%vT>b=MMf@PkdZuI`%FCGxC+m0MG}g)mnrG? zMyF3+lOw!RlW#j}#5|h{zQ8U#t=-qWWc-Fib54Dd`-2ax^k1J*cz?x@)kluNxzq1M zdmeg9F@BSE%gDhQ4oTDcjI#gb(PPElUGq;hXH2lvc*I0Y5U-!fan&X@G~{Yajp}dt zbpA;UXbNE{f6I(MW~aO*Vi|8-ZEMF=TXuGn-l%q%1TE)xNlO&KK$oYh$a{zOaJ{5r zZ@TW6aDPJBm3)s(CyQ0&!(w*lY!ZZBi~zH;tqsGj23@I6UPW=^_w9#^|7DTPU1RI! z>v?&c3V_SBR|Tto$G_%@pd--AiN9#nR*C!gpJ~%A&K^ zKfVBa%#Ove&v&2EeAq!I?+G^Ee3^4M+Zv2{x61v)2QnyBxr*A&~;+42(p({cgZZAr=TsD#D=HpP+e0bqK7K&6zw&N z(5U(0p2(D*&c@_B-P*9WhUdG1P!nLEZ?%(Pt#c)GW3)&sVW86Uj6 z0XZ?4(fRH>h*h=bt>JJ;54JQ58@nIRl5&o}&wQ>kSAkb@c<_GZzWR9%Oc(a9``FnNXQxK@!tqG7j z-+uG{bTMRn9zo2|ZG`>{Mur-KexR)m-CMk_6+%&c#Vu_petn1FD}tybP`F*&m+qdL zlE~M^fx^1t$jqHOgT(R?sG3d{8j6K}uGrUXTKkHc{WuiOBt{c)>mn`Vs{JvAzd|80JjD8A9g%u`&o1a!Wr_c#x|VMA8cI zsPPzXR~+PaMd3ji8Hf}o#iF!jYwP}cRCCRY>CAqNTHj0NhJoz?ko}Y7p;Nv66T_n2 zKOY~+{<+rfpWeOoyZ<4kpu_iBO*|g0_B#pih?i%VH+sO%Gm?vD9KICp#^nf&K^ls9 zm;KO|1%Jo?wuOs=MXG@nb>IH(c0;gan!el;t6=$@Ub;~sdShz z%i2GeQ!q zJS%pZ{mX@J=|nOr|3hWju|iIwrxxxN_5vVUl{zPWGhT1s@PX-4WAaz5a|NSIII8t zT~of7DW9Y%cN;<+evvz z`@<_;`6n^ZjJwn#sn*HXXYi8ka_OCB78qbZX)@A6MxO}1q=CQEU4>3^l`CE4UC$CN z@>_EbHElf?A0t2|U8QMh{X)~VLw=XR1IwNa`)hl)Cr|Eq(juxDs*0vdk5 zK_S_N80DG&Y@B@--YvqhYFlp;y~GacJ14V;fNS{x!TTuix*=)BY6qXHiDvS8YVy8P zhfZ8d%Ayh(wIBQTs&Nh|gk02bEF@F5(l2N)^*v(h!y&kMY)ESEb}sQ38^kxW^+s%K zYv%UmMwGihp^L9ylNiD^4aWG=7#ViLC+G}nN7=SS%is#QL_dx7!`%xDnuV~v7Lb41MJfKaE!lxPGn-B zsv{}0+&>@OL0V!sM$OG#J z`U;YBdu}98J@Pk)G8<55POl_QHQatSXWXC$;P`{}`V)TiODrZ$f700`biDAAC&aat z)6WlOpJ^BCnK;@JQY`BRpFN0|}3_t3L0OnU)SBXIqkSjTbDKGd$})M{|-7$%)r;u#CGms9q`B!->I&@ zXU|S0=#*N=7y1lT@);lwle?X^)TTFY(vJSciwWB9fhtWJvhn3sQ zI1Jro>OZ9cYf!a2Owy>Xyyri$kMx<>KU0`v9z5J-YWQTl`h3BDa&tyMp0GvM=<@;l z$%eGc{J|?yVDa-ayu)q=lhC-&74P3#X&rw9}#OoA{-;fRTNq4z-g{|#2w2#L|f~MG6BV1fff=I_q)6b z@@(KCu*Vq&p_L>IkzWbHDyG;<|a zQBoYD0IR77W471D6J)uVO173FwH2sp_I@{ph2&eZS9IH#JbX-Vs6?=>{i-xVl@j}0 zrBO}tzbJM1deh78N3KQW7v?Fg67W*>GUJaQZ>vq{7Lv#g^?=gd=60`0CTla}Nzg?6oqyFs@Vv}OAt$^C>o;jSud^_HZgzjCT45gC5I)S8 zz^v@^UAC29B|Pc3ZjRk?CLh6(CV?*mn5uqh|H$OCwLd@Qi( z`tV>a{ou=D3&B@j<6o13%K_}t^s`)~;^^b-Wwet=$@_4-Wg>-&72?~`fFxRL<{{4KHqB^$iYv0f79rlJTUkf>fcytQ;?t~BhFEV0YW8?k zQc4|BU7mR-VbL?#{!*cF9u>Z}(-Dk9)3~Bf*rH|UIaW*UkhuqDz~BqxKeJ#goGKc! z?TGa7tKManFhd&4hjx*x+U2fOMNl!*>i5*gd*F7gnj=&*9g z++~jW$(4{ju~F}hO8i*oN{sdT9$~JcKcK0yo*+Vjx@3u1{`HvG;OP`Ej9pEDXGZT0 zzn868)~_)CV4(67l9N56%Dh$_Sucnx=`RSe1S?i+s=>|5ct5V$CDB`8Mt<)WR^@uy zDC~6*?@tbX1&CU)V08Atc$^YyDOmAyv$>p0qbRFnfj|be8`WbwJHqh1WNygAkj4Fv zdo=#=IAF4C&er#FXt~K|N&snLWci176 zK$GJFRop>T3-MAKxTWILJa)Lx41lQzC7|yzo9M)OVmn2{KZ2%R z^Pc7X+PC+a4fZ=+^b$*^Uj88xT};J3+w%AP(y2Gs1UCIy>n{A)pmmn4Ta_B_7Oa@h z``wwvQ54UVifyeDt?-LY+$uvv#^y`1@M}M9LB)KYaL!sc)N)258vgVlwK8?Ohuwzz zq{jBc5zGOtikGo9+w(h96xk710GpCn_uFa8KW~d1D2~*>Do~yOF@8{yE%3K@_WJN{ zVb{Z?XS!N=1C=|O+Jd@uso{zuO(po$V(i{ zH=$DXZR?eBzXq?&`p{1wxOR5hc6vJA#61cTW$Zh*?B>vC*YE9U>AIN)yKb<>fW|!f z8wp0Ai8q|aK;q5$hx;KyX4zAT`$R|ne@*1UF)R!7(f@ez;2ZX!SVntv#HUY4%JcA& zjO~MuPchje9izM$QZClb#FLqpzo(*P$FlmS9I7{W!i24~^PEa>VUZEs(t3YA#_bxq z5_}3B&2zn4vXB+~RSff$yxDsuj+Z}CC~`~Yy1^YNg&wj7C+!`9%3UfPZSgc}YEE%9 zpeH!W&|sYCS$13pg7*YLyq(97--794v+D6(o@fo<0|)!a-QBdyH7 z3KFQ)X2V+`7Nez+gY+U@@(Ns5?dN{eLLKIR=mKFEN4VAi$QZ8>Husy~T`Kj{eQ&)? z0=qNRFc8{JQ2@H$Oo!~Ft^VZG78BiK5EFe3W^K2NpvfQiXPp?!@;0W4<;_u)cOG}o{Z66ML0Lvk zkT!l8W>+`i3ffAk?nIwC>gpEhq(r28UOT&c@q#Kbs?Yd&ZpRHM7`elpuL)Qfh0F#oVXw0iCc zDc`6wnM<^!#?Tyml@>8S4^M|szgECEx`pelt*Cw&;8W(mms?YpwxsxY;;g)u*a#bq zhVo}-hO5-VF&3aeWtpf_CrKU(hmjs4UTb_T8Fqr*d%H{j8c6Y}lo>;cni|lhh zsxMED&UEZNIa;9RX&_c%|A6Ykf8Huj|DNc#*Q_LZ9*@}sv73)fkGAH#-e*+*AOG^? z9=KP(0EgEJqP`w}2(f~?o^-qy`RUjnd4Jmir~TjR12TY8DDCGHEg|--ALpLj{6E^; zL{Y08n{~V!ewg;2PPUh6Ukv;|+FPLZ5SmlW%)_@gHrZZ>-)b*c?V)bq?|S(5+9cb{ z{;l>t?@D`UYxqm-KJ0j3rVuj?{(Amadv~e5mTK?z!?$-svb~(&YA>kvTB*I?9lpKe zlkMgGR(rd$X)j&veZyPUhne3uDCEXl@LTOYqV|ZrK)lq$w>Kx*Ug)>l8>#k=PhHfry0hi~tWWP9U&tGz4Lo{Z>b z^x@kZkZf<#Z?$)n+S3`1X?6JazU%CdcgkLHk-+jN;Uas0}ul7zpe0yz@?LGWk?R|bc?PaOG54In6yf0J8 z;qQsxYVR(!XAg*OKYV*PB-?wA_I&$wk0owDj-|B#{>CmE{&_|VuLQz;ENkRI98hwo zAFP%)jrpLnlTuF9sYIlg1k7$CLT)*3*HDvNoj*hWuFeFS4);pUg-1WxvLu*ApGK6+#lk;K`iSD4moW9 zI&Cu#clpUV*h;kmsqzJM{k_1 z)l7hv@K@RGVTy#-Ra&1MJ~v01Yg<6ovA*fN``o+)9jzR26S)nxXaBs-+~;DhVf?inV-Sy2b(Dww)SIE2{cx z_Kx5JoU7~SD-=i(_L}C?&JDa+l+y3}mpF}ITH0*EE?ujgs0Yhpd|=f%=xp6s^UMl# z*+FXyy90P@7hjDoXjNX#67bS%>uOWW&JE$7J1fO0WkNcl`z-SpSgp`mbqd|v)U1vi zUIeS9vYPfMNJFZ#PlA;2kVQFt)A0Ob=pfg|h{{P6`5DipDrgkJ{6EJ*hv&V&)?FDX zZS+u4ThIAo#lF;e?PC|!2bPY_N+~~(5&ow9z!Bjuqz@G4e~nXGc5PW!X#Co>g$P`7 zwiuys5Sqf~;cgS=on#}DaF;4QHiR~gZ&tZ=7MBZeO0O%tEG0Wo^)w(qXUm5w@m;ye zYlN`2v(k$ewJPKtPr_MM*325Nd#LWG!&hMG#=wj1NB*=jB^`WM9Y;fTD>K6<2bK=u zg4o*B%9`09CptC|1xk9eyp=f3yt#<}-m0t#EZo2_i=&sP2bQi6yx8V^09g1ANqB`J zjFxq-t1T!it*Zq}9iZ3MrW2`!kGRw|-Cy4miVfod^@~y~H_vJvSh~Be#q{*D`)E6F zCH|DLvSxWj&B7l>RwgQCFUVWT9R{Wl&;m&Wj7sM-yfQwR`PT42N6WevgVW27q_t*=){fqF|2ncO-kC!3=&iM* zEqW`SLBiUWDPR*k+C$4(Glfi9xEs*3B#0+T;Kdff$jXW@I!vtCpILSaFT=M|DeBGM z#z%ePoagC-Am0^-ngwEdkq1JPCP17Sxy{=f!B3VKQ=Ok_jEWMyvedXrBLOJ)<6<)~Fzk6k%aA*2!fyy?Vg~ZE9w>U1tveXE_ zXD0F6Xf#bR-LVO#6Gt=@Ok$m9g9W*BCmnw;+p2b>Y$hDJq9lK3pt3#RMR1KK6W%7U^n4i4D)MK4B{apd#_S=W(^qtd z!DFIJ`jm$&%qEv~rAt~cn4}dXHN~c#l(KP{#%L+OulT!tUT!rP#fpja-ncZwDeE8a zY9ZgEqk<`kHj%Y9O1gecGFJe)S3x5`nMLXC1+m-gRy}^B4aF`)^baZfv_Wd3KNX&; z=J9+}{AoBd>o~nM|4> z?94_J`~Q03=$5bz{%OuaC9t1gl*C-OqV*qM8U04QNV;Beanq_ybNiTTutUT{(8 z%B|rO$`6bURIb%;ruwWvB^xzF*E>-8FFtZ>wAEW5@bIy@8_MT!EjAP@P7SxH+Aw?9 zg3sZi^J(PR+?rtd9G*G~#R`;HGka%uWfjJ{=?JJ9iz`_D8q{n$wf7cu3T(&8uSgo& z{A-y)24W%%2rRYZ8*uwP+lhzZwwXTDZ7vk2U)}lT=%gQRYmKNt>5MFv%f9 z@!p7A!N)IZIbdw~7`C>`d?rVO45tt19jH20Q>9K2AB4Md#%nIM=v)&z+sO!Fp63!6 zZ{@0+|EQY{b_CnQeej~N{2Nz_{{%7MS=Wz1vOPg?41_-? z1~Srz4(%09S#s)82RU7)4PsF%e?vOK+s*y0N8EF$TxYe7e@WO^Vzu&h5_eVNQGowx zZ7Uie#4PL0_7qs!9B#?RIK{TqcO=n*vcn$+mJarI%V2sLuga`ymVnT2=r+aPW;$}g z*D1(S6M5QUsbmSi_imxsboRO3fu${Jv14HAPPXZ(75^bd{It}G6$dksf7G4rS=gS9 zyfEOh@KMG29MEkHR1Je7;uA<*+gyAnce9Yb5V*N|Wd_3^MX-}WRS1Grx@H&-bKfj2 z9eA=952C3}teD%77^oahtp@2j{t)ca+|>t)iDk6UafbH`F}#1H8Gf=aYEg|9#;gk{ zHm7uJe4o-!t}Qq{Ud($7w(f_$ZQZ+7cRO{7DSo!gS>bYSR?cpm4E3e!@s`b_O-liP{L_n8-mh-~I{SA1Z88ii6*7}U5KSTChzpSgz; z#gRSS9Qaf}hbYDgO_sYRyV@qtRg=f7Nh>ZBas{{6S?1gSpe9t|Xuv6RxJ6;4RQ^4` z=lW+T=GcU_(Fo8H^5%?X?Ro+2leVDXo#x{o_Hd*!EVDTO?Qn+K5mwl*AyA7rr(A0X z*mIQFzz5IJE{;ENiJ-XmC|c5qiBfdtZYq^x?=J?chbdvYEpkVeHfd11creH6%%*Hc+F|pT>8lMF%Bo!z#$JMPr-PV0cbrsjw+^xD> zo6^HLrn+IoHKCDm7Qz()%+vozRm)hxY|79YUa9{+ka2Z)1;eggutFxz7=U(;m2UjP{)!8(XI@X8&DeCyRnLw@hQR-oWEB(!>7A=pe zw3IM2$CkEXrc8k=v&xm3uQDI2%$Ot;Y?T&@9)jXSA*zk5bEm6wzUtJePB-c#t})xs z5Hk8raoMMl&B9y$hDtO`w(3vFMp7L23s%3WBImZHo2lyNG22c?PuI>@Bj{$QO1wh% zIB8qz@tcXzj6BD#+_$ej-iO)^RBU+^u3i`7hGIs#Hs3wPqGXBMoT)aiRh!;Yu`6r@ z2=tG8oPB4=Tx+9&>+8}BJR}Qo#5^~^vj@q*<{5dMJ?a=&9=$FghvvZ2*U-5_728si zb|=66$`CXqV_04emniDIP#fQv93XuFdrWYGPtE&rRddON7WzEkribFmD*9Q zG3d+Ot019)kdbl7w?g&As@RgBu<$2gVI#SRMQx0P0 zdhVgRVaAD>sgyV=mYXKB8Y@F;-{5XZaYIULjy}QDbYW$ zbaN4k^N3(wt+joOkZGh6iNn3D?1Zg7^SK-I^9&+r3Pint_0Q`KEGJfK_IDS95NL*b&Csq(AlG5?@iIW-jiM zrDv&B*W&v0@Z+y&$;N+xXR7bc4ShR!BOef+m9E>IWqWSuwyoc-nkl;^ueR)+yrIp? zhI5;)S)?|xVbM^@^psj(S6G??JPNWK3q81TL;1eeGmi=^-4qNwx{=$3ffcob7PY`k zyslYO%*3w;D9j&ZA=`nxfJ1s2iacA&ebo1fsaUJM6Lv`l-S2F$cz?k}pW z3oLw-q>|X6W{FNoeunXE3L&s+u%1!iE()O2d>VLh>}4rGtx{a3vQIdaIC8<4P6gRo z_Eu?Z#3A}SD^NAdPVcOGK4lmQt}Qr({Vr#juEWaW($ltiHCZ+?7s^qhEm34pTF{5c zn>CEScA&Ie`_mfR)fYh|V-`_=-k-G^&-Svn6TN`$km_b>&YvSv?*_FS>BF>gF{CJW zv)$Hkoj{Gj7*JVLc2qF1Vr?)VleYY&r6yS>^`5!>Mky(9XY_uYT%?!wT$oz85Y>^u z4@Xeg^3Lqs8sENLJ}0e_67ANX&PBLx+3P3nhZZ-Vn-W@3&+9w93H+Y$>zMz)cX>-v zIA2thQpxkw#pB<$K3M*GEbo^EW-d%$*y0EnG2~o?`CW0t)<8LB{{gJ{2qdTE`W-=e zC7w-UN#vb)Di{bZJxiZ@Y8D*Dc@mGMQpECl+a5ni%yS{>A2`ud05Zf;?@5y<7M8k5 zO9ybUI-vBqq}wHwxboFWK>pHph&FNJtGf36l44dTj(Jr)_@z31AMxr$rRybt-0kL{ z-7WX-OGik_e_q`&1y7ncdU&jxH_Qwhap|NDmu-zDgn+it{&3YZq9m5=WNAiiua$q9C*E>$SW=uKPv) z-SRVCA0g+l6w5k^*G22IPOItU-aD{ZU>lpkrZ?+8$$?wyv)YpDXIw_c!+-EH_Pp8& z6i|F9sqa7RrG7>#X?@JS-7IQ4cD1N^YziBjb;o`Nl;Is^#X(;@zZr}rnnikBMN_ikkXGEmB3b1IBU`f~n>1n)LM;(rH-o+c+lC zoaCDHgWKSDmbUjtNXGea*}IHA>$487(1>Q8A9%5_+Zo+ccBZ5ZOeug#%#>G`J9scp zo@57mTDBeRnwJ^uDWa`fkN{x}JIPm{bftj{jk8_(pS#%d_o)0yDt{N{>$8reN1~p^ z(pEp5RC{-s3x&9?iFRQ7+<{g|Hz)4Z+Fm7{Z;TYye@z2N_A<&i$$K&zL62=P>qt6* z@=5v~j$B(4jYR|}FCU}2y;dV1lyKWg7x7OhQII`kXZTt+y}tgGlWpe=Vz$~TGnO^# zX_q#MG`VLa%vg)F~yhwyMUy za32Wm6aw>vz^U|TiLjuNyzItPH^q&I^J(C3OrMSJYi;rhMc%^mZ}oHqsJD0aE<@a}1eo7Ztu$OuFvWZW-P_p+s%=6 z_D-|8PwpYHjI$i1{dWWDQ-bt4M&MJV#m5EaMprc8iXN$=&!}isqoSe@P&ddG{i>&B zo7;9#)T+amG%D(&u7`-uMZ1F)npUprldkGxs;YHux=6`WFn}m(4pU>7q_q&$&rhr*=3pLR- z@l)Bge5_Jtc_}(!aED(LT}rsG#GzPL3V=e{!RpOOXy#houY!da$iN@VI&{4FKhJ0H zG;ed^BXLQ^x-^pCK#8<<-d3G{UL7t}s7}^($E74*Gxf*YiFw57n_Eu-5AUkYpm)c2)umPDaXK8^G{?_Hg(*L#TBSkjvK#q6hCq7PKwPBS((f)GtG0$AcmEbNw8 z_LHP2BK|u8d49$Lo3XYy+P<6WR>`ENaJ89iwb$^SyE(2vlIC}A99w!=QrMZpIATg4 zZk;T)H>|k$F(k9sJWELz5g}N8mr7>Iy_iWk+ax+!oWxvPe^pz5q=bEf56qR>noNaW zgE5~v_iT4v6|Gg?A#kGDz+tMc%XnA?URB9%On~KLB-AM3XcFRgGC(ui_1MYvc&2)M zh!TlQ%@sChV5-aMK#p+qS5*%)WO?f+S*zygT*mA7`k0!y-yH86U(wy7=DprDe!uGX z{k8g{hY~B6ip3-i+{MvjiX-j21##~Qvhi|1Wk_Z*ADN_dPzo=2NX{liO!lI-<@Z4NfabzT|SDSAL|XQ=gh z-aIBTFVUVN_DjI>faJc^ijV4x$K#|Q8vj}-wO0CjIg=uNgxS8mn{4I7g|HvE`ems# zMmAGjTrr>!rQYHp5!;BqU94l>BN_zm@zFU3P>6}%RjwdX0aV*%(fmCK; z&Pge2qu+Q$R6cCmz%yrTu%fnk{BC}l2^}pNwgdn;((S#NxAO(YoZ}kJ`&3Hu7JZ&8 z`_W0Z?4>F@Qe{UpF6IzbTeOR%yQgE1WSH;20j-VUfi^;z@&-6=)b=n#~n&{>5s zB31L|B;mV)k=h_r(6ef5V6muHGme^A5*>J+>VV|P?!>5xOGN_`(I3s2hd zvv~ZL^RS;$gtro6E&7D+vyG1}jJ4^*cv1JTh>WLc+oV`bubsSC&C=n1%-kN@>3h|N z|AwC8{6{d-gzq2FJN$?$GEenoi9|%O#JudB}Tkg2txmlWv&0XBmAVcKC9f%t0T{C5DVTRFWPS+ zIU)=$ajmAtiJLpRpPc?#K{a=C{Rbi4&b6a9qh8!iD_e^wY zP}V&AlKf|6d<|3{RQH%93PSm7rhN@t05R+Np8gSRuRd$!KO8Vwv-l`~x!V{TW%YF8 zk8Hh_{9^xo`FLF?qc_jwyTh4$;~JZ3dyHLG&pFlXDRDzN?j#B=^=H$rNpNTn)?%*6 z?JCkF=T?be?1~Hmi7GGA~*N@m-HqVKSyubKD}j&m-zOm4jsOe6>JRzXT))dnlSQoQMa`FK?Z8}b z95A+QR{1TdW5d@brkD@fS!~RvD%e<@%{bmw-R~J6<{K-6NToz7f7%rt=8BG2(T`Mg zb)%yGRZdkUg9qeOo?l+p?x;2aU>57tWI54eFO?>Ui2l#XTv?-Jt_w>2a$f4rMo0JQ4sK-;JAkTH3T=`6R7+WI07q5>s{4dPpi|iPb4CO!haqm+43Tam_Kdz zWJ_lYT2mS;g6M1I&3M|4)&u=?&Riqk;QdgH*n%9AojB6tZ;^uvjL#;j5G!Wx1K5=b z_9<_~5w?RX)xqOCxc0q0d=Y-OVAOnbA~CPl2JUzeYq_;|B&G*newu0<&zYSjD5&&Y=Kwm%&#uvho|5dNM1Cb@Q~V`EI7IIs6lA1Ybur_=GBY;Wh4| znzd#8^03m;+r5|h^wzh)(px{O0*MU8@%*)o=*^X%=*q`CgXzY**DvI!F;q8ad2PcG zqiI>6c^ep>@Mh!hlH{3H>XGMBSYkc${Abrja%t{0gWNLAykFzS@cv@b}s1V^K2eJgVT( zkW!0n5|3PK{L#soF6;ZtDLvhlzQH9ey_ckMB)MzqTGkgbvUn{Oo8Z^pE>4cGtsURG zcqhvGIfW+uwRG#cCy5XhtB^Bzg}qG747#(<1CYHhFPZ~#D2QbYNg>*=ESpf-_FDnw z=nUz?tTadO+Qi6uxAk$51Mw-y&8^F=kFRb5;KU{VgltGoQDT(&lk4M)de+ed*HA=# zJllpoWR{B#1vQwboAh-veVH57k#$$fON=f=m?@O&QeGjHku$QbyU}%boVqKZJIp#w)^8iych1xDDm`OWYKqD4454vZhGRG}i{<>xUlX|F zM07^SEXJC(@^Oe|97PWxC3j25l;tPTk@=LKy@kG9`~-pEe_YY8dF_y*Emia+6@9)@ z(XAwC6Ppi!64-3G{@oXQ=J#I<6Odfw-ukDk_Lx}4vQ*12uBJu0v;3m<%EtVnr#kQN z%FCZInz7*sZp*hn0m27^yvtN;uIa~fGXEj#8D-6GPu9A$2l>{2-ob_kL6iM%mfHSk zjD@9`bG2yN3m+x<%M>$(*IF#AUTIzkdHm&d%U_W3Yt2YNaBs?MnLpM-YK^_7rD(Ru zu%BbREftv2DNxL$Oc=`zTK%#bo}r#|uAyfyF3SzXyo3>z`u#j)`(bCOHSyyeBe@H? z66P`>6dRf<*Wo}RyD(m@o-ZnfESa)SC|ZxLF!}zW02*Vrk+^x7hQvQ4vZArTKAs^( zs-j~b4yr#(UyaS8iyFE(Sjz4Nu}kYSlK~i<|6M<0S#v*tM#tKuK;^%MX`4K20?9>Q zvWSWYFw5{u=a|df^n-t@Qg$ROeFQ803V^XsWIkb`+9QO=vYx{>Vp41)L38)@#4Wgu>-4!>9RdFCEkV91{P&6 z4q9Jc`{FMYIq75IU$fhr?{+}t_7kA0>n{(Wy=v;+?^12=S6ZmG7E=rC7P^NQxm4hJ zuRuYvz@BrJQ=`m#U1rsII9t3kC4`;=T`CyIB@l&=y}{fHDKG`G6T$?h4pho{hHc>n z@E<0WmagYiy+E3l{ODZ8mk3nK7z`0V(TeMgY+6}nF*ol*k+eU#gh1uZD&Pc_J{>Jp zRc(V!#ILdq?WR$4{kM{O==R_mx~<|JsQUOEvdN48n;#9Rv+^|Zayb*gW+v`!rxNja z4wPPYfk1mX$AR{&11-ZrVm$_dZ$~=|-%l+qz=6sI4e7rP zDPc+)VD5%#Aknkx%rZunzbbIY&A1)J+tQ+$xtt(ImKQdym3Y%UjROIbzKWNU;Xirr z!tNeq7-n@#d*9DRSCNHJ{O-r)D|`s_rXrll)>f-62t#iJ_Tnc_i3U`{WMw&|va zR%#s!sH-BTR zrPXHYHKGQe02*#d&{jh)0(A-xg)-pOV~7#zD*Tn*)(I9SOVMvC;lTTa+;SKBm2X*W-97At8bwF`GvX4{tQ$- z2auuY4Dm>F#-)O&7TYR=(?Z`qQ7^BS-5-COQnEKnokAQs%`~rd3`ACy+&WgwX(tAA9v5lD5C#i)%l?*-RP%}Ft2i@j;GiI3OeW)L|)AG5?+~zVbQ)X8(%|2o{ zYOY%HWBFtJlbCE?aAp7P%I2!0a4kK?}S`LVw9i=%B& zT)*J)NI`$f=fqHp4XDFh&1KFxbwnHceQB(39hV+6*uTtMrsw@sPQhavjY3oCZV?}O%<-SO+l+=>52mYLTe9H7Tz@OkFemRrQ z_h)>w!+iXMeXTF(V)ERoCfB9CN}m4X^JV{2KBe|N=@x>&h~2m-6}y#ENOi&Q=`XHTZ$foqo_H2$7+{wg$n zMSU|TO9TcXAmAPA>vxbm%%jlm#H&i9NJ5B%z6f}_lv_@1oUnz5?GD@dOhKIs)aOT9 z=tc}y6hN}#YA2goI3WG}iU9*4Xrb9c_nkgqXAn|mDF6GiDmmkabTK+~cE88!xwKdD zwTeVfU&0TKI}Zu)X?EZyxP5(~X4}KkT1S9UxEd-+MHI| zI`R*cy^Bs~+FWbJh01&FT;Mqj-+*n&ek0A(2KCRTAQ{kq&<@z#ev_XUs9ZxuDIz^} z)GS72W{0)%IX>oxeWXxV{|d~%C@27&PLX_n(VREKK_H!i8al{Y0sxW$UY+8y`jcf{ ztmQ_KVN6RqI=Ltyc(~%`k8So7&X$5r@sHqfN<#|g*Rrlis>f{@QWMq-EMDzZq(HB5 zw<-E(T$30sm{w16cC2#lk_W4Q5Q0{60aJ``oaLt1pghRupo<-Xy>lL{mRZdF*(*1R za^b5jVmJaprW?l)OVK~>2WTPYLNx)aoP_IuA8{l|ny|0pW+&$mDtKD5Ai+nxZ-pmO zIOBLn{#o%e9mlu8>NFo6iA9{$x0JAuskM~$EIgzE7x7Q2U|uKqFWmW0NIR_bZ`ZEcO4L9oQk9PGRTZa`unfYju;B0o#4iKU&VBG$_|MANQK|1t1mXj7WY z9hTcld8r&KYg3@7W6B)PifJ@CoVEQt@H0VqF^;V?)1Tf`pLppP`UNCxJ{n^82KP{> zd$ddeF&?(-AnmW~lZ-xx6!goI$ZO{D{+0{MrHd6+AD*Ve@|<9GiYiHJ+Hd-ZOk!Ez zPSL4>mu{Z1w6nzwxrjco{S7FUD6s|5H8ypiu{{AquyK1tpmS#(m^L0-{PwB3lB%hH zldwDRjqANYs*0Z?d-nSzTkl8S#Qo?(`bym!8d%S?&kU_C9FRkAqtYEz;u#%)s>BKz z9K%}ehCA}cY+}cU#E-Em(k^oqZRW3;^PsaS+Q5|!Anz%%#=rlD6%WjYQg2fVHh%bl z?OMxW9A;Ib% zDmn5!OGH-VO+G8>wf|nBYu`Tfm(INPMeQ=5rGrLdmHtW1;xFwtC!rm+nP{a4>AeA1 zzMQw=ZC-QHjJ|cy566>7m=g(vTTzplNUPS7o^O>D*`}C_T=}XEv$7(uRqPAQKZ-kv zv5YR8sT^%lt6Pi}EAe#B_|KkBDe=94<#*bkFORC0iffK^P3iGH`NV&AUoBC zdNb$)FaFnR4@roeU1FRWeXE6b8Glp z4nDBA;=RGfRy7&f2nRpjzeWiXXL~EC8Pf~KztsEv8T0*h3sth0^@V6nn5Md5db2Y8 z|MUV62p-0n*}!c)Aqy)BR%eOsr*C47Mu&B}Lv%lZn@`6b$Cy1UZuT8&EoD0M}PANl2v^s7QS6iVcni}7TDc@6p2ZtDv# zU|au#Ae)NkiZ5{luDB%mEGt)T(>*gvQb=$P*FJTC*wDm;&9o7Vr zHTeHw?M=X=Dwg)~2_zxNGC_%ef{YS1QN%yUBfFjHYf@X0N zU>pWeTyR5N@Zxe6_ppcwTfmh?kmag?TOUUhPy#_={_k6T&MZN{&+qsA^E@Xr)2F(- zy1Kf$y1KgB0v5B_=1)nogQ_L(c4;vBU~pLcH=@7rpA%Bw|DovQ26+Q)O3PL*5x!J| zADAA#wpk}tZA3h1_);pDSYxU1iB z`o+)}TNW>(f3ys+=6?+ULd^XE`h^1xbJmCp#qS@XDPWYL)GtSwkcT@B`c<-7w&YzA{=(F*vh#+LqBhN$#0;Noba7)!r z*uD%kgt07vLkepE(`E9k06uTxsIAwR*|`F^w9JIllR=|OFyVS@I7$;(4f8w7XE5#2 z?@eWC7}K@HIqTyFeKIW4Zx9 z?}KR$)atC^_%Pl8rOo&G`aO0~|Ai|C2ob>bf7B`}QTM$HyWhoGcXA;k~Qmc*B}R zdll{K7~2k*d@yj`a$)@PaF~2clVmvU5;TRr8tbQ@1RlQIxTJ>O^ zZq;OfvQ-JvsweN4Rt3jJTeSf%)LYDpR?&(n{f{PCt>Wn~gQxfbpw74roP|EYeM_a( zMY-RE>r29EH=*PYr0?KRU7ZPVL;}I8;U(mz+bXGFeJWE5G@f&?&;_0b{BcHlIZiO4bzGrj&u&JkY2dcyAC7*2Z~Wgpp8 zr@Hb?Er)hg2wU>+GRXkv3P>xl__z%M+SaR5(zv z%l`a_liOP&%y%@=^Q-S@E6>lN7MEXoKB9@<9eOH(^+V}-`)ViTuJo8Sb~&Osd) z{WIhi?vepX6u%{kUx$9wC~gn!jmVR&z4Y+peXtLrd2xu2L6uDqU4x_;MAJ2*qv4xD zG>3=N&N0zE-$e7mCTQ~VbsY{q19?N|5WSy>-VYW9zp4&@K#uYzR}9WhLF_;dlo;6} z{S8!Y>U51fkG5TiF&48PK>U|xjQ@hDdI+yu%lI#QP-MA5*8_kW*T=C>?EQsxJ%u77 zkUo=DfAGo^zN4p1!CfA0=8*$R5-Z{HAKnv~l2!L}E8q7Cp*wM{pzqji9=GpUskb+0 zpmTiX-EE%EzVZy4CnYc>NAWC6eSzpC^#xf(A3B!5Ajf}WcO-K>g)+f9=LV*94>^z+ zI#cqdQWs4Q6yRV|a$rhMC}|Z*BkX71b~umj^B?}n^C{A{n(4pdlaDne>G^8=KcuPk zPK}n60vmIqGnb$BPQg+X{5976{DRIwg&%7^O&;GpXsILG(uvSV;#-RG*Pv0hCe)bo z$5-njh0-Wj$HtK}oiugZtEh3hd{ZadJVRW8xr_J9nHdpF6g~x#hcSg5}?MHO}f%5mT04;V0dk#cBJPuHEBW3_KN2(e$Q9UvO!Z{FN$*NIN zlJ69#n+a74C}XnjWUIe^yTlycyb%;UdQW>B&y&e|(jrSf$>MDmYF7R|R+7aK)%Bti zVTmcVr%LUEC#nuo`y8o#y40>6xmmj|)A)9^R(mvm-EG#6^N1*M*;1hGtQm;^##421 z>e0uhJRTTD7eU%T#r;}nZ!qmLOGhh0UJ8RT#&lGKRlfvB2UADagYf6x04Q}VTUuVk ztLw4NO4*wS`(_SC8rBhOai+;UjZ_P0gtYc_K-BH9w^d$XEL6zd-ZMjX=SmLCJiksr z@Q?nmPWMF`BZFp1L0a)Wld(I%Hm4z@y{calT$WDj+s;laIa1Fh7W z)neFzqXAWBd-ZFm0rJ9Ly%=8rWUqcoQg(Z;bOmV9^Yjr$3=E!#<#Uhkz<$eQS$P3_ zr9OWmpg|IZAv!fs1}{~_N5}9Z&VKwPIx40wl+}#4J^k{(| zHVmqtue`m@UR{W97JC%vQS%GhYruaC)O$^G^zH%6glV1P?oUs=^WDeX7vh_hd!Wuu zKM+F}1($Fz1>4;6t*obB;W5%uyi`~16Gemy&vGB3PXX~F*ANRr)U!JZw;J)OXkP%|#B1 zmsg-kiuEc=hoPuh(;*+S1kgh&3mpZuwoXzfKc!UEn(-?;fS7p2473J^h5oD{P8c@@M*7l8p#x~nN{f& zBK#yIKSkK$vqF9&-Uf8&@%V0Q!>s=N@#Zjs)Z-j70^;PE0xG1~%;U;`ygKkHch$KA z<~NIjXIZX26%M}k%-QJ`D9fISf;q?ok1oPb=9{QWr|5kwo+@w!R;K5oK2%0of_u&u z5Ha+pH&b2lm2}Zvdxgd|7lFpx@zS8a1;7Ow&@+KkeHRYl!O5UViNY?4r#*jvnu_hS zlG@NI+53^^{Rs+;D==pf^I^+5gEu8O!~%IRycN+5b^sCe;g?eBAA6+I0jzXAGT`pf zu<;%NUeW-&HNZIp_znQ74%lKvAOmp*5)67LVi()(zXSWCXSqrSVOrb>9}5r?d9lmD z(mq|KL*J*zpMsxv9CHGh`!2_EqUH))}S&<{Y4N zQfT5A4Gn?ZMBmX=`%LolvSx^yV}HKR?mG{^8uG^{cn6X`uOTWu_lZZe2!}6khJN?< zP#^wRxBqM1{_bdA0TJDYW?RjBnaz6+|5(UvW+4Yb{3WUOYIB2EU2Uc}BHed1Y07G7_C>uaX=1f`dEq_GGx3?of$X4I7LJXi84iO{00O zM)Ro)fo3Mr>>Goobr7uu`3U<;wX6$f^qOj9)pqL(7TrXk|UWCABgtCOdmvGjZXbN>tBX)Yf#da+u5-M{AN5G`KLw(I< zTp*dOo(yJjc+NooXbby5|K9M>j^qtSf&)-}etR_DR-ZqWA7qx=us{D^S)dx95hmDX zXAe#iX$QX~x01tD7ccx@p2n2I=W0M*Wj^RV(UHyj-!esqpnbd;0-}_&iW^rGIA~tX zVui|w;78O$aK~HhpI(pNt@HctfioS>4X({Fh%8DPijo}a2vA{OtV^lCL~Nc{=xjI$ z8PEa=TNS6_$S3-oagA~$QUw(UM6Gbr7>Vg>I)hD+5A@}HT8qZ2gr;!cJ)Hb8bfCCA zxCk(?_1mk?!0$jI;*s0uaGc2k*q-;PYe7Pt8#ki_0)&)_R5tr5^Wg7y%o*N7^0z@2 zZO%a0K^%>=SIOqX@SVbNGvG{)*=Xj~V7O@*s>V9NsLS-r+w{xZ`EoQ~sv)~DBuz(6 z23RpTU8t5Cy03=rN9dageRd4=!vGB3r@q=DwWey&b_BhZpbaR(DwsM&=$eRiwr>Wy z*1YojD8dAx8)&9=!gbL2r8V+CpvzpjTUzq|P?UKW%X}`TOzu(9wsQwiij=(f{8-C8 zT@n3ugwOvvmObM2RTrCk6|%t+9C%e`IH;&+zhtVYn2fju(_gN>{mANY^3=WRTi$>h zY;(HC@=9XKkZ69>%I%ikVb4h-+|H zyo?Dn7a`5%RFVHZ+cIY>5 zzDdLzxewWD7J1Xif>;Ae3Epc#M|b8vS4o|#_i86_a2?r!Z}!Jl0|C!v)f>w$b*60c z^icD4(d%{y#SRvs=&mgK6(D03{Tz$t(#&1*iC%$X=O{eVfqnAf{$wqBSK+n4EtHXW z@I|i@ub*_5%qVFI;Z9uy{9?c)2}StmksW6%+8oRC zG8=nuxVwk?>T{t-sz$mWkuD(84QPyo^oK;+!McO85Br zR49d<>d6H4noUaPg2|I!KR_lKX`poNPA;_)`Wu_M0~0nr6h07Lgdg%4G@N|Fl` z;8o03I%`WpZJ`e^V7ylZ60&xDTo5%#2Kavb!WHX)zYgy}E z@&C>tU>gkxsS!dL4eO+U%o8-ro+LT!Rb&r$pj#0xiK_Z9zS9E;PFozm`M@41t5GL- z6|{Yh!fQQ_m*AVs9oz6FGJf^AX|B_B3jUDklTpKP7IRXVF=yKtDV z3P$sH(I`XqJ=^P-`TFHpzI+@nRd@8T)sqr~t6Ujdq?|J~XkUUpL(qg6P!WB@)hZ2o zV4GB#O3)Vwx*fGyrTTy^O#1UR=sFGhoe!Y@A?PbHpeq4NDLI8~XPNYWJUe32kNBWF z8}iamP4%8L5RQ6@IQ$RMKpk90cVXbL@^7Z}i< zQpP)IVDI|7yX5(goLH_ z$BhBc^#6tbJrA5Qi@=Yb%a%oIkeCz?< zsIB&YCN1x(p@$JVK1V&$!{CU|ajF`FH_mpUcg1J(sC|y39Lt zna{DzS6Sxpm@=6q?d=6V}3j(n@bUNw*bm}q4ih=_>3-%}=?Ul>g6cas~wx+q!~cX>}n&v2P91crV#zgk5kaY;?mL!S_RR&*}Det4?D^=d8rjU5MI+ zg92gikBHdSd{V#Icl=%{aCxUAgmBeq_nSWu>ALDPpZTLXibzCYTrWHVcKX}usdOMF zE%iGcH!$KhOQ+ma4g!7v0^(+sz52~*`b`1ftR-n1;dfzm*DWaDIE?8D?R8AXt>MN| zq~yWrZr>hP@X9@OX{q}qiJ_{nBjxn%0U}3>0l3j1o@nYHt|MP(Z?vaY3AOF1vs=-= z;k36)Y>R*HwyAuz2uXakRKLn=>yUR@d{=9}+ho1Vm3Kq=Zjbryfc36e-eH+&Q~;+$ z?@TJdJqHKdXBYZbr@9h$x+?2fe_`F8B*ZjK%|o}Pw(gBwd3_Gxe`}nwsCN{?GwU07 zZ`=y@8kh+o;XB%^`rsRSQJQ|#70}Ho^rN$Vbt$giA9_QQ;XZ&FZ3!D}vvH6}xi>$f zlzWe){c3FNTusGZuNP;WX^J{ACSi#eStnw>D0@6=nDOW9MG=+x|6_i%7w7%|BEP&$ zHuGn0#-5k`nOfLs?2oK=tS&661rtZqzB>D3wRtJEj!inYF!7JGpJ%Y5zzFEuKWJJz)G3DWJONV&Hc6ef{+e-TTL~B1Dyg z?GdxIU*SZNYKDbp!kz8S%%P>#ciyXG!{Ar~HH6v31zh)hp>SP4pzW+lC&Hz-G=?*` zouvPkLI2$-O-$8I>~xxS1NrdC)@gQV*dqW0hY(~=B15d*q1}71dO4jaS-1?Jj8~LN{ zWGPvuc}HPe+3$hM@!!Wym7^-1$}2r;#0-3wX-y_S(~4XnpRnrE(p$`bLtQ1i)F}AE2GF_#$cl}7G#!Ir2Xs`pACus)nHV<`-G9~#y0dZe2$r85qWcwP zx2nuhKNAct9h@@q`#ubnRXSfs=?{xPMYuOgj`FIt~WLG|iv0K-s%gA3^2KOT$p zFU|c$B^&7GIm+x^5}&=h$Nb!6e%6|wYyjUcGC${;pB3_HaRaU0{jq&W<^iQI)f{xM z@7VeFnQvfeDvTkH>p$XG#3+moxj69*n?b)u)o;B}=Z=j+okgjjPDkt^so|iSMI9e% zC3PI^cuzkWQE@gZFPTBGMHpMFI2F&gIK^JE*_XM^0Wc$q7 z5H>Y-RFaoj%i#i#-?XWSClG1&D0SOPL_y%;M!Y&PKc;~PYX3$9n<8-`kO1N4{7VmuOl**u6=H7 z&i{pSn(){4j3Iog)kF@!{~7tFgs=XmCVZHpD7ezPd$ACq5n@KhkY#_PWx6TD%{d9e z08TJbBDNbscbKNt&$v!=^jD;-M{r2H13P&!b#JZtJ9Wp+bH(a4lq;^Ka~{UlI#}X{ z>tiI`C{^~i3Z+C;zW+ntG{IuYC{0|P^$o*7G|Fg!FET>86K4tEXJ^N>ceom`^#7r~ zt5B1_9`3;Q>gTcLre34hL9or9)iN8MdJ5)=&mk1l`r7C`QB9gp05r@!Fij%AaVtJP zfCh;RSMhD$X#DGn2eC}TtMF{cvjfiwJj(#ZhfT7+H~mb*J471^kHB*?o;o~)If~C` zu_^c#o^5!}geYSAWXR~IW&G7|MrDiDA4X{H@VhpmJIG|7R694KJIJ8Y{sn)o1BkkW zoj*6u4{U+Pzbp)7x(yy*w_yt>AwPHd)Y51xJ}&)Thy@BN$i=8>XtEZ zeXT<$ZN&07)ERKgp~z6J(&r`UMryo%7?hRD@k^?QzpfPrbagAC*@`=n?R^(i68Hex ztFEK)VJMzvpoYEbD*1`_(xv3S6%5to^3~Mh14gQr^75eS_yra)!grts&S$9>TvFH1 zSVS=VxdJoEcO-8s-&8m%!Xqn}9$Aosa~I)FcwQ~vYp)o&&xn;*({%HGF)%aM@HGST zf&Oij>-!^ADXPf9r2m5LP3oW73HAHRfpQ;KQsNRvjvipv?W?%T^rB4l=HrkJ+L6O^ zEyA_90wX87OPpEk$$SW7zte5)ELMc?4(+Qpx6?^BBd&+6_{phOsn}@R>i@(Qm`07Qd6NcR zzy^^$>Y$(SP>%s}g15{@;cf_|TMg5e4fMssXcKnIXd5`FW_FVatBTv3zWy+{GD@8P zj!`s}ufVwhuuZX4kqA8vj;uJ8IJgvsxD*&pgBMApz>T3!Z~~i!_FYphg1>hyXA_-xH8( zWNVmO4bwFO!vrY=4qYr{g9bQ($wb$}1Ob3>BfusN@MQ#m2?Fq`2H2qiRz?7rAOQc- z0DCmRlMw(W2mseSs6uIgX%PS>2mtRj1i%3ea9sp|2?jl8G|sc|(`tAQQq_?6WH?tY zm*IT%F<7M6b8OsN6ICy)5C@koYd~;m2){9wpQX4X+pG7n5(EFh%a@^~=n$)T6z4-i z?@AqAbRGK}QO7FQ(LAOO%Wvv4r0F|htZ>o6+mHK>H=ES9dW*LwTDTO|;0S@Qv6=Tg zTvIiXj(90xd$lbCjl}1D`#9IwtDYe~xR>({qHM1|grC$v!f-AhJQW8>?9~Dh$S*b) z;GHz=>?REhv5Y`jv9X^JM-$3$4%)zK5Y!as>#LTrv6l1o{6~O0oPX65L?tk%0-4ka zaj1$qU#kg#LMa_kipGq0BsQt{UF0<0!oTwVaPj`OhP5E1idE4CRz=w{6+P0lqC`Nc zJKqu7KZ_zDBJMg0+Fwlb!%XyvWoD>jZROwwy-havUb0KD%LJ(wzG1se=km86Hru!S z{Al~mvZ~!BA#>?`CZxGoa$6WrwX&ASWc>#GgdUT2iSyYtfKuT$B*Y_JRjlt2@jo+tvSZ> zzxOoP6AZ0Kg~g!zc%Tp^`aAQ@)~`G-+ImNmCO}KV$!Eh4SgUx;Up~z$e!HG2dKMlJ zLo$0DoThNnrcO1#?PxgAws+lfvZ+096BMJoO@3@uS)TK{$T<7|1~89W-b^runGHvR zbbJsO+}`CDgFsRhPT5)h^MK|Ux$Xz3XmE9() zhn|F<1+gUp>r*=X2WF7WZ%@D$)6RZBhmdOZmh{+zk4cYxT!J2xQy1OPlem+BO5O6+ z>@k`{)t7Gybcu#uNa%~1ei)@#(36lrHKLBZ07bf%c*DGwjfTR`4KGS0#95ft3|(w% zU2Fl1?TJ!C|8`37a`o;dM}V?(QlT2HA@^amk0H^8khw&_6NAA0CIY!YK>TS9RlPO^ z)9YRptWB{=cz!E95GMaTPYbp9YDD-=l35ukqYDFx>8^kaZnz9STmrJ^rMt3s*Pvkd zWi)u~I4_2CQzE){rK11jq?WqzdrGgV0YK+6B6B}?pelVR=5CU~#m`{c%5xA3Vf}jc z6x4`1_4{hv*vwWQRJQsQlnxJnxYlB0gja@t_#oHIv(qa;GQ>Sl`~Sn$4GgKIX~fJ| zCGW8zn9ePgsIdwpxZr_4G|%`f7YxNB^uWAjNr;7OWlkw2DwAvhIsg#z9In zZ4nTmPcThLHhkuRAh`@6Me_2HYcXB-H-cB_F;~3?awhOpdM?T1eKokfuhQXh&1N zJp-n8a8Cu&>?1c~I~g(hpcPACS9KFaFTG@4dZqR!@@f<+P^aj!2KqlP^4ClJ2OhOh zc;ybfn!X30jA?A5r@QpL-(8N8bax|Szdd{Zd0P$E_ z;w1I_K0HVQZaH=;&aV0m4uIKfIC8Kqty6U$m?LMWT8~s2Ij^F0qO${^=()tyGVlNbZwIP0C6$t22< z&r#VM>av!1+>Ys68ZSKJ50w4MVecFCPkUg3Lm=c!13Lhk(3lo?v@Qh@vi>co@|T z$F$<#$s0Nb9mH_Q^>Kd0qN&9g00u+V_6KmQh@{x7W}%ckj4bw!x(K;Bv&7;D>4N=p z6TMeAX(=KKJ!KfNu}@T;)u%5ihG{}t!~+Lo}~(gT)3G|8>7i*%;&om@ZnfVQAp~uwmI}eA|j+dnI zzq}=lf1UXjFy8@`XEmM(8eDG1@GieC zA&_fjo6g-wd)@YXBb-NY7yl*4x91lNG%k8Bv9nc>Pi_=(6>gb!aLiZ?uh)HYvEqNY0zQe)UoO$pdAOh=(}QdB4(C4=AfNT&G04?U83tN>^h z%UPB@1YLt|(UYEmtPbeHaGXJO2wovFN}YaGOO5%!A52M61E{Ew5jhS{l4E4b^)YuT zLgUnDFG^2;zf^j9`$$mmTa=)F__sBn@54_L=pE2H&)yXfx7*j)+;s3@MxFrDR*Ll{9$z4umR@_K5a90NaGz3EXqB^vqVj^vt;<(9%p+UIYq&#@+CJ`M>awebWi8H+aNDAH$aE za8ycOAcGO7`khM!LF=A+HH06W<@w`eUW1+5)G6~2o;QP&C}08HNXzDqf#|R0n2E{U z3z5zmp4di-!=q25>~knw+(E=s<(mq;`I~h!yU;&UEtK}SY6nQQ-rLj}F9>OBW(jFd zxfZ0!1+2ORB(q3!I}+fd{tH+Q=JKEh{dzb+FC%D+7*J~_+DeLJ{<-kY6kF(O1h7Z{ z>M?mxbwC==kIMNj7n+A7t{8SWo&x&Q;M=f)BTDdK8DA`7y%@P#x8J~@AUm0ZQ<^$t zaya=ZZc4M}m$88O-}U|YnMO}kar4avHuel%mS)I8Km8NIvp-Fa@N6xi9^NYRjlJq| z@Efx|Ofq-J}^inm?gjuOmVT^~Di<)MpzNk~-9^p=(s%|k;=Sk{QqI7b- z;*#Ha=~LMw=$7H>rF2F^-;BqpWOHCsDT3^zT4)_gbOA}|hS?huP zxhd`vNNcNX9&Rkf87voS9m-l!gRX{b;c^9El}0i-H(Fuj6l4x7qzPj=6dcxb4$6Q4 z%Ed<$S*=M3N^5cbI5Z>ZZHJIwb$M1e`T{uqz03~>rBzwe@j~Pa-KA#gED!4} z-I*m3%~H?AWO+qnHClt-ra==3Dp8X?F`%5ZDcEvRI^80{7@8zphD?8&6i9pfb@Jyy z<=(30TP^Z%Y|Z|~R2RwnO~zJKe@h0`U&#Lz@$%oYPk#QhYf9Woan5A;<1-ErzLW*&gI^)^uc9SKWu2$$(uF$;9hb zOG-(9J|n!~hMne-%`J!j4j%JUEqhTj$5Mpv=it4$W0i`%j}$2H!L&6fDSP8UvMc`A zh7iA?EX!uE;@P2StRZ{XJhs!?zv|BYtIkij61H;ts;t=N{YJJ)#uO7Kvf6Xme!$ia+>9ZqmieS$t`) zzEw6AP|rdp2*4;NnVV|xxQhVHvj8p=0NlH2uf{$AI+bYVNZ!u+RW4rPNPL)EKJqqK z-pU>pF9o$%bMGqh7FRI0dQ&n&Sb#h;s}6DZN%T90fcSs z)jSyBv|6~3iBih%n7M#xtuDpDuP6``1!%hl+Mt2f3Q(=4#zH0v&>I?PlLmSw4v2}y zYz?qO1C;TX(1Zz6!kt#qm@N5yqhw&eR?QF?aI&{Ou&rPbUIe$%JFT8^__l~?o|}Ao zk9$Y^zB}$6>GbV8?!DT{gh4nRa@;%6%85`(WOC0xkT6TKU4R|5j*%U-`4T%kwimD}h3*0*ljVV&ht2Aty2?Avn0@ba}hW@nBY$!k|ch&U($qt8JMeO31;MS{PM4g6b4xZ=m zEXT72&o6jVF*V$R&uo0U@!XDQ7M>UJFum!s7U|#NNd`-ukEiMT|LbpD_!gdk=OH}r z;n{&l`%CmvGCM{5CGs25Tv2+0Nz2Xe^q2V5{O-{4?`}MLqG?P9j!~QX{s$?q(7!ow zbs{$87Fw0_qFXL{|sH<-MT!Ne*-HMgM}_T zFf`HVjd~-Ko)!Pf%E0Zyj_UNz?A{S@)7j!_Mau}dV~i`jFV=HPPpG=SWGnjy9#jh# z=S1dQv;E)e**@T8oEAm|+wAtQwtT=M!1$u!-!-1D_+$&dauJK>F)y|* zg5USwneDG!YF6D^lVBj)WqQKt63zBU;Q5;<5FUjV{W<)M2)sygz_+J*4fWj2RK#wf z$DJ;JL=k3p1rv4>X-CQhkS&k^)JKS5_x=+eW03C-u2*uQ z`YHMkQDu4p5}kKE=LV7iouQ$-_if?1NDZbhL9or;Pl(`~b2kLvb12pri>v}?ui^!T zJi#zT7w?9ATMl*0rcV8(h0XNfd$<+_4^=i@T81tym!&NsWJmacSepB-EWQ|JLgt-? zduplGk;mp$;05Ud!Zz|)x_7CbuUyblb%oFV98)1?jbh}?2Ls!y&L)m*y^cg=>9vC9 zg$G*bJ2lNnZk~?tuGEzYmmCal3cZO^5F<_KcXwRwr&Dl=HABHF0>siky6^(x4A;|ajvjp_i*&@zz zeOq|Xv2bM2L~GDtEYnLOyh%a^swd%HfG%6JK)P(V2Snx=QS{HSh|ClEoU6*wAt82n zsf|(U3Q~X=u%>^h@Fc$Dep+2R_QbG)%~c`Hs{H2SYG&0wS<tr4QH$E*XDy;SP6Ke8tDy!z%L!Ml~lyBqNq1MTlh7E@iy@lX{SgRJAQhN0e$3jf(PLTZq5?Dqj4rERKl(TsbsHi&8MH%W1TCigl?` zWgTHB;ZiKb2g$K{E>(BV75YE+uuyUp_rZpMa%v`;Y*}k8|Gr^pZsf4z`?>< z2_j`28YanESuf&F0|V3VOq82rmKCYUsPi0gmQ@W0^R7HDo}f(6a~} zFP36^6$1v)1a2=1Tvn*Z7Zy3*^I`jf80GfKR4B5zvCZMC&vgKpgHGjLbzE+FnT}b& zfhCpHfSE`$dEc28fb-so@r(Tj^gqXfm;xn?qB`$;jC_zDt1S@z3Rhqh40ixs1yJL5 z4ijHFM3Z=_Lg3%Q6rJ;rZzwa?7z2?6qp$20Nmd`S2JGq&#H@vbBSfp(`rGnr!7)&)#cOQwLn3hu`J}K{S@-pJ3ge&Lo(anP1@f@-zd)Za|{vtU8*^;_5T{W zQD@;LD8QP3_zCy6v=_ujN8dh+x7oYd^{2qS&27*7 zwa9-N#@aGK>q}k!<5(bR0j?82+#Al2+)QAHA{b8yyiGFfiOx3|y})Moeh*)({R# z33I=l`Q%&&nUQfAJRo2P2^^wE zc{`~q1!|B$oo7P5F8nqbY1l_}2PT)I>+u8P!W}3tB~HLmPGZI)X0e&FF%0Vg|Zr;3-?8e z_iT_@g7Dz5yrHHc{5EzcTMJYu=>x!`43kj**(qBmF%*hjTtRKI7dzW-;WbAa@4ICj-SqjUw~;^ndrJW3TaiNcEv?HNv6%@Fm3UxfPV zIU>|=YlHdfBg`i1%?BXVP1gxasFwxER@`@hv_6}&@pc@H|Dkjt+{vnoEwa9(Ea8i9Vm$O*e7Anp!5a)(^BmR2^D7B@*=ke97xQ z7JKmU?=-9Sm8D=lD8_GtcGmi2vYA1Pr&=~s8nPGX388|{YK4X*V%9d!z)Q>iv0b=EbB>dq6vP7hpc|UPtM7?;CJOLXOKhiN1~>mlMX@)>*?( z4e7Z==XBOEU!Hy78Uf0)=OMmv*xMUn96VWh!otcN+Vio*px~(h zyvsku5oqB%I=KwD6nH$nE@wTS78;dl#N9<)9S`9=d`}!B_jL0e?d-jYFD~Ybg4eL; z+L=Gmmj_h_w_w}wXFLAf2|qJgsy|Z4rFx*i?b~yr;*z&GmMBmi1p(tVu;*CQU!O#~+Q60ba-L2ACUvH0Bhi^U1fi$s7 z2GRoU#PGWt>aVLgM#8LWYe7&60&qDrVg}Ht1vzPgiTktkgW?oKqr z($+167o!Qn8o~sT+afJ{;EP*?9?MU`7&KskU?f8Q7vT?|2C>XbqlZVCFwjo3VeTA@ z4LhPPxh2n`o`jz?F7rjy|AT5S1Pup2Mi)ZWVEk{yNWn>1^yU#X=)nEbppLphNzXu# zm6HA`F%8-c1w0(x4EF?U=GGRBfhq2s0*Qe8@}%NF@{y)Y*hHC zjA1s=lt0y*gn*C^CD)wX@ddN3j*pQ)&xv=vDSs9t|6j--y#h`*E8uyWq%gw|X74d~ z1kzU3oq_wGPVKDkf9e8R4ORD=7ev(!ryV;}r_1TwU|L6gq%54a&rClG(gMEQ1{3~6 zGyMmh{y4($fH%WwZ<^`bb^1^BX8IFm`Z}F{iv@qbnf|s;U;V8Kf4iCfyiRWew=OCF z8Z-SNoxbaJGrfX*C@D zq1H90@FS_F6+T>bdlG3toZH?O=m!PH)NHqYiXP>*R!&6RW~kTTtqnQ9i};h@D>8`TzF<@SHVP#`~47~EZ%0`YX&-ltl3M$XI z%0>=od-WqgQ#a>6U*qJmzP?6}=UM=R2%h1Ujq{Mn>(L|`UD?P%?m<}`%$@<2jiP{D ztLbpLuQA!v%h%Z1o2Mz#T~p*-O|-LwBB2hNuBn=?Ej3+f%+RD-FbBRKfxMKIHO2(n z(WtCo6V375^AkOu%;RHN2VtWYdq76_@6eO}&FWhW?d*-clkE@JiLKuf*_u8LA4_)V zbF{2c1ayFnmZ}iHca&=rAtkXTAz!hpVa!KsYaqG8@iP|Xy94>u^SVfI+k*RH#9V+h zXU#mYirRQR#0iSMiqlU$(oqoEYi53yx{sp9pN}hWYdtsA=W{8*xvtPx8(p+^8Y9Qo zs~^C-4_-it@X1V~QwO3RqAKXDJuZbroX@(tz@NktQI_@1Kw4<%2T$|;C-*X6m&28k zui+A$FW}91>N}JdZ?k&~xkS5oSx}@+!CCVv%jh(kb(ohG#?BSeKbAf;|0A=9wrk~g zTv0?OWvK1Bhr_&4H~w-ed)HhW?x%uz<>dRRLe1G95p9ZU8~0o;n#JxGh0viU{+o(3 z?v#;BLi$6kO0jwSaM=+0huS(_DE51WwEVd+C??C55GbNs&czC+#$}*IsJYAEU)>2b zBP!rIXb`phthgp?etPjkX{?x~apd)C-ln+)ffm-#zLb7Jc}W$kgq$CX1vK_z;&H;S*G22!gku5>{aA7cd!759u6V$rshSp^m;NPT+{crV3e?ETIdT$BAD+RVZs4CX-h~0$|0}yj}X7B+nBD{;$K#lH%a_J54qd<2S z(Y*n^q2BT~g2a-W;cfnPh(K~BXxCl6d<+xUD!AFe&PBqVp<$s09xaHBgItaQ3L=bou)q;~;b}l#{-s)G<3h``)RlcH?D8GxX?qnV z8P&%edhfcy`8Z+E2C|28)O$ae!ixctWoM3d3n#+5Fzy zWA3k)DsNbXM)WP{=qa+?qzdR$Wv`|?s=hwsL$656t;HgGZW;j5a~5BAMO)M-AVT(j z=gJvKalYS^=|htkS1~k~^ZjWk z1}W$}do?z5K%6@<;p0ZLG58^@ z?9HnqLitLJ*UH{PvI)35My?Im3i`tV38QH)TaClP74lgCCdti(9%8S02?_2%TkU6k z&19kJ_1Gr`P4B-9G<_HE)$O1~JWX>z2NBRvTRQ7p=jvQ;=32pA|A^$GK0ssOtq(HlS{~xZD+(OYxvCf&&?@y^|;3p+@P9H|dPC znDHY5Pej`+-Q-Cmz={1L4cbeC-VgyD7z6q|K-u7SAc0{=gI!keVK|OqR1{1#WsL#n z>-g0}R386U8|mEB|Biq63!^y>u{|&2@bs%e3L4nEAT|TOWT-05s{+Dts0F#Pe`xIH zk;z|He4>dO2RHOr{})a!Wo~DHqTZ-#8L8>4`GaKg4?$%jSXr)ry>g+aA$Fj}K-%&~ zLYVo$={waGFtwXY3rC*67pIkeDWs3-%-x!>fdat7fbLs|D`X*7w;&M*BP?HT_{0>U zz&RB}z*n1al0Wyl)Ibr|t&Oed&)s^KE1@oo+ezp(`;E>zI1ACzQG9A>JJNG(UYvJi zk%M!5YjpZ5r03eaZ<*=2R{D!bciFs8n&~=hJj$7a^r1Gd&rBaGA&J(dih~Yb(LGwn zBa&)HnbjM^$qsbFwaYDuX6|C5X5{F1+$5$w%Y0WVe#v~_QNPEvAJr|R?r(0}aI1N$I3Axu$73BTTkXF?s%f}Rs%iNq zs+rDNAt_Q#9ZKft*@_fvj?{j~Q$y3$ey-H}01#_kV0j3`w|5PQeh26_1J6P{@8bCs51r3b@Gc!s9-dq9Fa(dKuvzx! z&sYB6m-50BRUqV3Pj%iT+NNjEWP}9m`vj znZqBQFDut%-}x)g7#VSA#?8!o(N%Di5p32K=) z=&O@to~t=AG-clPFj>UOJVY@`dJT!V@)R=f+kuGdoZT=mdw29Wi=A>W5`WE-cb)zR zd1uPCljNN#2TziBX8Qk6^3Lg7D{|EsriB&-9>PIdLa8|N{!;y(LN7h~zElJvLu5gq zQQ%?9u<}CSX0WFnCo-q8@8*-**zQ3$381S8wS zwSXEaAS$B}OmX_w>sC(CP@>b?tFMnjF(m?3prIBa4Q2I=LNP^ZJ6A(2(GaIaA(&!L zNrp`WE!99jLwe|z&tZxb^A*z~VedYULev1lX_Zr>p=xzGFGiu55`m(pKg!ymp(>(K zOp()0oSVjLs7)G*_otgUF(m?(uc3BmsDDJEm=c-C~-KnifIzy~^BX1NB7f5f zj_otLqv9j=AT7{+8jweD)iu9Ai0@9{x5w?(Gw}^~NALhqIWD_s=H=lbYz zr&Xxbd1O6vd2e+3PBfEaNwbiNcfXr$=Jn;w1G}egw&zrQ(?AzEec!N@bDT#QBndtM zPK`vo3gJV6>gBQ0RlWWtU3KxxXdYMCYBidz$x{a~su1(g;CL0HStu!XGCy=n?4-1J zAPT0^tag}_;t;r2S@h+kq*ehWelj1;iznBNop%Um&w*3yi+EPy*^cLDJT2jIl!>Ph z&%f|g<5`TS9*=>?4({uTXC$6`@!&XqnCEnU#FL6Ld*K<4=YBkk@YIl~YWv$V|1D3| zE&Wq_i+OI~6%7h=i@#UI+!8%85NB?|Mb4k9C%VGi(qC=Oj4`)tK;t=&=KhJfC0Dt` zj>45MSDQTf51WhHvaX4_#fb!(TXHe?IpWMMoSgsC-0}dtSgpCej76B{mMf3y@8;az zZ7n*DGjeCa6()CBcz&KkbIX8^o{<(G4AAD5WvEeE= zUpUuZwOGD5>{TDg7u-o1!WUOyI;<`jc5mW`T3aG$4b_#lV5QrTZZzN@mC->dxZC>@ zzUAHCqEwwdfc!hCqsrJ${vDK|=2EK3g>txwhNYgk67r!pQ4Zvb@nswk@?`kJ4$lx@ z7>LM8KHpTA&2u#({h&j)c+Vh~IshfYCSw4@KXYg?ttj7PY{-p)b13%kLoEcmBHlQ~ z1z<|>KH^{(+W8Y_Ll&;}hZE<^& zHGGOThn_;0jG|vlH`P;v)M=2Z1o@62w*v&0oUo@bYRUNmTsRjYv_T0lVODtl5|X1n zKMNl=;|7Q~_5DpkiAlP+Ocu8v#Tkf)8CDzh%WL(^)_nOTUaGyoA)ZXHg9Jh*qXjaG zejZ~VdShv1iI9(h?dzTD^&rZRK8{VyWVkLQa`3vVkk7l}c1UX`gUX4s@PWemI+TrI z@42Dzs)t5xxwea)y%4DNV_)Q-gj#!`X4#027E5cZbgt0T$mL|N)JQJNM)Va@R%*J!grbsHaF~_%k0UgniKmA63&(q-N>*+UxqJv%VpyPX_?j zc4Czd6LE|os89?+Lv+U7+L(1EGxEHMx)sf~W=_)(l&(RSYS0c5(0(zXmLVtwtuih^ zs|qbcP#64=AsEPY8Sw9ouCcUyZ+K11cLzEJd~KYEnM5zc^nT2C{hm}w_w{zbXcj=A zZ%V&P>Bd4h(_W3uu{ti}3d8%9_eSZ01dZ+(8(LP%mIcv_c%Hf*4Um0g827Hwxjxd? zt_}QF`EB%!JmaY znzv1lk0nLidh^@=M40(+*~6Au>s|JjO74&PIJLKdb0xlHC=d8k7zida83KB}`Q_zJ z*1M)RH3Mv@VvR$d|04v^!GxM~abyFF8{@J?RV6`M><9=+AA5eLnx8iFlS(Jk_n4oX z%+Ff$bE$kHvSxIHi{Bi~v&e**XMR@5XP$W%kMCI6o9GOT@F3tw7EPsi((^E$yTfYt z{2g#w;I_20rU1Fruc;XKkG{pAuV9*Taf5!`#yWKZvif~;>H6d#1edBV`-86 z9GL^Dj+lzn9bNW;hXNh^52urP{ZFN@i^iY=!r%8{KwhyPVUH-Ztk9Jm9cfPTRpwG1{DBkojC z^HS95toe|G;pM*Mt=T&A3pU)IctuSAm21f}=8}S(u&oMZFy7%B$<`n9JDx|VxRddvSDK(*yf8AU{jJ<=}OoT-V>P8*>}8!z4~QT z=_^mc!o-FQfdN^*T76B@OTZMF_Qhs$1*T*n#6V|mS`^bEe9z0kMr=SB#iiC>q0ut$ zNEk;z$SVfRpWfOAy9Z4i0ehgG?^wBgPIDArc`S@N?Q{0wKfc31+&;GqP?g8@UXx#c z2L|@4`|#Z;$3Gy8%`XaBBD?1g-%)r)=MmXJK~^Bw;V!kJX zkCfKWXAK!l{}eT|^=O*|)a1U$_%kdDkok>+L>%Xa{sFDt9T?)^F)qZ)LOt1A=+A=l zTkUv$>l&Wl+H%i<%6&ko+$aZt%GI?N=vuq5)@PVLDyCNaX02y?&aj2bg14XW9Y4!H zlQ%zOkj%q2u&bo|%dUiN;qP$PBm?W-2k^>R&G&(k4u}wYl@3fjBuhPeHLkMi!)Dcw zlIDIoLYn)63l+~~`qw}R2IyN<6479}bjSDu z>t*1BiOyrkrvslYLalwra=kD`+>>SVTpk#d#mj{oagp3-00z&F5-eIj;7ZtxZ6oZY zYY90F)@1k)Ai$y8btTyJep1gdLa2KFaG~n)fuQO;93MHLc9d|Of099GO}kQ7gK;Rj zcQ=%R8BPm8wR!(Mupbe(WPsCQn8o zc>(EpDH#4CM~?p{o(^+JS+v28Jx90R0d)n;rl@mXnYd2 zNdYIlew(lF{ma+u!1cxrfBAYlU-xHI{ySV~_{}v!!)1A(A*F}`xFnw7g_uLoEsS9D6Vt8a>(UGvaM^{mys~Ev5-eDCFqKX)4V$LA#&%+&tZ@@C6 zB}B}Ct$W^*4smr%107F-eIx>l^+_D~M}G!B_AkNRp!1)y;jdtKT2n^3`~w|~vYf-v z(k^?uB3#He&}M|OWKd4UMEW%ohZ)?&%pM@t5&jR{gXL{%{Dx1r?a?F1ip2~eyrmWr1AaN=%K?HOBZ0YGfLDPJp4{A?|i&pjE(+U zJn6_c5)Z#Wh|hcQ*%r1n>yzLA?6;L}eYP;+tl$6FPv$3ArQmZSaCi)#@8a2p=Qli- zhG^|a;gA-ovwsojFA5^gJnL1*Y6Sc@-R5!u^8e?m1L2v6+VL-!|8U*x|F+-=Bw(63 znm}Wktxx=; z@bb@p!_i(Gc?>j%+XtqyTtuU(!=hpXiVn2~z*_(iH*G8?xKeQYUr4XTC*5|tW;fWo zR^bPJ-g_C7}M3?aB;79`un zRj=%{$oc{McV;FYb&p>=94U&d9qzao%#YXXNNV6?o)JZ*axFVw+o@uM34lr1RM>wP z3R~bZD!D%7Pwl_(;>aNjy{uvFnQzOI6#dc<)*vij?_pe8?SI)3fPt2Xrrwy)X}`ob z!M*MJL_5|dg$PN`pOu6f<7xSceIz^@duHQc>^v4Lvp+r$05$&JV}5es$KSQ)=Th@? zk@-2#{H(wyr&sXaT@E}DVA&At{lGf8(0!=?Fh)5-&E%z*>g6lJ$xNTz*dFnAHh&BD-AtG}vW_ zJwx{_LKL&Z%^(!ozGO@USFRC@1-lygzzfrksol{ z5A}^nU{vbR1u+S&7?}1>O*RPUU?Yc#{b?%s2 z13wE0jc(?uK<3M{}FeA*W_aB6(Lw&1VCU=VqkiTuQ6^&sW^Nh21d5Y)p(0hgS>%a@8NKguiwOe(i`(-)QuD~}K3PItGo(`&4*@-aj3^adtSLg9JZqiQ5 z-A)F{cT^zE$Dr(uW9L=eo5J~~Fv0VW>W%jt4D|>M?yL$vhOTBpoeQWQ*6b7upGf%1 zz37c{jPs5D!JWlzmM2T=akfAtO_49$!jnmQ4iI`??&4h&BcQgsQdIVZa2*c}#(FV9 zXdCJbm}XwYj*7yxAq+6duJz}4j`nwSfav`L85nmh&;x@6-VOq9hWY{mpGE@Xf|Ues zj*}I5+Jr9luL<4C9|_*{BDiQ52FHN5JC~ERjBZoE

yXjKf)<-x*0ZSz<#mRzD*c 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

Z3R)@BOmSd)=*@w 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

w~u?>M~yuM{Zfo!qo31OEJe$9v!P z(e8UKHt$8h@mxawy*RNndgA;P%a@RUhtI=Wxwckpm46=w0HLa5PL(D9_8D*aBDW+h zlzR{`h=KAx_Q{tDS2`InD*|vu(R!QHwnU_MOFTNT{s)8VZ%{&9&H&H7*T3)$t?p~g zg1WH|W4~Ao@2NdJNW1GV%4=irTQ3J}ufU@|%J(rv<&K1O{G%{oZZvx~`aEmk!!wPT zKLQ)>6PBCm7W<7=h&d71Vj6oj9kBjdHoewFcRT8Dz;-q!0F_D+R*A~|3_@(=YMl-- zrL7yA>u(wyUt%F#Tl{m_G$ln(m+*m(SFI-Sg}=wX{|gyr&2PngIOgk2nRWo&7sTE> z40uShX$y37bBn6~=0Hv_{FfLvx5V0Mw#}tQ?o`rr)hx2#hiUVQtS}bMS8Ju=lwcUem|GJc~Wa`DiZ#~b25Cl6zsH!DS$FBLYY?dpr`Aw$u{3q%q zI82D$(b-dfQ;)G5F{eUpVv%|Ry{+RKxmZJ$)zC9uH8#^sy0J-=mggB_%NHr87@L15 zktwZ8ds;0CqEc4S*Gtz;aNL`Ik>I$~@9rU5e+Jq4?zq|R>^ZPNs+xUHppBFA) z?3owvJCC2nPwbh$5Bq=Y_4*kU$I|h*@{e;;gFH?C@guc}-a%`gy8PomNo~p}uqVmT z=xq~q@qSYVLTH*z)ZdC668Sl8NN^UtT|Qk;|6$*Gzrs=Oh1? z!f&QLg*JLO?ilh1Z1M^A8Geg**j&}6^LL!HKd%A&$N45Fm1_k$J=&T;w7d?On)Z5?lUrv*_@^d(X1Ut#iw<*?B3j#RXGed1T+SpIP4i!WUv22$_MFzYqvS`I0n!0{crG_yZ8YG9^Rom zdxpP)R{1+J{1x!Z-6Rz_HidBSQA8IJx{lVEzgzLH72eZy`)q;G}MN1CBQ> zo6uj#gNt{Z+js+EP_;XKHbb7ls=U}`Ji1h}N%J92C+rOtLSwVeYi2cP2;m`d}l6ozs)DHq{7G*34d2y7!F>;S-x&@?{eh4 z>LWj?yU%EfzMjX2Y|5)m4`YScw0#9Hhvij{wP94myPXn0XJbxGraI233yzGA=uWo8 z7rS#M(0EA%mhGg?<1=VHM@kar^TC|=^|s+zVdt-Mn$g1hrPqgNT^+X1JM#&*+|SdE z13B+KZu6`UMgHZAUBl^5BJT-dqn3S{rS3^YG3wk8fd|S6MLsc{V=F$RRrlKeijj^z za0Q&H`CtbkJFoo_e$VXZ&St#=9r?#m~mg0wC&^&dB^1Mf&HlQGkeKe>cC!C~*?+rNHw$G@L%`d5?0!)NKLpI#0LJ zLNR~B{U=qFfT|m9f&($KS4laKk)zn02PY+-R>6u{{!&mkNqH=?$}Lmn+V2r8CMw>< zfdXB@{IVV{U8*&-3WOypX6e6yUUhgB)Ofl~pzMzcXI2Kh|7NVzy`z6;JmX$W@mj+x zKv1gud5;a-pCCclXuW;P=(RzY4&UY$CyZF6P0m80?=MZop{|tC#Jrm?}j(ML$w0ju1

~o}o>Yy%`7bi!BJ8`;1L*_c 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 z z$V-H-Z)-GM5E?EB4Oa*ak44CCjY^2n^=*v`1))Mgs8AtP7>khI8kG{E>)RShsJkE~ zq3(j%sC34#Umo6u_;uvZ-`z8O9J@z`4^^C|p*_d5&(Wsx$_=RMEe-EO zth#ZpZlH0m-GLU`!u#Zjx*N@_1Ed}wmub^yX_7{wRfjhq0v%-8MY8N7Sx%W`*=3UD zl#r!7{7%PY*$_E{9RWzlj>v+$oV(hLYx(lp(74EHibc#+Z$o5$HK{2#EGOv7rh3vF z++T5mFxA@-X%8o#>g~1|*f(;TQ$0${3Q9{sm!ze#Thd}T(bXJ%M~VIQIANr;)KFUB zd3QltmUl~9Y=*vk1*PR9k|k!^32A9DrR8d;orbhn(@s-bI-ho~wWMWGOLe!T#qKWDP+G2AcY)HYNx=4 zv{+MMQ(8Ko0-v^|B|owo)8IXnmUc=@d6%T6rd!ftcU@YpYu5#2$)m7Tc1c+3x+N?& zf2X{i!h#}hHV$Ed*eXMGwX<)v-Mp|I3-MOeJ9Zb^&Xcky2LzDp^krKL;KQqV1FvHLD1*S+sjPHAcHlC*fc zB`tQ}rSy9DU1nB7S_--(EG69%7Q63K{#o!{7~i_^STw%1;?A@*iQ`~gE8;j9(~5tL zXT`rpY{YvHUftH<|C#WH84$Ec1crWWWq4%$S{&d_yQh8&4)C@;zkW9k%38vM>zm*$ zai5?*;X!y&;)!fvRabam{W195+gaTk?puFaUR2|e(r|ix1ar3*ywf=pehC4snD(E* z0h;Ig>z5GBih?im7vW_-B3V)JWj^9q@#_5^G&mxEsET9NTCDgFdmQUGo1<~8l4|vu zhO%}NpTVdLZHo4w_K-I9h_!>tYTrGCyZ|aKZxXVlFN=h8NmnITk{~ z2!ADBg5eItU?F2V79yr&A!9lgGNxl8V>%*bBksH#F&!CPZLAFqRq-1`sntrHu&9k@Pm>#|~eRb`D2Mb%%Yxh6M2#+F(@R-dAkBCHg zJjnO-^BLh0<~u*!xhcGh5guVqPHxgvTXDc#J@V zM=z|@?NaSB!ecfgJa!?%qY)7vOI5Ro@G$yAgvSU(c-(Xous%1ILLF-L`ajOM@RZ=Z;$4+H7RI84Nv?)B^ngne|qQ3goc<{<1i6x zy3eyK5Gw+#Yltn;0u%V2xN2;dL*> zirj=)5d&++ip;J+tcc0$e^AXLR>bHJu_C4G_woR;W!aMJqRIJD>w*-rLr7V_m zk_3*-QGp|)`43nle#FEYhlyCzJDy#EfD&NcLagx$Z$4M{3@8v#0<2pIg50pMWsFN)(9vOUJpP($*p)HZeY!oz1bBAC^32c6V)sNN{s#xP%;1^4)@{( zxQX?xcrkvf3MiT7b_%bhES7STM3>A_(Iun#4_G6}#KanhiCEM5#YiL8c|;oPcJmo@ zVgP}l6Cj;N2;_!^G=om=WzfmJ2s)9wcQEM0KpH_O!s?q4Kyn9yP7I_ObOKcoW%Vyr zvj{pd`a{qOpW#q8k>(J;Lj|48a!b^SSIS~3CrJd#92J2wn*V?_0#Qt)ahQlSU0Vz^ zVYQ1`<6Yo1%qGR44} z%W_a6QC9zlY8H_xMt_J*xfPKq9Lgrv9O6S&WXdeJgr<0~u%meKqNtP#>;VvWN@tm&Czpb6Hc#JZKpA;!gk0x>SYx|ATu4GU|&Ktx7!6mZ?- z?!An0F|fvKL*X^-D^zmG#G0#WP$E%Y|F3EmF)l`bh;bQ;7#9v@6Kf9fdt+i;X1O7j zuwPOZOF2mbUgoHPm(lzO8;Tdama8Gn1J-#_tZ|r#wascj zS3|6iQl770t35=pmaW) ziZI*?`6oI@VGPq_2x4=$Tah~Hu1h#TaQZ&HnJOhTtcW(jMc{_h8m%kW%_Xm zkwzWcf6U;`h@mtb<2BN*$aB@Ax&-9vnk zx`+56_+;UO@&p25jH?5r9#6z$`z%fINurwY=E&R{kOlEUrx+h}l<`3db;JiLBoQA3 zvXq7!J0{C!#MCr)N*3JZ%7@LHD6g&!6*E34ZVg~Fr;NM=y?h2?f(+gRd>+O=Tv~){YZ2nBSi^787ptw~C2n+b5gayGtP-?UzSGEda z2#d7}VG2vgeGOK%iQYg-F2Bu*9D=OU67VB zDRW(}^OX`v%iR!`g09HQn3TCLb6c)=*JUnU7ev{0MOISgx?JBYEmSOYT@VG|1;sKZ zWvB0CR#C9-#2+ZkCcXcA%5sq#1Gws_@RZ29~yx8p@obe;w#mKj2~La_@RY} zA6m%xp@obeTFCgJ$fA3!2onU6>_Y}OW^Q@2Q3VV!uto(8anD}lzAIF@?kbrI52@{z z8M*M58@cdaH*(=IW+#s^J9&)R$y=~05L{3f2$t^+1ZTDcf~BojmJu)U8@ldXdHpZH zJv!O%d%iC}pX|5wLe1}!{eD^h`$NfokF0)ZWU}A!fyW->+VqH$K^~;onv~n(X)PvUA@| z_WRnTufCq_H+!w;jby*|clBJ6?6+>w!+%QlJK4WwZL;6k@BQnpWWPyIZtR`x_e_3% zcCz2net)V>P}ftRQ)g2zQ}OQuX5s})an zO@Glf>g(m@6=mMC@~7v_ojdD`WfjkSHBH>Kho(jYzklpsa>P3>=v^=_`1)C-=^Gb( zKC?LZaA8sK#tBu~HwQ-UsPK;qR=$o~MZw7##beBG2dAB-3r$yAxzu#6 z7u3blrC6`t3-F5dH(KQ1idOlzsvZA8GrIKgG&{5|a{2P6*gtJ5LiQ25|IpM2C$GW; zA+5DvdRCf0zt+EWgX6Vv8EA~zpS?IJURvXL?aMeQ3cf*pX-h6S9e;Z#F04u`Nl)`R ze%|U>@@ITP&vpj~2DJ>wM#uW#!nwJM*Pn+a`tz6 z$gy&#V`Y1&HAg+G1+%`b4a-`L-{0f6Qs3)XxyR>Nc_DN-C)A48Yqa3-SG8foe~91T zj~O=nE&YOi)~wTJ)yYMj+&A$80j+GywQVm)% zD}}XS#<#R#8K{(jN*S-}jgFOPP-%_cY*lL1g1P?!lu#)bm2zM8ggkpaZ+o`s>pWXM zb)k^cvEFl54|~w%pLssibWrv*+F0#)JM^~G)2t1fnBh664J!lZ#%6ggdG>}r%+W$e zoSI|hMJ+h$zqMhbzK7p$#|#_wEq$Yj%G)IUhamk%gE=657V2buOFyiC2rwNh51UoC zgCgGtHBd#U@}Cy6huqbtHug*97`X=h|qWIyBsScL>K%47{3keH+np(1&91+=wRmJ zgP1y}_1%S=Z-(|~I{t9JZ`&Bh%IsJBp4Wy=b!qx8eY3vBvGNic35AZLks<#cI+znW zMmC;?+^r0~pSk#bY2+AgzKJ#+f7smjxW1NXUc_T9XfNZ{zUx6?w}6rL+t9v_!Ju5c z8QO;y&h>568}%l~%B^T&4O+Fr5JTD%G2$=L%=!49~9|#O>XhD}24)wi+$Fg4S zdkBOY0BR%kJ;HY+=7;kN4Nt%hfFY$JGJL=G?Gsf&-c~^O47%cwBoFs`PQ~i8a(jG5V(z zTBY^d7yDEHX!;(l>Yr}I6`y`d<%)CoKU@X1c3D)@EdvaV^f?@8i!WC~cg?#62h1ur z?{llb*vmMp!DYwNV_9iwp-a62NatEW&83S1E7C7QhI?yE-s`C?`KZUZ(0dusMt=xpU?c>*?S8GOiZJ=Dv1 z==t8@J{~&4db`I2GqU_TzU=U{>S(dSv9cA^g67-z5Y!+?%l1Ik(4BwnNYjEZ|3ex^ z)vHi@wU6U`*r zPsu_n*xtbGW|tb67$)=n1N{tVKS1u+?m}BX1ET#5SN#k}Ka+02Ktj#;*fey(!w&t& zZW^z??~VyRnuS(imOVT4Mu@WhuA++;oI-+R|H`ql4gdbrv9i8c2l&}P^0a$)dschS zYWgWpM6|T#32ULS)3MR#Ii`7b=_gTPjsC9B^C4T#!R`M=XKDYU zJov0LI4Rq)*|8CqLcEj})N+C^<~r6pwjR##)OarIquicGa?%T?xSw{UjR^*Fot{Jb zW`Yde2UgZo@%pRo?e&-=ZCLJqIaVG8qt8wu19N``umYd}ZK>G4Ud_|w#xgdP?L$i& zG3MAD)yUDJVAg;7hGqSeV zyi%t0Kf0%(r(*p~QE>P_`i2evu4CmU->`XA@U&Wo4R`zX-?=$=eXwC;`40KJn#5uHrG2g zgf=)eSQ*XJq&c=|FrDjt`YE4hvu{|you|Qc>PLs+0~k-(uOFuTWC9St=610q+{rA~ z_kb$dE>b1SEegg2X4Epjz6+xS;+gICc{Wn`bM$TMHlQyG<}M59A>Xh^m!^#k`bMF@ zy*|ALm3;ad#id~G4|x~(jvC?vwrdT|?F+hp!;>7}u%~{X7R++PvIVN{{W2UD48}h= zRvw3SF9KtIbVrVYsbKLDI4l2!+y5`9j(guH#fIg6Tj=V3RrA#QJO_$A4VvcyD0?9g z^x=C*f^*|AA9oM=wQxai3wEChFMbx2R@X$6~4`725kno(`+lhNVb<z?)SXpG+eh|3(ue!W)Z}Vf+WP0kVULKkz4{*jn!rnHfYE+s<)?Z6~Q1rH{QD4GUr__Ce zbwN+o{hZHpSuXxUs`9GOQ-d2{lB?Bn6@)@vc6COs{+JE$BB+O|edXfoQk5WhTFq-e zz%{>~>n2Uu8zfs%WVux4RgA2vQMX77-(l_GqyHauZv!7yasB__B>{p4H&n1-K{hqC zODmQLD6~*_%?9r3MxY9aRUTFa!NdguD%KD}K)v0j#adhYt$r)5w)Vj)t+Z&rw%L#c z0tAx~o(zG6$AlO*KzIm}&HsJo?&bl4iq-G;_s=UkckaD2XU?2CXU=El&fJSUBFVo3 zt`qHeR;8{=L#UFWA(~;ix=!_Cj5C2F1Cn>D)JwT4)r)Z%JJXDEZiuk#>h4oXPboEx zK}{1xJqM8GsiX%*bfiZo2OuV!y7+`tF$QU*83js5%(M&X4!Dkip&;g^c}fFGTIDhs z5t`{PGrw7iRQsuE(63KPw&3#ETJfxZ6QYk!Wt6kmO7%k>L zk3TlaWIxqtb_Na&aDsJ{gDzv24E;tz*F*o;SLJHJngy&^4?yufN%t5Hr8TKWi8D|& z&?PqNlzLUeV`)v`@Id^^dZVVa#u+$>YGHWJq{I?a)|wJWOl;Fgi68_tLa;0Em4;s0`46!GSTw##dLL*n0CN|H= zm5LBcH*%$N#2z+srNYE!7`d;QvHOf%5sb_DlSs-Vt-<}_F6*ni1`LZ$LcsSJei5gO z&sd#RDa)*{o*SSWwRpQ+5xEP6YEXA}k*#Kc$l!TtX&U@I2jeK0Hw`dyMJ0yaoixFC zUev^8h{Exc3i@WsSlGOwx^ja8X z;cZ>3^Cc z8EUwp44J;lr79u}e#q4b?31r~^|k)!Ph3eej(d!w9xm7s=Ba_hZb&lzBtjRxN{|*q z@&l>MNF%qA%i`H>sn%BuF#K~a<9TU0u@+42)y)G_lMlFzUrE~trw-xzYFGuf^c0Q- zi3RrKV^2tz*)zGocs^Dx9lkpGnCcyS((C+9q#Ow0EH^j<)mJ-{n_PxJo@>eZdQRaC zA{URhCaakq8|Bv6LTaRyW~{GSbfTq&>75k?>{*j)U6ii>&hy}D($s=RlWUXL zC2vgL%_t+7Tw>iA*qD$Efdp$&a!qovQNZNFMyJs$`Dk*b)0h+MG$zJ54QG6+F}A@; z@0q;Mox8rlX*}7OYTQO2zu>$(H;~}YEvQQ4#ShC)FamY@dpZ5?SYx{sc~ltO99Y*k z8HprNLtIHp5;e>}^5>#^eSF6g!Pa z8;Qfk*z+`gZlO)J{IqqGr>wP0TMw^SZ5+aZw+mR5wnzjk7h&x?@|o0ABhNt!BrEin?18Blc${RW08a z)FSu)-r4;?4EHbmTf3B8gEzjzJuSCXtFvCP7@+g_k9dqCrlHl_N8CoSn~B9E-FRn6 z=qju>Z}fLv{%$V+ov|(^fSuNjF8{Q6r?tq*(9XKK<_-$tqIn_JNabX6s*x6(YD|ht zHSUl1_(vy+&BsZq#GZ7cGVLxh?gd~y_KRcyy z!ZkTSmszH<{gX=rn*b!6#JU7XmjJ3i*HUbhk`yTgyJS#T`UTX*PANBh;dL2GDZ{_; zx{Rch(O-C79F*efSQpuSmo|#2KGfWQXZ*tJU5WXw9v_tEJCn;8W4jVdT|G9J1{nUL zEw$d}mX?((O^tDJ7acP4oyKk_7Hn*bKIQm)?2%EJ*yHpo#U6*>`hRPWXOZ0bFF=PI zpxgL`w|fnxeAuzwzks?_QOdzDye@d~v3tJox|C2#`4?W7jg+#bV_mSvd}WW)8$lhGY$0IJh0%zHm-2LmoI|Zr6X6KnQ=NUdq+JTl0T1n8d zL+ggrVQD<4gZ40J51SOvto4mx2JC(|2dDo!BozAv=qjDv*!VBJeU%sQ*uEE5_zS4p z|Cx`ED28=K1yC+6?~SD>g*#AXa|ia@(t2!6>+ylxpSslTzyH}ZBXwn(G3iCOv5p~w z)otu{2Wpeufx2W*kBxfZfYsBZ!fm9!;x_JoJ zWKZ)i8k*+M8ri9Ox2g4JJy4BWx2mkCN0E-odN2Va4Yi(xTAy?W4x`phZEC&M9XNto z?@Kclq1IVznJxP`I{DDqXg1$6@Jw>y znQo@emexaf<{&(yx>>p@!>^kBC^IqJ0_zukCcw`HMko)1Q*{`lq$0pVmU5(3{$uZ$jdcr?jV@(w=|*`GMNNfsji5 zuJ+yUYW@24+oSE-1IgE>Y15`@iHV6H6Pkzx$nb)m2xG(ngJhaS3DyZ2s-{OLq(;zaGe_ufm_($gvWX}{+8Yirl8{V(mm z{tFh~{7C!gBkd<_TW`V2Qwz0)3$?nsx^HUV{3a|7Y|t7Sw5?mWF4vYXhqdqS z)AsGth7KLNS=+oB7WWL-h7Z@KPoMsn_Sj>vIxR!X$k2F)-ygI;`~jA)EY(U&wQH}v zHdc#`h4tU{)_V8Ws;jHNt$q93RAJO!ZSP)f)22pK z>HfR4yYAA)j2Ux6J8^=l?G9_#jp2Z&hhkRoajtLmt!~d=SwrOwbY%v@d<>OG>f6`OR%X;s{~OU}d{6t{ z_q2cf;~%@UUAqwRA2wk)Nnj+T?7efi5@_GvyJB9FaVyZUNv=gyrE zXb(Jq=zG^{wYAzk_uTV)?f1W@3BLVn?bpB7X3m`XKidEN4^6Rmur_$G_LZ-ErAcdQ zqDgvvOZ(QhwEq43zpcIfHcfNW6WS9`XjZGWo7SxxP4vQ}#-r{(3*bm0Zsf(4q(<$6bZ=N+2x{j=KHv)W(( z`qw{efBrK~x$b7|=9{%OYu0>2`^GnD(kDl2qepAcJ@?#k?f7w;Hp!tm99mpl+%|37 zHk!ERpV~kFslERC>nU1F3QawukJhJ;cK-bNH?%k2pvmVvu08&^HeteqrP|V^H2oi! zVf51}d_uY5j zER-G<%-w>rYhAi@8G@3dip5_-xwYGGzuksX3kX_`GHb(z4f`cZjA~|jP+o2J?AZk< zEh>6?3CgOSJbAJ!N{Xt!{122<%g)YDMk!I*e}5Te)b{V+zY`@yb-#BH%BTJ6SHGHx z(xJk;zJju8U;EnE`lDp1@tFwR9i>3!A2^RPXgA(?;{=od)vqm~@wLT^ z7iZDvn85E18e99(kACzE8W~gg_4jF9?a@aceSt>BB>ty`#?*fN;~$sNh?vIULK;sy zb?TH(qhTUVXJ{<#?|=V$1&xHMd@F{=(VCl^KcG=CnYVAHF|?^ur~Zsaz;vFt58-Ps zzW8DngpLVyyB%R`bLY+-hLAC(BeM~%Hg@dTlL!@)nxBm@wRhircRxbJw90>l@U%gL z27L{oVPb!}1z~Bo-FDk|5E7>L?9UO7_M6}Q<_?5{$>seEVQA;hox2esV0sG{Q+cg> z_wGNU(wN{ok5XA}(xgd0rjnTA*;7DQP`R8UTqg`>u6;r7Mrg`&=Ft54Y z?zu3HiGE`&%xVum{P4RliK&hr1asQRkt1(|DNOeGZ(v4y_0?C;!33u3=uY99-EN;m zp_uTtV-%+S;0HgrfoL?? zyytG*saW^(s2g31HiK?jyw3^PlHO|pZcVhm0QWAv+@EMZ@uvTU`w}m(i0)5%$8NYU zK@n4c8yeo`6`EDF^f}ysXw4bimT2!9;D*$q2=f+c{f2H|WfS^pYv4m1yC8xNWh@ zow#|C(gvDe(8qUi7or8HaQD(n{RFcXt(*-!5R@{IZdRPV`*91>o36mENw3orH!R-#F1mR^6%DAPc=?y;KBbp@2z3^;brf?GulifmUQpLx za09fS_TRuW>9s$kyOQ2~Ddr`;))3S{(AXoWy;%M0sI8!vZ_>SpH@O)%FR1BvxJBtr zgTN6%VNV0wq*qMC-HY`NquUj2Z^m6q?>Gy$D81D^zzngfpW|)>wR{^lBwoD-FhhEY z7f@?K1qr|!@s3Z>?TB~YMRzP#a2wsH^s-y%4#j(fFn_V$ui;k2>l^}3N^dw5Gm>8Z zTHKfP8n*&V#j3x9`H9zBMmH~3`FFZK>1`@-x8iNfaZ_TIFXBD~_0-Va3Ci>1CZ(6@ z0?ZRHQ-|4!m0ba>QRo*qBj|Jh@I%nwTfjs?4L;0VymcOKTfFzPn6r4bt8i!H?fT*N z1jRf9d=oEsCoo#j>6O4cL4AJ!9*WnD2R2J@vlw?T=&l#tqV#6jbW_rsf0gc5(8Cen zs`M^XaZlpis)5DQdwzgfi&q;1Y!;NW5jQE`=}Wj#L6fn-9YL!LfqjD7?gp-l_Z&*M zEM92|ZdXvt+pl1JRNYQDC%w)j+`RNI4+G1^D@>q!lwR^*bT{H1N^m36%j2aB#fv4= ztqa=S49pNz@(sFsK^Je(-HErd0t3YxZ=#!$(ZxL6zj%!n+?k-+n}G3xKIZ@v#XH3T zF9dCRqk|LNtf1B;V6UL%QicbD%0FT_A-(Fmz&#mN)YJXRXd@N3At)~gcP(f!6SyR( zJ40Y8D1ABbQBd8l=}x70J%-y7FLOJvP0;c>;J``)azz0DutARU$LcWZ<5wFk)EETl-KMbD)MQ&rb zC0=YSut`Q0?=!3suX+nGK}KKG7*+|&8V7t4Fa9%zN8-)S0eb{poClT*YWP0GA3+hn zU>G5zlrZp9Mq&2?Zw1}{nc;z;+0nptK~WxtiGmhlaO;9LM*xS#>z@RU$SCuAV3VN5 zwZK3b-Q5q|7q2@PcqXICpECTE(cgB48G=3&f$M^fTnr;+)He|rFQbC>z!gC|Sqxug z)HsUaql`Xsfpap-+yg9@QPCf9*D@M?3fL&9`Eg*2pvTD!8wA~TWjG~h?JEo`#T&m0 z92T_Q2RJRGz~R6f88uxeqikGMf84a7`?J6W8MS;5cPyyV1{{}BU~k};j7DmKk%Erj z!+pyrsgmKajKU8xT$Rz;DqyOH>#hQB$mr^axOW*190!IA%H091lhNOkz!n(|{FC9V zpnwAm6E(VK2g64hjgRmA`2f$yWi^W3!}61%C#~0=({s;h*5&{1qG@g0w{+u>Y~a#! z59WLFCg!*Y?{}a1z@4){pQ%F6;BvQd-ff)nFjHx4b@|_sNBka^pW9^x^iU@GmxcKD ziVb^TsWZ=}`xn`Cf0o1Jzkh~qP9N_-qUY3W2{Mn&(?FwiJWuAGxlQnKhFp5?{sujl ziMG7ny*-93M`o3#Y{p9PjN0$=$GH4=@Bsg=IG6wX@z&)l$Wa$Vr;^G7sZG4=)pav7r245Ztr(jst5~JJEJl#} zDlS;lptGFOCJP(7958FHsvW8GMmO0>>?RYLH_39wz9g1q*!^8d(59roQt6~5#IP{& z!g`BfYUMo;QU2BXP$j5A7pB#XF1m4VT;1qsS~*iF zX-Z_#jMBcs_)w33l!rw_9v0<${4-g9F$dk6YS;5-4sE{zBV}gBL+R;rX13eM__2Ez zg`bZ5_h_s9kKe#J{^HGhnY?Ag!BUXuuDyp@xi+tg9ed2xw%^Iv`BuG;+s9bjEuv+@ zsrmP@{Ia*;mh-gwsBP9|Z(t#uKVY6Om>%JqOGPY9k|eo5G?U!HlSj@U8%{OixPQBO zz95P7vz*HwDY@S}azApuAXe_{eh0?SueJK0dDG&@Nn+*au%)wKujrOen)$*#ozZnj zh@`_G4RFz)T%+gI=odAa`)&W@RNa$n-S|mbN!OJhPeEnr&d2_`#Audo-DA66ab;YO ztE}-oIqkw}H%_nM^eQR0{ojS>?e9taLjEsIFTAz?6TaL3UGBN$y-+?7cYFs_K249@J#-18rrT?_|%)5 zqegcb$B*~FrrZ6iI8=Y1?X&u5ZtJ`Ihgz2<*?F44KHFzM`X*1x&lHFXSyyZ&T|4)j z`{6w&t-c=oCMKM8T77+IS$%`)$tLFQb$vSJdaVfPpCX}mL`3h3 zj9fqi9{zihdb@BcJoUdXssAjeb;%wVu5Y-D#cv~BtWh1U_By>MEhn46^jW%f$quRS z1l>5J2ST>GF7)pfRvQ&M1BD4L3oFtpWe=9ia@d*N;0)x)yDT9VsX7AzVv%%lt?7tG z(sgf5*PViF6p;iIc6hkKWhrG#kdsxTF3ShbERqiTk#xjZ)W+&rp|XuVDJJJ(CwrI_mq%E;Yf_G+gGwYFu}C_oMA8YrcKE#k zer@oZGv}<6cFqY%L+2EzmX48@j**s*k(Q33rCl-XdUwT0 zOUFn{$I#Ny@jyGb*KgXwaM!TMM!JSA8LhJtZI(3@i0b4LbkgWK$ZRy5Y_DVEZht-R` zKZ*yRRipbKZP5MG&gywzYmYQKH7#}VePed1V|Hwks&NvtQl9}G@MCz_?&D9n`GicS&u36TR+Fnk!ak2?I)w8S%N_!JD#?P|W zZE2ru--XHgNwV%{vUz29&a%!c8bwg?{L1#(K1b@$3-wX%mW|dGqrt9}B6UZ_kyW_0 z?sU$M1>+I(*Z4iktxzG6S#+tPVz~m&QCeNjg-TSmJHPP>1XFAT)444QdlZHJ1qNC- zN(l{7cGfOw$@#aTirom}W7F~+mgJ4MJ?rX@GHzOQ*lD#(+uIk{xbqS(P`gjmFZAd2 zi2l4N`ty3Kb+B_SfqNst#9ahaYi7Ans?xJ46_Jh}rRNoAlva&cU>?UjDaTZ+uY^oe z7oxi*YHCga6*MKd+0>ed#(lX>Wje&jjsiJFgNSXD80w>RX{5H5=0KrH;F8M(yGVd; zIhAq|tur-@^KHGkFN(I{RJ_D0FGU&k8>zkjag_T>A&PQ$DQR23R?6K?%2v_R)K)b` zoAT9pg{W=O^Oq_a4gUW@olN6stCO4;nZ`06Jw^gzErb`C3li&+7dQYF4iS(0%P($I)Nh0OEV;d8rkG%I#1q6RO{lWRv*o{-8j zbE#Z8qq59dDRI@#+4EnK42$1FMM!R)M`~;K%zR>`wPw#LAx27T_WUY>#SNUoisHl? z?wZdgBTnGNn(a;?1{>Dwi8g|%L+c(k`%CI79CHv}Ihb7fgiFrgj1Zm(6*oTN#wR@Z zqYUyKkPD!W9MP60v zOvg%vg?vzgt)x*NL4c)VT(wL107ZzAgg%r&#N#t%)W*G8oZ=oKwQrI$mm;rvZ`!3Z z@@5OSac>GsaUbf)J+jJH?LeELfj(&-nj}F=-6_EysnjM3@O;WqsdfnisKQ63KIbJk zB$YB{)K)1|F;u0(9V>O@QsnWlBQ@HkGxBB&w^hniYgH-iw7s4k79>^1%o41HO>3pY z7i$GgNuL6goF^PNz=v1dz3-gF#NE$5F2P5f`Zf}z7m(bCtkb0-GQ{a;>=O#g(8J2m z!^+UZ7&@Jg@f*F! zh)QM%@Sr0KOGgxzjwnn=1To5kgzzF_ffOf5MT8pVU0 z?@MuCLGP9?UTNRw0O>Xs3ZlNShh|D1I0aXa?kl?fN zAq^T*-Bl?>KQb9HnV<%W`Dn>{&QT?r75R*7YJbQ`D&mi=xsM{O=ol+^6?GJhpZS%v zvSaztsOzuNM%2bXoVXZz=Z$VL#@lLCZxmQWpu!K zx(ut2N(`l~T?c<10mx;P|7n_5HArV|dVwgd%fAOJig|W6Ds^bj|J}mBYG$boO^nQy z%5Wx<1MWoX@j3JVzoD8UigxX|@D3%0p%=CxuK-bPNOGAQ$vK)&rD;b-%`%j`Muu{| z)AGJ&4&}bwI+Tl`K6?c9*(0dWE~w8gsLw8_&n~FXE~t;OA3z;b$?(l3;NI zS&wK0q}&}IG^>VtO_H`Ldrkr|InADLBUn7NZdN-Jqh|0+2&aUKN5PiCCZBNT6Q+E^ zk8kQ*+@G0GFsFn-$^}<)BaxpJ;uHG5#Y2UGQ%71_6pW4R>T)N7l)DL{7)z|PL-*`(<*F<`;=vN zS7oZOOobKQsbzp#ZladksO2td*+MN3QpUpXz=;z zoKTrma1URg3@q9K%>w(%6q)fIoSIVvAF?abGH>bd!)Zlka31<=^qee6a<^X=;84& zdRQAq539qXhuQH-idX_VNasViFllITv9~lh$NN#RX>K4`3%S5xAvZW1;G>6977#*a%+r2%s7}g$I zJWU-Hw6Nhd=>iZ!lVDUD4Tn!0@L&5EarT`_;^GQem;Xx0gCd|XI!TC1f0`f;2x@nm zAcAWGA|?yn=wD~_JCQV%{4_$dF27pwp#TBJ^;fJk(W%3VDNm*25jX&AsCsP>X+-(C z@c1eD`xs65beu=oySTX*rbOf#8>tMB50qRN3lxm_b!x)1gG>8$d?a&BpRa3IqWU!c z-Oh7CR_Rpz2UAM$U+B%ccR=y}mX^Z(S@-snGu(VZ*1fH6Ue8;uzLUcA(yVGVz2xIi z=*&{iKTF(s7Vcaw;Lo$N-LFU#hn|%^F`r;+Rj^qcf3u`)&gN6dlCC-1Z6ijq=In_M zf~hkKomtB7XNfn@%ATP};>)wLA66vkVY9ONyjoWF6IEoz?>9@z%J85cv$}^9(l%%F z@u+6;`_0*WFsV8Fi5YcsE>u3wgIVI|vwZGX2!u_au<3(2pYY`qmV8rZaD9d(dsvc5 z(LO1f?xoo$<@lrs-xD*0hi?cES@iS{!l#mE6yBC~@3&#VWj6(EK4qYV9?l;Tpu*Hg7}!RyNHh&o1cty zM~i}bl!EfUETro}&;!DlX&?C{W!LZ%`P4wKDhvz|fYa3r%L4@9be-E&=*&pxbK)5p zj3WjJD+4l)V7rlT&Y7QB3uDgrdz*u4-VMP@08X(0oUUYZcFi~&tOVdxW*jbjpwHMK zJoOn{58oMML-3t3wk26=vQyQE_yYsRZXrH^H)zmHfCa>)@GR%lvW{i@1rQYjh{lA* z+JdJ-cRGUGLyxjb()6-nJTo79vhn$*@eTqXcT871AoR6hAHh#ULX#cAuAwEZdD?ya ziSI|r$Dg8kdQwh(XtIrug->(vQP1{yZh`s~DQCKr)0Srt@M}cqPWm|kU6tUiC~r#u zPkehy@{E%_v!mq*Py!uUSG>d%kJ>$~8j?8}u!A*slXxqhbE}g|6GA!ENrHKzCU0^d z5TIc`N3rJa5Iki*8_!0PLQftm$aWijBi)lR2~jyLLVrpK0;a^VSUk>mu^4&@F^XC6h=)YA7 z!O*{ENJ;;BeIW5`MKYZIZy{-2K8jHEmxI|ps(x}R`pd!WY-?p{_7`2hL81RvC5{OF z_!TMn^RF-M`)gIilI)*I5evsq_k1E|+r}!};Z$r#IB1rkz{jBf^Is79|5WpPrW<*t zge8@2i-kVxvt{pzEdAcD>vy!Ys8|mbPPBN-xZKxUou&KoRam6LGVk$+^zeXRY$v(g z8&pipT=AjKE9!1-uPns9s}%_ei&VVKyZRwVcu$8+SF~mF9?#Ol0VyW5OTq=46THVW z^zb(C@kM%QlY|S3;xA4WcX6ujl8TiZ>QKb=3uhJ2U!3)GDnL<=E;AazlYOafIN!T^ zkshY%)IqAQhbz|ARax&oL%t^RUA_?28e~3#MKs)fKPFlECSx4FmPq=wyqU7~ujl^6aT_)Lm!Z z2a3xC3^TTBI!?_|$bGTz&KF>LtC#3P_6socPcgn%Z{xH+vyx@e|G9n3;J)jTGINyQ zL(V3uQNGx=UD>uB!<2X#(0#yrrOLAHzCq6uYc*24ucB^{GE$V9q$skjoej(F$o>>Y z0|Bt6*sdMhWlXRQh%0t$$8I~`jq7-q;R)7CP&A65;`xq=KO-rqL~U}WX_IzklXhj3 zc4d?Hs7?M_^!qYx@_q6Y9>~HZB`j9q4i%dAxFF;5bXy6SUIQ@~Sy$xAN-_xOeB zyB&Q;?d^bT%NL>d(JFij{a5Q>Wf$6iHowKRRp@=RSv=Xb%B_v^E+9VD;{6_nk;)&e z)6`+}Bz4&KM|G&cxiu@tM~HC{mZ`8p-9BRG37L7$n0dr(NOw?$^(s8A!WQpD{0`;F zfpX+PIdY&Jrz%InA{8%FVTHO)Ig*NUN6W?v3%z9N-8K`hgH$tq4n4ui~dw*y8QQm3$Qzsjy6i z6)HTa!g>{+R^cVZXwHoFZsV#gCK3)8iklA?E0?}QL5_V=>rj<z&_I51sJN z@2ZDB@|F(J7i>xJmiEyX6m#gQFWAbVtG?j<1ns5IT(D?{ zK6jgUb-g2fvv*a!BLgVftfzk_5FJIpm+5oJ&dWO>0wMIRS&m_E_t_mtJ8Gv zK8epgB<(i$40QDLIthhNrJf#^FypZIz#=`R(&atLq5hcnWQHSU6K5RCj(HC}q^IoU zj6)0evhB-Z|NA_d)!+(ME9|GNj(UuN3Q`=k6wMG9=&=>J$iMOdQ2Q3^+12U8zfFWCiawi%zr~quaPjL z#5?h2sRtqT*fv5>3A(&@aiAV|rRh=+IZ%&ngY=X$oN=HYca2f?kOTEN@Q@=V|50P%-PbqrVyG;(2-n%lO$vqBbmEKH5been|c91WX61c~qg_IBJ zDeK6`p@OsfDUW*`4puU|B>VQP%dQmUTb<CE`a;vp4AZ$s%JJOeFok;@rYn;Tjg9lp zA3?qzq86dYuJFzuM7}Fkl5yOVeB9&kIQJyqmCB4CjrYzUqK78;R0h7}DrMobtTNR? zbyP|pNyspTBpk9y!eKT^{w+yl`b8?f;)JZ0di{dwmz4~N^}%Ok%FViL5;vowfkf;r z*5yygrBzwFFCfE-$P}G***8^6#CMgdYCI|LluH3pu8*Y5#~)djS>*D3`tO?+Tf<42 z6it~QNvY8!)@3rYq8g!_N-3qElS@+ix=2cgO8JIdmd3bFmENC}CF)XpK2^$Gxh(B< zl@hJ+`8&BJGz+49s+3dZvNY*6ie_(8KBX?TE8U#V{gzyoR=-Zsl+tA_m2@P&XpJ0H z(YmawN|~jjYR4n;w96%tQmZ^w$_iwq^uugC;WI3kgl4NeRm#H*%=^G4soruSPfJ<&qLI70c6f}NTmPi;mrXlZ?LsvepOR;+GT12#gzq%sPVu!6Kln6Z*4 zqUMuO!H9B4u&5mXC!t_byRXB;+d`*2x0O)gP@(N4jo?tBEn$V+l@(d?WLCm6Dm~9T z2ZRTGX&vQADnX!9j)a0hDMtak+Lb6yt6(D$s(a7(;v~E`9>8e}4?0>P-dNF*PynaU zkx&4q@Fk%DPT@;J0i05c59NpG5}M$lxe45PT?A?Nkg>YgPyi3hxn4f+C5nQuU)!^^;Qdvmy?`g%?*G z5F|qRbmdcHX@(2LaCzllP;s+-h5yc)_^;E60Qwrc!hd!zmSsM^1g+T=_OoSdi$1^v z_t_Qhvn$+ZSGdp4y1Yt-`t06)(T9lSwkyPECwJsAB52Q^Qpvm+hk9nhGHfZEIO9+j z0|c_C?Bt9?i_DVQQ})OVnmy%U3}{cd?lnTDFM17;>5E>2n7$|s$g|qO5bru#Ci1K{ zn3ldVSe((omB^D?FrM(-tB>&9tEceXt1CS7IFu)*fu7zC#N>%-&$(xUL(*%4L(J!; ztP~r-qtA6C#`5ShWrTY4nWi4urpY7Qb%Vy9;rbwX44N`#>>lD{c=RaOA(ig#MSw#cV1MI-^fWp??T|28E{CD9h^6rerR2L6m>T) zo=3gi%z7^YMnTnei_Ng3nk-kgk+X)gA^8p1506G!Fbx_yYkdp3K}g1J#~w1;bWv- zCuyT#C128B-_|hU?EaGWKGNP!+CoXIKm-q{Vv~4M!_(kT1`-$n%PCY2_5An?!xA0e z&%*1JI(eN8GgHIfmonXY-cs3${sq5L<#TwU%EryTwY4-Ci_r=% ze4k3x?|&M3r24PEGq3kG>a{gGleEYrAF8STyJDUGJL01Ea&O?Cer7i>3yM1%`L_L; z-GSPe^C$J!YOUPlB`)UosKzcczjk#NGU_wa(bv0dnY`A*B? zw%$fgp^Ef1ayF_+e!@?8&d$M`c&!FXH6^U4Mr4>&DY^nX})4cR6d2vkgyI zagaa6VpAW6Z!xvzFf!a7X!cIW-1wai^LE1+1{C7HLS|GElNQq%8W z@MUka(oViNcbEAIveipfO?6`@V%PmSDx?3G4U(TPk-0toze|Flp_{IA~f)m=Bnu{C&Hf}R`J%wZ9h?1wlzZ(l{C9)C|uVjt0m z$7G<3?ylHH_&0~DY!>hGSKfSNV@3VRQx9I-q(D5at86Ua>^<6whRhs2c-399xJux@yS5J$>TP5hdirr@p zyN!Uy;7wO$D!yLDOY}1b+!n6axOfLX)G`@q_N zo=v{-$m_va)g#|%;{{D z`TUCXm@fL&w^K4Nu%kITh+cJh|BDideiA6!)v6=kL-!a5^yGS;7|%myHIE=KQK*=A zB}n6Y+=+buVYe&s17lpiJ5Y12F{KaOY@9B~6>9J{$5YJ&TaZ8Lbu0dsNl#Jco#x7$@Y?@KsMjujFLFcMy zISDFxZ&i-XEGNk>A_FLsJ+W7q8L_V=7Po);u@pMmr`(n@o)JNWov~tl*7p z1@CGlQgczkXQKtjbu74VYaV-tf=5OQ)@jw1owj>a2`-`EMv==uu6J}vU5`}0^||~g z&{f_!Xk9*s&Wjb{>go@O>t_lE(ovX`co%dR%24Pfq8&%7vEHq^@}+6U7MHQvol{L# zI<=Z^?!dv}X;^TeE}r)MQkvzkr`KjaHqWPS?0TTSug9`ZEZe;ypzNO;iM8ZY#-C={ zC|M)MPnRT}3B3|sQpYZ+=7(g~R}?LL$YV5!M+_Vr?ly2td+715)m2No0|(-DQ#)bi zsnuRoO zdx6kH?`|&;jJLf&Xry)mscjQTF*Uzf6dT6%MiyGlk5wt>KB3a=5LG>~?2Ch3cYfnz z38usmOphm+o#4rPDJhONV8ugUpcR78&YU3O4`viP7iXu;lhD7ELM8J2%1Go_uS6oh zeJc|AQ+_1!w~|QYpH)O;nTeh^wo13&*1(R&$Kgn}F8A~dLG{o17-gOFJ;s(Q-(p8F zkXhL+_)g~DZo!EeTZ5(P#|oX!^n7+ZWtQ55MVZ^LEeuSoV84_zvm~}K;I3eu@Tkm^ z>$p(CSb9{&hG24?+h%=w7ASzSrk4=RWRnVInwZr&|B&KA$BQ&d$t zlj+*(f)st8^yW%zQ<5g4{f+$f|TE*+)tg*%@;4q^d@Q&niW6o?M5|#jJOh zXoS%QZoZndlm@8ls#HAKqj5XGz0$OI*@zus&lo$K*ap|gQtKX#Ok*t0jtQ=AWLziF zJJ@?Vr*UI&aCu3vN4dKD{h`6y#trxV!G7h{lKcS&*S80&xu00hCODrg z%I{I$z?H51D#=l+N@OEh`GLZ~)JBHVQyZ&Dy-QJ=K}qGrOs-WSLXe`#%1qZx$uJX! z`juCqGu_Cz8^Zg7yBk}`x{YghCU9|Ip>t|uz63?V+RRWQ~?WhGeu_QF{CJ0f-Ap^3p2~VKa@(AKOh!hQxgqh4Ppta8*BI-hw<(3 zEUn`E!BVaq<#&=Q9-`96g2|0rL`aXURD7z$fyNWTKx3WU7QRznK|-gva-82;G9ETX zu-JFUTfuiA_YQX?g`}54@`Ixq^C{#*B^MG`Dl%+TY}_nl*vD92DwpnPAm3?4^_|8K z$sbZ|DU*Gt@gv9-NKwjsB~uAR%lK88W-Lmuy78cC&cCF2o-3D)q>T?$aXFw2+$cX7 z{6l$u2SeZ8RT;Xe&KFpFFu9^frxqW4E^`~rL`#UY4+Z}aZMDKcV)-Uc%egzX;z)2+ zW+^cmk+v&mcf{gy=4J+Gp6H^Y!Cz*UaocRw@;&Xz%!f?JLafjH5&wsv=AC9-Qea!u zI~XWG7c9jticr~6{0yY-Z(LsNwZZ4+x!^dw8Znfo+Dq#q6{7_EEbb+4zAb<>0M|;<_=DKi07Rz zHs?&F_mc{WF6_>1>>?3VOf1gX4~tV>_XR{c6cJp!WC~t&$@HHQ%zlx;_Y%S5uTZv6 zArYTKB0hygd{HFgiy{$U6p8qvNW>RKBEBdR@hK$Y&ik!QNPxFH89rWO{GOZL8#F%o zcuR}FyMH|IK_AC^(5J_{7_aJq>TA=C0GN8XZsA*+wefBXBUpy6fdf~0j1orEebX#p zzbZRJav9HZ(Zd)0a`(sR3T#)OmyD52>i&N)0=|s#$6Q_*Z;n3(G&gXZ*a6}d! zM28Kn$N$I1CnAo8*LN~LDL>XxV(o?X#f(+5Oo7XRRC#ncA|0hqP3O7tkjzq` z?ug9N*x-AaJFg8EXYQ~EM^qFCgB8ueF%`k!kc#cW;yPYcv8KFCZC6-cg`h8-zYGRJ zdd)sIlfGBZwr6>RSn!QIhctc=oYc5K_+DcJ zztZ3U9AlsIqr^`JD;w`-kkPd~92^AN%9QgB!k-l#M>KzT?jMy0ysDi9E^C`RM|LV0=EKY;+tS zu)cx^%?|mR;A5_#%j_cI{aDg)OXFcg0O;_NT{F$`K(n^3 zOW}cLtx<(%yA-B0YlU6p`X-vGebQD6pGpHhq3t^~`33FVv5A|OjALqO$@rfUOnH%D z`bz|}|F_ZC$2>n#Po9Eje_Hat(3Ah;+DK2%dn%+SpWdz~pIxG!8`6_xOLy**?z}s{ z#}f$H%`H7RHy?L+3r6TSuk)U0)~C*iQzu)z`?(ZSah4`G-!6MTRtNRW5}Uf#;H?hd zo*f_SUHzV(S!!O}tJ0+>SyYO1Dmm=zeual%zUMERwysO{PGq>5iy(OFzko!$%qBT!v7>R5%CKLNII4)UDh{F_M2l~qs~syd zNpqM=I~tF9XKr^iZuQQD+79nb*xK8H(Tl4wKdk$%@g66943DT@2;eP!*}-tfJ3mja zIHhWM-dnoHLI3QX|C-LaW)VZRxAX-EZqYk`gaR`+UtIig)_${YoweVmy)#*o>h7|Z&sqWDf4b58 zn-Pwi^Ho@+!ZHCb+AK0JJ*Ag#j<)Cu%$ggOqVmUIrUT>Tl(cney`+@VQHmae%MqjRB9=>+WO2^wbHQJ zAwBHsQx54=*+(MX-0C3aYV$t#o_X&O_RPgf%$CD;?2LL97QPrbak49AQBBc}fsnq2%lnjS5HbLptWoTLi+bVUr3?+*IT&2sM>6)o! z&(S!0{u5nO%O<>;&AN7SwRanbBdgWfF7r@r9*(Z|qJ`yqRl^=pDzi>$+!m!#+v)9g znMAs%!+GV6)<4bGdM_xm>u|TrON} zW=b)Nqtr;iME>U3sUvW2Hp~#kHsCTLeH7M3hhGZOx57Iaf60KX zFdzlBvKJkmsR2cFfps1(T8ISgdEp1cw{bAp6Fn8CY&nTL~5&BIAV z&BIAV&BIAV&BIAV&BIAV&BIBg{XEXSbFmG1<{5S!iCaVuFUasSo z%iJIV3wPY_M9@4>6`qiui*;L53er}v z(#2h%Cze>3%Q`WAaDnq(`E-}_pOUOF@WfBgA%SgFUE-U8dvTA}dIPL`y;g-@i}$z9 zcCG$>b+$lo;hR2IV-x<`4h0pPZBd6e`^p8$`(%vTI&fQTUH`V)TpVMyX`JsGS$^dG z%|o`EK^OJ5)N=Oa*(!EOShwA~s@Yav;Qd?kn_9l64jU(_L&+b#tFmltCt0<~7Ao@Y zZ??a^PQo>tz57qu-!7DJ4H&uJ{&rBpHO1cFG}}^ldY7HDjoT<>E7p5gowDJwq<%%u ztC9kdMCNU|QD)vKAk`nVlIRI*$a%nHGN;S?w{D_E5zV)Zsfq;l&RC13d))~dxnH>E)_D!ZR)t*cvzd@$!erhjEA#j9`8bC(H86S*&Mtx z!*&3zgwxa%kJs{stfeQ+sPI1bhAnejUpcAU>3y!=US8(4tg%%TdY@Zk%PhSClwiv| z*;}sFk%c>382Yn{EsqE5wO@ia>y;PENonJ_7^%ic6=$Wvxh2!>1hZvYXC1#S{En^# z?XmHz-WDpOGPbwNysHk`LKPBU^MQ9&r7d(2g|NN7-@9tNEmSY@HAlUxify6O-u;K{ zZ-YXEwor@2*PK&VN>qH8cYme*?J5b^)Oh!Ax4+#W;hM9GTB)M5Tcxj7>1$Q`<0^fV zcXggEv+M@%>O)jK4v~m3at_((|KSJ_B=^g?za6o2zY>vnbGF;cj|pexr_}u&-vC|M zGS6HA46p%aa)P$aYISlPVkJnq*)AeKWiQ{_vT@%MmzN}r ztRQUG4(^h$UWFCBJHjlcTG3yvDARryE9jz$bRzcb>0LxG0oKH4$4LC3iu+Dtr(wdA+q&15^H|@U2h{XUt zN!{BUx){=DSN#s%#3$K7XC25V*@oHmbYdaxdLHk^)}lJ26+cARMLT@HSWyfgnkHw{ zF%u56V_L1HT_5>{w9=<(luywR3LT4cVB~FMj!|AkBF32H-|3jgtwaR=$yK<#k^C@ zo%&UZ+A63?9^N}7d3JKAa+RXC3TipUt5p6~O?*Q~$(esDf1e3DUC>}DcbM>E4hx{xg_*2_ zDy;Vw5D%zulbXTZtHLARGOpL)E96}igbG73bs!kZp)eE^S|BBd!cfdN2!?W)7|NkA zltW>tIP3Bs;V|lYT?n7Vf5^<%bIh!W|8OXD zf}ZrZ1wH9&1U)ggBIt=K3wjbU;FSZ7d5*axUW=tBnbd6oSu*1Uv8dp2Afo6Tz?1+XTNxZxj3*dYjJ0x!IHkIy-RSIErc2OGm*7xcxiN$+>!!8RBqK%r| zsW$J_TD)?0!aO@ix_0j7|4|2XnQo=kjyv}+GWhqj-{T?+g?cmBz%)cDD}-2%U)@)h z0QF+mi>wg(%qxHx)Pim`?NUc>$t;5Wb$TS93_T@(r$-sJMSqe&;_@fQC_#0h-FY;P zBFJ?{kLpjx!}5pERev&Ul|P{)zxL(Gz36?S>bvD~l=%z!bEr`9ttNfozwJoD>YU() zNV@3p8_Y5xZ z3=X6jn^4f$gi#gci}NP!wr684&Sxc^#my&mYm@U^&x#f`qTkU?G#$&kv8_B`9N*5`C}qZXEK@#? zhLuq6&F#ufORgCE!~R{IKh#`9cS-4~&-RU$e)y85uUvV#`(w+fXk^c@?2m2T8~ZNT z^*jf=-R?Q7`yX!b_-E93{0~=o{PXyj^%Es-)-jWNMaAWN0W;Ke_Zy<&L{Rol~5Xop-nHS#3^LdsYQ@FR!P3_+XnZ--W#`FE+?I z7XnP-4zP?%=rz8#fBHS_l{L49GKo7-&kgitv*@jl4`6zCn4Wvqk$JV+wA)bQy8BAa z^7uWqQe98p?7q%_JBYal@8f=&akeIj`%ZIzr`obxlgn-WOoiLB*PXlG;TasZPT>WK zz1Ul$LYoQ`2xF6+8(c|3jVax*W5GbT(IC6AbfI&i<~wrTXwY-3^4VRPd-zs;#p#88 zjK}ZMb2l&(EosP5^Xgq!w(jj#yJqimPIFFQw?)Zkl1OOMU2An{)qLQJZ0kkRrkvizev#bMMA}@Gz`L9hi4yPs`OHB+DIxXu{Eqmp|?KWp%eT@J1 zSXwRCWt?>-SFu}>&}k`lT7u3%u)F{EIBxPmcP^iGCDsr+EnA$HLT8}xO8@Qg+~i~M zTxOqR6`|AezSFY78Q5^O%l|#@aIMCb2<4ho%MSTqd=jOrQWx<3`1U29;v>K}w3gZz z!hB1<(^BX5=Nt2UhrWHOq1IAwv$>2zuH<@GVl!c?WxWd#^mST}sZ#Gvav5Ada=W)l-0f<|82vm8dU9+rwTL9xRP63i6KJ&+>unMw-jbMe{p%EjnN1#T3o8p zl$uyAw0lqq>bKX`qu6CRl@t8s;$`;+f-z2mWL6;&TekB!xXvjYCeTOOoo;#N*Pe#%7f&42{^Ll6?$gF25_OcdF$R zxpN__giquxW+8gsp?SNi<_$-ix7HQ&U{aiF;KOYK?cp>&OzrVuYGPw*k1ej4(MhS6 zBav2u7QyXchb2m|Qb9ufNTivrb(P ztc!74ii{dMjO1NTOCVB)A}>9}zGO_|eM`8Nyvq_)TD7#|rONwnJAKTZN zlW&sGhx~4neE!(H-f_;H0`o>e52|s4%gCogN-nXiZ!Lg2+g(P9D>*;8A+Nvv|6}fL zz@xg(dtYrMj0Ve|Q7p&8QG>=&6Je8BJJ1+nl4$UZy{8`8N=##t(1gUPLmD?B4kWmB z928-P9eLcGp0qd5IXS7@<~C`Ylatev%fZf7u-~(E(ZObX|HRtn#n=lmO|I0^rQui zRpGP#3BNa!pLreZ?mFpJC)HuE|Ex67I9?^ylj_kuFZ;dHKKXgqLlD(5%{|G3By{-c zs`RA$E7OxcU#*qe&`XDW3waxUzM5U~W1N3;MoIkZf=~SM8y^4gq%3KJ%*6ltU}Prj z^oRV%>pJHz)DaJT=}YQXVjAb%_s+oEm0GKe@vI&2kZPS^0)udOaV2pt41d1zveeo+ zR%{>$3mgS;H6?>i4rT~%Z7Um1)wrD+{kJZpY{e5T{0YKc#gD&qgk2QjrJc7{hPPC* zlOnvOs`J+B@Rn+4ZaeHKi_)nf(Rm>!J`h2_xwyOW>=?&Y)C3)Ui`=Oa4WArd{{GQ1 zze~qpTy)^a`1oB5I1qLK#>V1bU2E-33p#peXBS6RV0YC5XH*PNo>OX%ZzWjdseJS< zcbLX!O906kDL(v;Qd_{((GMa{2jMXqYoe{wrMCFeg9P zxTpbPcjj6TG6gQ_~L$Nz4 z7~-owt>5xrTjYMs=@5?H#Xq=lR6jnN=Er!`q75!3j~I`bQIZ1h3L z6C!5CvM-`KvpPHIzqqmgtD4H?pV-F-BYAH$Wctd^7{M-#-EvvQx4$J8SiR~-6fyYPOV{@>U<}2#04`WRefK*F~?xk6%6)QemZhuj^{m+cI|GDw@@4cY? zhTpOMcb40qS8jj)c>A}Gx1YP9{pR1X{l52=@KaxI|Jw2P-!<~k zVG{@Sad%}q`z^=z1)Wo@)l2E4%ARf^27(i#u5L;2$aGx6g*mHlkDi)W-#*Q~Yg&E# zH8>z|V+Uu$@Plgh|8Z;9SHtSt-{yL#TYY=IdrGXn{r6Vi^LDzMK5XXrcj(H-z)4KE zHn`z5@YqxyPV~fZW|W6>oqGa+>)jIoaQcoA008e^eb0LU;D+J90QWWw*|8oq*-&39#n6cVPMb?g_ANa!-IY&piRw2Ug$nLBRUZ>PvqG4JtS*NN0@U z3in)#2eFiYCvc@o|DL(%`yzgG^8PooOkRfHPI5{~6S&U{^x>CWF|!5*`m<;h{`8hV zydWC&fxv@nYwiee!`eFS*fwoQz2g@vg}fOXbDXC(_xM){MaktXKAyU)V$%PX>>U2Q zz1ms)M_%=tM&vD3_GG=iWp5uJ+uyAuVEwo_g=$?>vH(Vv{=H5`~4pGd%uug(Vwcp$VZp4`MMNLyHT$@o}aSp zs%-v_N-9TQ1x2po8It|~>RAkU_c`gu-Pj9KXxpU^s@F$Hwqdp(P?ya56W-`h9 zdE?LdqR(IQxpM#Cu3X=;`nKxEzpZXqeRHjn9FXGE#rh3r#`t8mQEb;^{t;`0?~ZuI zFFeUxN;jzVE9=Xp?~F>T=kd}XpmgUd2hJhCh?i?n_(x}b*V;|ba`3FavwHPcE)TaA z4kcFKc|~}LP427jjIX{evHA-&;RbvA7BXvB-*zRyuD<)Kupl*Z+gtU?+pE*zvDJ6h zt^Ufiba>Q$xyD*}TR5EU=&fp{!bX zyt_Dpc0gfO>-0ID=EcNyBI_8#U*tc2z(L{SvH(1O+<$odTJv5ER!QVunD;xxufGRIir2g+B9xm^8LD2q3Yx=* zD=E;~+_{7k7Zz4^F0AfsN!;_E`1Do0M&T%)jYuscP`n}tYa4USpTDCTQwbK=r90&tl9#&dx_>2`lqJCb18zt{)-aPB}8>x!1;=xv$C->Kw^nH z!`_bjaf8gn0K5NR6kw^E5v-9Ue;i&!zoiX|LNmQ|PWH z?m0CZ?uOaZ{&3pc;16ZPLF=Q1SOK%|v^S6oHxT_Q^i3@kCZ|0jx?P0N{edh;$A!-* zIR8M?M{qRWas_cftbj z6fL?!L@+mJ(NmOdadG&VgP)MtK84Et5m&H*5`?M;QecR&&?RBM0gJ~|w96H0q!2Bx zR}`;MxLjj#qsTBEf4~)Lq7Y$yEg2AGJ2e>F=@-3`RK9*nd(VN+zi%t_;DzNHmv|e) zQGPA@J3Xt(z~>NVoYLN>QX2o_LSHNwez$QoBu{(WqsB&V&R1WP&es#@v-TLz6%Iq| z(|*A)wBKRg78k;9&`&shQ=x}Z^~6%%*>KmXp%{ePHR*xII`4FQK^-#cxbT<`4-$?P zCjBF>nPwWO7V^CFM!d_sq4311ZRyyv{wAV=_1>ZOZr4KJr4G3)9=ymzk_xOJxJG8v zNW$ya3PR7+Lign!yt0$w5BP%wHk;FVi)b2t`lpBj5$ZiaInD6mT81fz&fZ8kK*d2Y z0%6km+GZl2uKz*|LWn9_XMwP@;id=Y!A)tfNf5X@4wWPq-BUB(Zs9W>gS3l?3wh5F z_Dp#_<4COnl?V`wr*)uxiX*oG{A9j)#mR0#pbKy|ro+BdMANr(DrmwRPWyY(`I%P` zAhp4CO-Vz1!z(PZK3On9(%Q@V`T2Uky~i~q`P-Hbx2vH&Y0g@$O?$`E{wv;g#}0*_ zOTDu~ZrF|BC3kyA3VpQkoOhUw8fvGZb3zvzQ-5|H%52cgq4M#m! z_(R@6gxP-TA5Mp-)c{rDD4K7aLDb%%HRkOcXZmCT9)iB>u0R63T@D0b0;#X2!yQ7& zUXghYYe)V;$a79#ybjbt1iUtx(T?Y@-r8q_43!D zwMn`Z9Oq|FiI)BhwP(?4D3as|SVJ&CGkR)tvZGEf)Y%l_*2HuYYuFeX@XGo~JX#+S z-=@8NVF8v;C*l2pB+xA98p7K-tIy{h-Crh>S$q0de%D{Zt%M~ ziI+8+2ERK$^S$l8u7!1oF)P|qv*(SoGv0`;2){BvKkw3ZTc{Bec-z85{)S|?*sTfH z^*5j`-U4SEh59c|vrKAA^0`w_#mE37#GVn=yAYx?D7dN7;!*+p?o5Z>{-NXstz=dE zhtL++z}wbP4_flDskbwN6}=+o+zKpPzkQw0MPO_6q56L)!079!h(xVRd_h zhTcNo704v8gu7|tSQ>U|fI|yim-=T|7JGkrS*&ZSHy|y#ANV?oV=ItnBHp7Z(ct)rdW3z}}6lxnUDG5)1oC&dMNDb$VVFyNe%G){tqa23| zzS6PhGTyMN&zuS`9{=FdKP&WH;T;zT==a#}9R5a2nsxd@Z!GQamQitI!9Vm^Y=n5} z*ddff0M*j@*+9`}&Gcb5whN~+)guxv7EAl4$OI1B@}Ovji-kX^CXT_-3$aQ762Z}R zc^ZWX99V)_L6on>&+vE6&(}_CkJc$+LTOl)V0HHaX{2E=J{OD|5Z#yJVq((^6d2<7 z1OvN_mE3$KMWRJenVaWjwbHR&85XVmqv$k@QNLP%9VbgKqoapZqLAhk@5Z$Eyt9Al zeDyRKQ5Z2Di&n!y!fr{aE%t|D;~5SV_z@94>I|2PtVgf{Y%@kIU+b4yOt$#A8L!wb zttHnYzA&-xunJkHq`YpY2-d5aAy8k9*^u=rI7lQ_-YX98SnGN)1lIGjn@Xf9Opdhc zfOJf90NN~sHdw$xue-hbx4(SbUxIhCCBhN8IV0pZfFi9x%g1qum4X$Mg@gV;I*I!; z3TmtU0dzn`d%ray#G=%kzbCw*1rCxL;+=~q++U3jko6ix?G3<)h2G2kL9|2_#8DIS z$Gm5qw^vLv7AoKmjCegIq;_4A@dgd4UH)itP@+3C;~g?z3oVg=IIbZy+VQXmEA9lT z%`B%!-k&3Kf!WD=Q%0bqD>;gA)|svcCtbp;4MnDCm84U#0{7v|VISbk|hP zH9$yL+ItS@FXzjJC^f$xoTjk9J0WacRKGipSr;x0p95)cjM}u6pEn0qbYtj-tm*L7 zsU5LXIn*2zBUa9Lct=XC+K&qArwww2971;%`lrY>?V5lP*AJmAnbL zR!pzJCawgCv@9m29vBW#P2N`8A4%>+-&qzzqP=(ctAFp=wT#@p&IyT2B3ZXD5K6E$WfRxz~w$IvO zW&%x^3BiO=woQaZC$_J1?U28tX{T^Pasq`0AZ#M0hni`l6ISYb(--C ze}hog-s4&jewh_8*FKXx1d8}X9x%U!2*DucxZ!2DzgvbCsQ^!x%3{^N7dp6R#Pa*q z${~*3XZU&`t)tFdfNsR`IY%+)^ZeH9Xm^+l*F_J*IW5O z{&9@-m2jrDgJK&B5BtZH8znIQ@w9gcvR{fpp@`8aGSJ7Ebka(-CM^YAqvv2JjBKy9 zBCb8nEVBq>i>r8$?eDEZ)aJko4kxC?_hr0ZF)bsOOp|9+TLGD^8BL8bIs+0kx>+~X zA3)j8l;kko9+~W#ai%_lp7O@R^WYucMp@$?m1*P^(?oKdYeEo0mS;oc-#E7GuOd56 zQvxIjM~bp%oX-c)$SAG=-X!i5&KV(qfC!Ygy@dKMqCXNJIX~UNhZvt0(`yz6H}7H$ z!lgXCGxTc;qz9whBbEd^lZ|iWSY^Be=@{J~)72!*W(j>gT_TKB#Rj0p*BLba95rpB z*BY7Nf{XiUcZ-wVb>cyO?N+-l;mL?bu+|)=H5rLk;GAR|p!bY7m!MkAKIH>sTDB_X3QE*vkx7YqnR zE+I}KS8xUNCI5{%g=1sDdlCJs=|rC<$Oj^6>zkY=`O$mUa5LujyP20Gw{TNuC}6gU zrewlPn4Tod)EzER9U;;TC{T?(p%`#6C zfg8gfI!8M$1+C~NkZ66Q@XhRlw(kd5a^Ur#_u%DXrGTe=Yeu#0VP`J$7h{z~K ztrWBk3E74%RpCO?a@!IinY)9TuqQ`IR{`3rrLLt85l(Mkry%g}5YIn;^@MnSkpFmn z=ln%c{#yMT=dZQDb3zDQSw$;{-z0zSE=szH{;dnjL37o(of`eOE&{^w{I&nm`)2uT zEFQCPeBqLSTQ7T`1KuUw2m|E*thA4RFXQJiO6fmql@$hCU$Fe|Xr1Gkw;Eo`d2Q^i zU{QMBvOBWBVR=*k#k0z{(tj(nT1(P@BiUcMhl&dV>Ma#H?|@5&<2sk3ashMRk=)$# zUpuHZ#J2>61Cz?Ra>WLIo;cdnqSPO|EcG{8Kbu2^;&l$-)^J^5nf&R_$5-+`ty{3B zkWqT-*^U>d5ziHZb&ZSa?f#s{Oemp5P3_Oo|ZviwM@B$Xc;VTJARwAZz- zpSJN62pZnc=M!TfNAO?!Rt@l1LTWIBMEN77Ufz09FJE)LWM{l2SSO(Op*(emd-+pZ z#35~HO3+bgY31~0aISUCf4cUv3eBQe^f0<`l?A3~ea(Dg$jIx|6End)e4RU2u?JtNdU{DZod(OH!7pboD`s2-4M*0e)u(9*6)LFoq4{e1* z9s8#&yTZn{^+-$A1DCcOp4^wdv}I*}g~z3xba3B|?PpH$`L$`lO|}3Ca+$d)TbcY( zlD6|77Z;QwK`|A&re*ka4H=ejOp13^`Pkgi?p7&Tb^%cXUerdvv@y=cAk^0ye)euwqahMGGXJ3qDL;`P#|H(4)T z(4h6wC8hP!ri<50XY+e$c&uUm$FG+zVZBr<_)%iR&!_(m>!mU##Y2gRu^wp@X$)02 zywjGJWqtK7-I?fIQK==7mSCB-nj4OS&U(Inq9(SX@I2+K`I3E0VSiQZuq*iWE29dvRA{6^ zY)}qKe9cF$qJ!9>*skdJw-#Qkifv29y3&BGHv@qtm-r_F;i_qi2}Ff zn%EZByB+{KwQexHEq0gzucVz=0fgnElJ&I05Uj}Wsg?bSSTyLCx2ry$ko~CT(H5`} z^_Wz58}wT`zUIr*Q!!Sav_|dvzjk8J@30ORM?W>v;cPmJn*IHW9lmp7hfQ=?JKo`p za)%)FUDjkeYVx{?yWySsQ1b_c1BnvVYf6XD7J9EthX+_fO64p4$?yI4_l~pr=CiVWB0QX9AR51B z1pY>Eqj%QpQIsSV|4CQPtQ>(NCe*pc>Y-uYB3x5q1>Vjh&Stadf=^CB*VP0oAO^b< z(~Zmc&JH5bN4RO2D0#Sxq5HcEJvGc-!aapPfEgwpkguFJ`A=&7gMx*yhj8Q{^|txD zyiHy&4Y2|9C*3u(7Br?){!?;>hM1onoI->e02VUyFaU<=xk!(GvCv!N_cCh(m?3~+ z5qweOicGk>^Sg#?8ez`J{1M=aq(Klqv~Y9*a7I|U;qFF%y+6#N+Kzl}Q+xNurq669 zWbJJxvb=rP1C2`np-Jl^r*SxPh3BR)jZX|6iR~aSsyj}Bz~ADt5ZYZRTY!bmo0oL2rtcU?z>g}_k))f3M@&5qe5fKAJk-Ba4LCW(DsUTg-mZ_J_{%{ zevh#W->dW0b?t>u-cs`w`c^svCSrgH@HP@jdc94+kN}2ICc5@ehOC*{}t=H9X^FXwF0#N;nznE6LCt`mRfbXQ{Tt__W6fFpS~^xaW1e zf=D4kb?w~;Q7(}P^*CPHWW2HsoYaC7IE6(YT5g-DSgOkMqx`U#e@INSO~{ajYm-bj zXF}mMay-RZHAwiS)1Vck4%_J7)kUn_>_CPBlG_c3_Q6|P76f*8OJ%Kys zh&!ZF6X1ft=@9FHlH%AFM0d914mkCVxI-Y#lrpK#ga^eWDSwQ}xZ_)N#!k~-Fh?pJ zgk{RyarS1G$tKT5j|dwZQ!G>U&{-AOXagF5l%*h=Ybx|lvHtn3*6$Yy$TgwQg%fEm zf@t<%hn-@vT^bgZp>3W(K>RaO6GnQ9);q$t5H1Gg1f^M#B_#P36bpo8J${>VcF5OA zh26Y}i%bnEe_h6Znlw3J;5p3+2&5r$Gjt|APS?#&gE}Q99fq#xxzH#Tt}JT|uMaS0 zIxuGY12#fN+sJ4c=p2z;6fn?8{OLY`YH^^E_+?65I9Q^*qDVclVh5|SzMDLaUk3k| zOp5egRVF+nWTvFmpTfwH)+8=Sjz~|Cq*MT$g#t%d9yM@SsB_S?IcPB8h?L*n{jER$ z_J0FQ(l2F5nw2L-pBOJ9@T_967M)HO04D@r*dL<%vOg?8FJnOg{i?h=FzT^$7-*g} zrID2B?eR8>k}?|z1_CF`nhuXRVCtnCFi}Vrr$kJh96wQelNhMSFPq%7g5jn%_pzoX zCnQFm)h@6y!JN_5Hr5tk7*bSBMXK#sbGad6cpJd4hwusT)ZrsxBAZd#rVR6l2WT7L^umLioy?fr%cfKS*wvSUMP;#gFJY8v; zWC@ULgXI2;`GkeVOn3@Ns#5+AQsInGwv6)$-mLf}?EtBd00EJ;4wAZYwlzk%|HnVt zc`>8Rv*A}LyI3pglijiI ziOd2FZL-SD-%@_JmgmJQPidXySaP?24Bum$#4JGZh-(+hD1-GLw9b%L^wOBg+%Q#E5yw z!I4vo$zjnx+?hezl12VvVECUB^N>vehJNhIgzE&DBUxJcSN=hDQdn2b?)I6i_-wxU z1HC{4nqJBHj6$Bh5S5^LTqYEGhPCzZ=}heDRPxD8Y+WkEd+Um&%!y6I`gA#yf9TAv zj5i7&kvN$OkEHy)spKjB^;o1lEg4+QKyd^{QhtAiX;m3x8eY!49^5YKmf)KWeE$s% zB=|_UF!=g`Zxp;$s}~e7-gYXvL*tA+ok;@bYnhNGAF>N@wopSEpTNu(aS7?;CxCMf z^iq~^FL1snO_V%cRzy`!5j8VAu}0z=n z2GS_aFd%hk#3GruNz#8Wm=j?^>!hpXifC<^1*yI(y@JbBz`AQbB!ndlVc`JO#1R8& ztR~}1N#l#n=9!;5fW8M45Y(o)Uqv}Ns5WIZ6DT2!1Pshvm$7~^6|T?3hBL`R>SFbT zG-bkLjJqE0m;LAmPaBx?vNF%|XCKMP09I{)VNWIig>x#P7CWFK-mkt6M7@NXav*KC z$w>($Qq19f=?CNep;TuI%k7<@fX8x<<4;G%p{xgmJVKUNuQ(-aX@606k z@s}(!{-;1g#QnG?aKF;m<0U;_mGS#B{^m>w9>{Of%y=hi3$VR@td)455>n+&N+KR- z{-F%1xk)evg`~w zwr7&tIV5DF0yAmKpiD^%O8G~bM|H#Dfsh9(++6H;-U$dv`5bNgl!0$TeXiq*ZxulbbwXQ1Oy*P4JSA0?Gwm z64nTZz)jKRLzrtKmF$?tIY|p0Bn@TPgv}D-<0eNX=bGRn%|H zbWL98A))usbRtra@pr+v#xLtj{F31ip^*FuvCM1%gAy%y)o_o4rq)4IC^<@ZqJzvm zdD?kO0%xX)hSZN^Ufh9_d-8gIVaBrumOz3xi-W~4+vP3c>Y;tMFn>wgyc=lhfkqBW ziC^$Jktg^??f?;$YugJyLh&egu*GMy0y7Mgr8=dLyb=Fitab3*bAjP+iUp7rp zGLBzt;|~4;2XUO+g{*NBL>c+-kQ-S6@DY%Wn;d& zslD(4G({9A>4A$J3o&Jo)>&s;$i$5h5l5K`6($>Qlv!@dgi5|c6{6S?-U}H*_D7;a zKje^BH?$X+CVh*JNFv{zPCn zDaJI>AK!hE|I2=h$T8WBQ%G6H>20?F3)7yrLn9(PeIj>>)~G9N3VyM;SIT!ZJXUh8 zWdQNKUIC{lD`aQS#OaV|f-?giw*z?-kTapdfd!N1aX=!?V$t#gWINl&HU;QLq)(5L zyp%kn$;5@k$%I=)K%1hY{ao`3PBpZ5GfBDM2B6fVo+~2(hD<~j@gck&)NC;8+=aQG zX%V1#7tks^lp&VbJw-DZY2+%<+ZVHVVv}(aknJX~DtQdZ7&}Mw>D({{jz|yN25}wd zMDm(O@P@^#bJ`0`SdJUB5_x3;PJU`{6htrUZdeH2jgdlk%m0`oO<=QHlCns?!`~~D zMS%^d;snBCsZ2o*(t)=L1%;lDY2r+nF$#;Cok9+n&Lam*p#}L&W)_Oq5((kpKXX#p zT17q~gw6zJGW~w)*LM8vlA+T$B6rFB0h|;30U{C`tSTF^V(z>kEQQaB-` zqe?xbJvLcb3aSelU@GB1MSl~LSJA(ujC+J(=2cot4;g_@uH6Vs~VpB|Az6jBYV%mI>#R8UHnH(j%OQmWRXGOGMxWg733=1!28{be{Yi zv^`O7jJrY?K!hlE+YSSXiW~s>C=d{uqkJG3-xz&DZumZFH==S(3W$CwSJvAiwL^qs zkY>>}EIBFUVC`zX&Ro2mW*oLmGz6x|Wg2ya3C%C0Urqa@^BYK|XZqK}#+}Iu66h%L z#n7H&Fw;bzI|z3M@bnT$Ve#eKl)tCYGX>DEL^=4ol7&zck!mm%7&`0Ndq`_J1Y2kG z5~+HIF*=$xLI_rT!anw|!$$RR(J{2h%oImRNCgoTlqaC2ulkY;fCfS*?w2+si+HoV zVt9Z6grv@nku=prGxL}_6X_Ju7DBYN69l57%wEYKCK|_fPwiTgGaPQUT}OBWr_nkJ z{GCytZ3DY8P}J4hX|V*0OaxQm3R&bKkIn0^kh+?~uRe-0XO_wIp9!4|KFx-^XlS?E zITK?;nu{h~2{d(Vc92=bDzVp_>@eI?;Ae_JPpN#h?*b4|6rGH#Lhz*uTud<&NER{Y zM!I!D%oN@TVL3%&38|r=7isS)35M*h1tv{3W?P6lMP!BLAN)B53Sj09fEf-^zBY-9 z*`A8+NhQyxIG!~ef_^w7Y+6v%*IW&qXd7+~QS4-V=WDNT@3uGuBtjaMuuXV?u4Y2S zW^iNc7BSmTOZ=xFrrttEvci(#E1FOz&(lS{AUcT=H7Hw1;|Db?dc|P6T3n)3@|g^5 z9^08pc4cBa`LR)=y^->OLFfvJQ87Sb#B9C4$;BZIZ5wgK@!@1qyiuK#z!Di)&-f=m z84HCe9Y8D>4}bYdz7SqBFy`2%ID&{hV3{}~TamC3u`Xc6~rS-C$4mtdFh`wwR zVK`2p1%+tD?IHSy3~6n2L*GF0Q8bi4>h&rvqyPf~>leIUFhYAf<=G&mOEdmfQ`8xM ze>gnrCzeU!qlQ>l=0p`Gl9zC4CEG8QUai>& z%(FC2o^`k+5iYGwhfC_x;R*?AE=u*{NZ*j73S90M`IsCUI%T(IUMrpKqV&b_^HVww zO(n~d9Np-=(5BA$oUUl+g*Lr$zIDy%spBbh%ARi7^(Oh&2S=w?a5Pot9gXqz;T?

EZ~Uf;eu&Gc_v>%|9qM>uzL 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 /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] DATAPSPSD { + public static byte[] DATAPSPSD { get { object obj = ResourceManager.GetObject("DATAPSPSD", resourceCulture); return ((byte[])(obj)); @@ -73,7 +73,7 @@ namespace PopsBuilder { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] DATAPSPSDCFG { + public static byte[] DATAPSPSDCFG { get { object obj = ResourceManager.GetObject("DATAPSPSDCFG", resourceCulture); return ((byte[])(obj)); @@ -83,7 +83,7 @@ namespace PopsBuilder { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] SIMPLE { + public static byte[] SIMPLE { get { object obj = ResourceManager.GetObject("SIMPLE", resourceCulture); return ((byte[])(obj)); @@ -93,9 +93,29 @@ namespace PopsBuilder { /// /// Looks up a localized resource of type System.Byte[]. /// - internal static byte[] STARTDAT { + public static byte[] STARTDATMINIS { get { - object obj = ResourceManager.GetObject("STARTDAT", resourceCulture); + object obj = ResourceManager.GetObject("STARTDATMINIS", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + public static byte[] STARTDATPOPS { + get { + object obj = ResourceManager.GetObject("STARTDATPOPS", resourceCulture); + return ((byte[])(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + public static byte[] STARTDATPSP { + get { + object obj = ResourceManager.GetObject("STARTDATPSP", resourceCulture); return ((byte[])(obj)); } } diff --git a/PopsBuilder/Resources.resx b/PopsBuilder/Resources.resx index 285a13a..c1d0217 100644 --- a/PopsBuilder/Resources.resx +++ b/PopsBuilder/Resources.resx @@ -127,7 +127,13 @@ Resources\SIMPLE.PNG;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - Resources\STARTDAT.PNG;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\STARTDATMINIS.PNG;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\STARTDATPOPS.PNG;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Resources\STARTDATPSP.PNG;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 \ No newline at end of file diff --git a/PopsBuilder/Resources/SIMPLE.PNG b/PopsBuilder/Resources/SIMPLE.PNG index c3850e7f70019f9da61a81bb69d99d3099d0290b..e8704d6f71bf82e3b1a2e7dce9396b3e73fa3b0a 100644 GIT binary patch delta 25884 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 diff --git a/PopsBuilder/Resources/STARTDATMINIS.PNG b/PopsBuilder/Resources/STARTDATMINIS.PNG new file mode 100644 index 0000000000000000000000000000000000000000..46de6c58a3beed72e11ef443fe5ed4fd1bb217ac GIT binary patch literal 7374 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 literal 0 HcmV?d00001 diff --git a/PopsBuilder/Resources/STARTDAT.PNG b/PopsBuilder/Resources/STARTDATPOPS.PNG similarity index 100% rename from PopsBuilder/Resources/STARTDAT.PNG rename to PopsBuilder/Resources/STARTDATPOPS.PNG diff --git a/PopsBuilder/Resources/STARTDATPSP.PNG b/PopsBuilder/Resources/STARTDATPSP.PNG new file mode 100644 index 0000000000000000000000000000000000000000..eefdc626acd78aea9059d655d63ca1606df50f6f GIT binary patch literal 4935 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 diff --git a/PopsBuilder/StreamUtil.cs b/PopsBuilder/StreamUtil.cs index 2aac518..3819312 100644 --- a/PopsBuilder/StreamUtil.cs +++ b/PopsBuilder/StreamUtil.cs @@ -1,4 +1,5 @@ -using System; +using PspCrypto; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -9,11 +10,9 @@ namespace PopsBuilder public class StreamUtil { private Stream s; - private Random rng; public StreamUtil(Stream s) { this.s = s; - rng = new Random(); } public void WriteStrWithPadding(string str, byte b, int len) @@ -30,13 +29,6 @@ namespace PopsBuilder WritePadding(b, (len - sdata.Length)); } } - - public void WriteRandom(int len) - { - byte[] randomBytes = new byte[len]; - rng.NextBytes(randomBytes); - this.WriteBytes(randomBytes); - } public byte[] ReadBytes(int len) { byte[] data = new byte[len]; @@ -80,7 +72,10 @@ namespace PopsBuilder { WriteBytes(BitConverter.GetBytes(v)); } - + public void WriteInt32BE(Int32 v) + { + WriteBytes(BitConverter.GetBytes(v).Reverse().ToArray()); + } public void WriteInt32(Int32 v) { WriteBytes(BitConverter.GetBytes(v)); @@ -98,6 +93,11 @@ namespace PopsBuilder } } + public void AlignTo(byte padByte, int align) + { + int padAmt = Convert.ToInt32(align - (s.Position % align)); + this.WritePadding(padByte, padAmt); + } public void PadUntil(byte b, int len) { int remain = Convert.ToInt32(len - s.Length); diff --git a/PspCrypto/Lz.cs b/PspCrypto/Lz.cs index f6d8144..a51cf49 100644 --- a/PspCrypto/Lz.cs +++ b/PspCrypto/Lz.cs @@ -10,7 +10,7 @@ namespace PspCrypto { public class Lz { - public static byte[] compress(byte[] in_buf) + public static byte[] compress(byte[] in_buf, bool np9660=false) { //Decoder decoder = new Decoder(); //using var inStream = new MemoryStream(@in); @@ -20,22 +20,21 @@ namespace PspCrypto //decoder.SetDecoderProperties(properties); //decoder.Code(inStream, outStream, insize, size, null); //return 0; - var lzrc = new Lzrc(); + var lzrc = new Lzrc(np9660); - // create a buffer hopefully big enough to hold compression result - byte[] compression_result = new byte[in_buf.Length + 0xffff]; // in worst case (i.e random data), - // compression makes the data larger - // so have to make sure theres extra space .. + // create a buffer big enough to hold compression result + byte[] compression_result = new byte[in_buf.Length]; + // (this could get resized by the compression code, if its too small) // compress data, and get the compressed data length - int compressed_length = lzrc.lzrc_compress(compression_result, compression_result.Length, in_buf, in_buf.Length); + int compressed_length = lzrc.lzrc_compress(ref compression_result, compression_result.Length, in_buf, in_buf.Length); // resize array to actual compressed length ... Array.Resize(ref compression_result, compressed_length); return compression_result; } - public static int decompress(byte[] @out, byte[] @in, int size, int insize) + public static int decompress(byte[] @out, byte[] @in, int size, int insize, bool np9660=false) { //Decoder decoder = new Decoder(); //using var inStream = new MemoryStream(@in); @@ -45,7 +44,7 @@ namespace PspCrypto //decoder.SetDecoderProperties(properties); //decoder.Code(inStream, outStream, insize, size, null); //return 0; - var lzrc = new Lzrc(); + var lzrc = new Lzrc(np9660); lzrc.lzrc_decompress(@out, size, @in, insize); return 0; } diff --git a/PspCrypto/Lzrc.cs b/PspCrypto/Lzrc.cs index 31febb8..6d0a05a 100644 --- a/PspCrypto/Lzrc.cs +++ b/PspCrypto/Lzrc.cs @@ -7,6 +7,8 @@ namespace PspCrypto { public class Lzrc { + private bool np9660; + private byte[] input; private int in_ptr; private int in_len; @@ -19,11 +21,11 @@ namespace PspCrypto private uint code; private uint out_code; private byte lc; - private byte[][] bm_literal = new byte[8][]; - private byte[][] bm_dist_bits = new byte[8][]; - private byte[][] bm_dist = new byte[16][]; - private byte[][] bm_match = new byte[8][]; - private byte[][] bm_len = new byte[8][]; + private byte[][] bm_literal; + private byte[][] bm_dist_bits; + private byte[][] bm_dist; + private byte[][] bm_match; + private byte[][] bm_len; const int max_tbl_sz = 65280; const int tbl_sz = 65536; @@ -35,6 +37,28 @@ namespace PspCrypto static int[] prev = new int[tbl_sz], next = new int[tbl_sz]; static int[] root = new int[tbl_sz]; + public Lzrc(bool np9660 = false) + { + this.np9660 = np9660; + + if (np9660) + { + this.bm_literal = new byte[8][]; + this.bm_dist_bits = new byte[8][]; + this.bm_dist = new byte[18][]; + this.bm_match = new byte[8][]; + this.bm_len = new byte[8][]; + } + else + { + this.bm_literal = new byte[8][]; + this.bm_dist_bits = new byte[8][]; + this.bm_dist = new byte[16][]; + this.bm_match = new byte[8][]; + this.bm_len = new byte[8][]; + } + } + static void Init(byte[][] arr, byte value, int length) { for (int i = 0; i < arr.Length; i++) @@ -85,19 +109,23 @@ namespace PspCrypto re_putbyte(lc); -#if NP9660 - Init(bm_literal, 0x80, 2048); - Init(bm_dist_bits, 0x80, 312); - Init(bm_dist, 0x80, 144); - Init(bm_match, 0x80, 64); - Init(bm_len, 0x80, 248); -#else - Init(bm_literal, 0x80, 256); // 2048 2680 2656 - Init(bm_dist_bits, 0x80, 23); // 184 - Init(bm_dist, 0x80, 8); // 128 - Init(bm_match, 0x80, 8); // 64 - Init(bm_len, 0x80, 32); // 256 -#endif + if (this.np9660) + { + Init(bm_literal, 0x80, 256); + Init(bm_dist_bits, 0x80, 39); + Init(bm_dist, 0x80, 8); + Init(bm_match, 0x80, 8); + Init(bm_len, 0x80, 31); + } + else + { + Init(bm_literal, 0x80, 256); // 2048 2680 2656 + Init(bm_dist_bits, 0x80, 23); // 184 + Init(bm_dist, 0x80, 8); // 128 + Init(bm_match, 0x80, 8); // 64 + Init(bm_len, 0x80, 32); // 256 + + } //memset(re->bm_literal, 0x80, 2048); //memset(re->bm_dist_bits, 0x80, 312); //memset(re->bm_dist, 0x80, 144); @@ -122,12 +150,25 @@ namespace PspCrypto (rc_getbyte() << 8) | rc_getbyte()); out_code = 0xffffffff; + + if (this.np9660) + { + Init(bm_literal, 0x80, 256); + Init(bm_dist_bits, 0x80, 39); + Init(bm_dist, 0x80, 8); + Init(bm_match, 0x80, 8); + Init(bm_len, 0x80, 31); + } + else + { + Init(bm_literal, 0x80, 256); // 2048 2680 2656 + Init(bm_dist_bits, 0x80, 23); // 184 + Init(bm_dist, 0x80, 8); // 128 + Init(bm_match, 0x80, 8); // 64 + Init(bm_len, 0x80, 32); // 256 + + } - Init(bm_literal, 0x80, 256); // 2048 2680 2656 - Init(bm_dist_bits, 0x80, 23); // 184 - Init(bm_dist, 0x80, 8); // 128 - Init(bm_match, 0x80, 8); // 64 - Init(bm_len, 0x80, 32); // 256 } void normalize() @@ -335,16 +376,16 @@ namespace PspCrypto match_len = t_len; match_dist = t_pos; - if (in_ptr == in_len) + if (in_ptr >= in_len) { match_len = 256; return 0; } - if (in_ptr == (in_len - 1)) + if (in_ptr >= (in_len - 1)) return 0; - t = text_buf[pos+0]; + t = text_buf[pos]; t = (t << 8) | text_buf[pos+1]; if (root[t] == -1) { @@ -395,6 +436,14 @@ namespace PspCrypto p = next[p]; } + if (this.np9660) + { + // have we calculated match_dist of 256 when its not the end? + if (t_len == 256 && in_ptr < in_len) + return 1; + } + if (t_pos < 0) throw new Exception("t_pos was < 0 :?"); // TODO: figure out why this happens on np9660. + match_len = t_len; match_dist = t_pos; @@ -447,14 +496,12 @@ namespace PspCrypto do { - tmp = number >> n; bit = (number >> (n - 1)) & 1; re_bit(ref probs, index + tmp, bit); n -= 1; } while (n > 0); - //number = (res_number - limit); } void re_bit(ref byte[] prob, int index, int bit) @@ -520,7 +567,8 @@ namespace PspCrypto { if (out_ptr == out_len) { - throw new Exception("Output overflow!"); + out_len += 0x100; + Array.Resize(ref output, out_len); } output[out_ptr++] = out_byte; @@ -589,7 +637,7 @@ namespace PspCrypto re_putbyte((byte)((code >> 8) & 0xff)); re_putbyte((byte)((code >> 0) & 0xff)); } - public int lzrc_compress(byte[] out_buf, int out_len, byte[] in_buf, int in_len) + public int lzrc_compress(ref byte[] out_buf, int out_len, byte[] in_buf, int in_len) { int match_step, re_state, len_state, dist_state; int i, cur_byte, last_byte; @@ -610,6 +658,8 @@ namespace PspCrypto last_byte = 0; match_len = 0; match_dist = 0; + + bool flg = false; while (true) { @@ -621,19 +671,24 @@ namespace PspCrypto if (match_len < 256) { -#if NP9660 - if (match_len < 4 && match_dist > 255) -#else - if (match_len <= 4 && match_dist > 255) -#endif + // condition is different if np9660 vs pops + if (this.np9660) + flg = (match_len < 4 && match_dist > 255); + else + flg = (match_len <= 4 && match_dist > 255); + + if(flg) // if (condition) match_len = 1; update_tree(match_len); } -#if NP9660 - if ((match_len == 1 || (match_len < 4 && match_dist > 255))) -#else - if ((match_len == 1 || (match_len <= 4 && match_dist > 255))) -#endif + + // condition is different if np9660 vs pops + if (this.np9660) + flg = (match_len == 1 || (match_len < 4 && match_dist > 255)); + else + flg = (match_len == 1 || (match_len <= 4 && match_dist > 255)); + + if (flg) { re_bit(ref bm_match[re_state], match_step, 0); @@ -643,7 +698,7 @@ namespace PspCrypto cur_byte = re_getbyte(); re_bittree(ref bm_literal[((last_byte >> lc) & 0x07)], 0, 0x100, cur_byte); - if (in_ptr == in_len) + if (in_ptr >= in_len) { re_normalize(); re_flush(); @@ -665,44 +720,51 @@ namespace PspCrypto len_bits += 1; } if (i != 8) - { re_bit(ref bm_match[re_state], match_step, 0); - } if (len_bits > 0) { len_state = ((len_bits - 1) << 2) + ((in_ptr << (len_bits - 1)) & 0x03); re_number(ref bm_len[re_state], len_state, len_bits, (match_len - 1)); - if (in_ptr == in_len) + if (this.np9660) + flg = (match_len == 256); + else + flg = (in_ptr >= in_len); + + if (flg) { re_normalize(); re_flush(); return out_ptr; } } - // determine limit ... dist_state = 0; limit = 8; -#if NP9660 - if ( match_len > 3) -#else - if( (match_len) > 4 ) -#endif + + // condition is different if np9660 vs pops + if (this.np9660) + flg = (match_len > 3); + else + flg = (match_len > 4); + + + if (flg) // if (condition) { dist_state += 7; -#if NP9660 - limit = 44; -#else - limit = 16; -#endif + + if (this.np9660) + limit = 44; + else + limit = 16; + } // find total 1s in the match_dist dist_bits = 0; - if(match_dist != 0) { + if(match_dist > 0) { while ((match_dist >> dist_bits) != 1) dist_bits += 1; } @@ -714,9 +776,7 @@ namespace PspCrypto re_bittree(ref bm_dist_bits[len_bits], dist_state, limit, dist_bits); if (dist_bits > 0) - { re_number(ref bm_dist[dist_bits], 0, dist_bits, match_dist); - } in_ptr += match_len; @@ -736,15 +796,16 @@ namespace PspCrypto int match_src; int round = -1; + bool flg = false; len_state = 0; rc_init(out_buf, out_len, in_buf, in_len); - if ((lc & 0x80) != 0) + /*if ((lc & 0x80) != 0) { Buffer.BlockCopy(in_buf, 5, out_buf, 0, (int)code); return (int)code; - } + }*/ rc_state = 0; last_byte = 0; @@ -758,9 +819,7 @@ namespace PspCrypto if (bit == 0) { if (rc_state > 0) - { rc_state -= 1; - } cur_byte = rc_bittree(bm_literal[(((out_ptr & 7) << 8) + last_byte >> lc) & 0x07], 0, 0x100); @@ -789,29 +848,31 @@ namespace PspCrypto { len_state = ((len_bits - 1) << 2) + ((out_ptr << (len_bits - 1)) & 0x03); match_len = rc_number(bm_len[rc_state], len_state, len_bits); - //if ((match_len == 0xFF || match_len == 0xFE) && out_ptr == out_len) - //{ - // return out_ptr; - //} + if (this.np9660 && match_len == 0xFF) + return out_ptr; } dist_state = 0; limit = 8; - if (match_len > 3) + if (this.np9660) + flg = (match_len > 2); + else + flg = (match_len > 3); + + if (flg) { dist_state += 7; - limit = 16; + if (this.np9660) + limit = 44; + else + limit = 16; } dist_bits = rc_bittree(bm_dist_bits[len_bits], dist_state, limit); if (dist_bits > 0) - { match_dist = rc_number(bm_dist[dist_bits], 0, dist_bits); - } else - { match_dist = 1; - } match_src = out_ptr - match_dist; if (match_dist > out_ptr || match_dist < 0) From eb5120a6747420323049d588d86a4086db806538 Mon Sep 17 00:00:00 2001 From: Li Date: Sun, 16 Apr 2023 05:23:05 +1200 Subject: [PATCH 04/31] Fix gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a6c5e43..df81f71 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ .vs/* *.7z -*thumbs.db +*Thumbs.db PbpResign/bin/* PbpResign/obj/* From 4665da300c12f072abd64cd052e108c4af64db5b Mon Sep 17 00:00:00 2001 From: Li Date: Sun, 16 Apr 2023 20:55:40 +1200 Subject: [PATCH 05/31] Update compression to work with both pops and npumdimg. --- PbpResign/Program.cs | 2 +- PopsBuilder/Psp/NpUmdImg.cs | 1 - PspCrypto/Lz.cs | 11 +----- PspCrypto/Lzrc.cs | 75 +++++++++++++++++++++---------------- PspTest.sln | 12 ++++++ 5 files changed, 57 insertions(+), 44 deletions(-) diff --git a/PbpResign/Program.cs b/PbpResign/Program.cs index 8ddad81..b1eebac 100644 --- a/PbpResign/Program.cs +++ b/PbpResign/Program.cs @@ -1075,7 +1075,7 @@ namespace PbpResign Console.WriteLine("VersionKey: " + BitConverter.ToString(NewVersionKey.ToArray())); NpUmdImg npumd = new NpUmdImg(new NpDrmInfo(NewVersionKey.ToArray(), CId, npHdr.NpFlags), - "fft.iso", "ULUS10297", File.ReadAllBytes("TEST\\PARAM.SFO"), false); + "fft.iso", "ULUS10297", File.ReadAllBytes("TEST\\PARAM.SFO"), true); npumd.CreatePsar(); byte[] paramFile = File.ReadAllBytes("TEST\\PARAM.SFO"); diff --git a/PopsBuilder/Psp/NpUmdImg.cs b/PopsBuilder/Psp/NpUmdImg.cs index 8f7c618..b7e3f79 100644 --- a/PopsBuilder/Psp/NpUmdImg.cs +++ b/PopsBuilder/Psp/NpUmdImg.cs @@ -133,7 +133,6 @@ namespace PopsBuilder.Psp { byte[] lzRcBuf = Lz.compress(isoBuf, true); //memset(lzrc_buf + lzrc_size, 0, 16); - // int ratio = (lzRcBuf.Length * 100) / BLOCK_SZ; diff --git a/PspCrypto/Lz.cs b/PspCrypto/Lz.cs index a51cf49..f974881 100644 --- a/PspCrypto/Lz.cs +++ b/PspCrypto/Lz.cs @@ -22,17 +22,8 @@ namespace PspCrypto //return 0; var lzrc = new Lzrc(np9660); - // create a buffer big enough to hold compression result - byte[] compression_result = new byte[in_buf.Length]; - // (this could get resized by the compression code, if its too small) - // compress data, and get the compressed data length - int compressed_length = lzrc.lzrc_compress(ref compression_result, compression_result.Length, in_buf, in_buf.Length); - - // resize array to actual compressed length ... - Array.Resize(ref compression_result, compressed_length); - - return compression_result; + return lzrc.lzrc_compress(in_buf, in_buf.Length); } public static int decompress(byte[] @out, byte[] @in, int size, int insize, bool np9660=false) { diff --git a/PspCrypto/Lzrc.cs b/PspCrypto/Lzrc.cs index 6d0a05a..9f5cfb3 100644 --- a/PspCrypto/Lzrc.cs +++ b/PspCrypto/Lzrc.cs @@ -27,15 +27,16 @@ namespace PspCrypto private byte[][] bm_match; private byte[][] bm_len; - const int max_tbl_sz = 65280; - const int tbl_sz = 65536; + const int MAX_WIN_SZ = 16384; + const int MAX_TBL_SZ = 65280; + const int TBL_SZ = 65536; - static byte[] text_buf = new byte[tbl_sz]; - static int t_start, t_end, t_fill, sp_fill; - static int t_len, t_pos; + private byte[] text_buf = new byte[TBL_SZ]; + private int t_start, t_end, t_fill, sp_fill; + private int t_len, t_pos; - static int[] prev = new int[tbl_sz], next = new int[tbl_sz]; - static int[] root = new int[tbl_sz]; + private int[] prev = new int[TBL_SZ], next = new int[TBL_SZ]; + private int[] root = new int[TBL_SZ]; public Lzrc(bool np9660 = false) { @@ -91,14 +92,14 @@ namespace PspCrypto output[out_ptr++] = b; } - void re_init(ref byte[] out_buf, int out_len, ref byte[] in_buf, int in_len) + void re_init(ref byte[] in_buf, int in_len) { input = in_buf; this.in_len = in_len; in_ptr = 0; - output = out_buf; - this.out_len = out_len; + this.output = new byte[in_len]; + this.out_len = in_len; out_ptr = 0; range = 0xffffffff; @@ -268,7 +269,7 @@ namespace PspCrypto { int i; - for (i = 0; i < tbl_sz; i++) + for (i = 0; i < TBL_SZ; i++) { root[i] = -1; prev[i] = -1; @@ -290,7 +291,7 @@ namespace PspCrypto if (sp_fill == in_len) return; - content_size = (t_fill < t_end) ? (max_tbl_sz + t_fill - t_end) : (t_fill - t_end); + content_size = (t_fill < t_end) ? (MAX_TBL_SZ + t_fill - t_end) : (t_fill - t_end); if (content_size >= 509) return; @@ -308,7 +309,7 @@ namespace PspCrypto } else { - back_size = max_tbl_sz - t_fill; + back_size = MAX_TBL_SZ - t_fill; if (t_start == 0) back_size -= 1; if (sp_fill + back_size > in_len) @@ -331,12 +332,12 @@ namespace PspCrypto sp_fill += front_size; - Array.ConstrainedCopy(text_buf, 255, text_buf, max_tbl_sz, front_size); + Array.ConstrainedCopy(text_buf, 255, text_buf, MAX_TBL_SZ, front_size); //memcpy(text_buf + max_tbl_sz, text_buf, 255); t_fill += front_size; - if (t_fill >= max_tbl_sz) - t_fill -= max_tbl_sz; + if (t_fill >= MAX_TBL_SZ) + t_fill -= MAX_TBL_SZ; } } void remove_node(int p) @@ -370,7 +371,7 @@ namespace PspCrypto //src = text_buf[pos..]; //win = text_buf[t_start..]; - content_size = (t_fill < pos) ? (max_tbl_sz + t_fill - pos) : (t_fill - pos); + content_size = (t_fill < pos) ? (MAX_TBL_SZ + t_fill - pos) : (t_fill - pos); t_len = 1; t_pos = 0; match_len = t_len; @@ -420,7 +421,7 @@ namespace PspCrypto { int mp = pos - p; if (mp < 0) - mp += max_tbl_sz; + mp += MAX_TBL_SZ; if (mp < t_pos) { t_len = i; @@ -454,15 +455,15 @@ namespace PspCrypto int i, win_size; int tmp_len, tmp_pos; - win_size = (t_end >= t_start) ? (t_end - t_start) : (max_tbl_sz + t_end - t_start); + win_size = (t_end >= t_start) ? (t_end - t_start) : (MAX_TBL_SZ + t_end - t_start); for (i = 0; i < length; i++) { - if (win_size == 16384) + if (win_size == MAX_WIN_SZ) { remove_node(t_start); t_start += 1; - if (t_start == max_tbl_sz) + if (t_start == MAX_TBL_SZ) t_start = 0; } else @@ -475,8 +476,8 @@ namespace PspCrypto insert_node(t_end, out tmp_len, out tmp_pos, 0); } t_end += 1; - if (t_end >= max_tbl_sz) - t_end -= max_tbl_sz; + if (t_end >= MAX_TBL_SZ) + t_end -= MAX_TBL_SZ; } } void re_bittree(ref byte[] probs,int index, int limit, int number) @@ -499,7 +500,6 @@ namespace PspCrypto tmp = number >> n; bit = (number >> (n - 1)) & 1; re_bit(ref probs, index + tmp, bit); - n -= 1; } while (n > 0); } @@ -567,7 +567,7 @@ namespace PspCrypto { if (out_ptr == out_len) { - out_len += 0x100; + out_len += 0x1000; Array.Resize(ref output, out_len); } @@ -637,7 +637,14 @@ namespace PspCrypto re_putbyte((byte)((code >> 8) & 0xff)); re_putbyte((byte)((code >> 0) & 0xff)); } - public int lzrc_compress(ref byte[] out_buf, int out_len, byte[] in_buf, int in_len) + + private byte[] re_trunc() + { + Array.Resize(ref output, out_ptr); + return output; + } + + public byte[] lzrc_compress(byte[] in_buf, int in_len) { int match_step, re_state, len_state, dist_state; int i, cur_byte, last_byte; @@ -645,10 +652,8 @@ namespace PspCrypto int match_dist, dist_bits, limit; int round = -1; - len_state = 0; - // initalize buffers to all 0x80 - re_init(ref out_buf, out_len, ref in_buf, in_len); + re_init(ref in_buf, in_len); // initalize the tree init_tree(); @@ -698,11 +703,11 @@ namespace PspCrypto cur_byte = re_getbyte(); re_bittree(ref bm_literal[((last_byte >> lc) & 0x07)], 0, 0x100, cur_byte); - if (in_ptr >= in_len) + if (!this.np9660 && in_ptr >= in_len) { re_normalize(); re_flush(); - return out_ptr; + return re_trunc(); } } else @@ -736,7 +741,7 @@ namespace PspCrypto { re_normalize(); re_flush(); - return out_ptr; + return re_trunc(); } } @@ -756,9 +761,13 @@ namespace PspCrypto dist_state += 7; if (this.np9660) + { limit = 44; + } else + { limit = 16; + } } @@ -766,7 +775,9 @@ namespace PspCrypto dist_bits = 0; if(match_dist > 0) { while ((match_dist >> dist_bits) != 1) + { dist_bits += 1; + } } else { diff --git a/PspTest.sln b/PspTest.sln index 1f27768..de35937 100644 --- a/PspTest.sln +++ b/PspTest.sln @@ -11,6 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsvImage", "PsvImage\PsvIma EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PopsBuilder", "PopsBuilder\PopsBuilder.csproj", "{D1DF66DB-52BE-489C-A292-525BC71F2BB2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnicornManaged", "UnicornManaged\UnicornManaged.csproj", "{AF774802-8809-415F-AE18-437036630FEA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnicornTest", "UnicornTest\UnicornTest.csproj", "{E7B6A043-340B-4BAF-99B6-B7AB903C817E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -33,6 +37,14 @@ Global {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Release|Any CPU.Build.0 = Release|Any CPU + {AF774802-8809-415F-AE18-437036630FEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF774802-8809-415F-AE18-437036630FEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF774802-8809-415F-AE18-437036630FEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF774802-8809-415F-AE18-437036630FEA}.Release|Any CPU.Build.0 = Release|Any CPU + {E7B6A043-340B-4BAF-99B6-B7AB903C817E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7B6A043-340B-4BAF-99B6-B7AB903C817E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7B6A043-340B-4BAF-99B6-B7AB903C817E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7B6A043-340B-4BAF-99B6-B7AB903C817E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 50c91e024cd905effab28ee8e2094945454e9087 Mon Sep 17 00:00:00 2001 From: Li Date: Sun, 16 Apr 2023 21:25:06 +1200 Subject: [PATCH 06/31] Remove unicorntest --- PspTest.sln | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/PspTest.sln b/PspTest.sln index de35937..1f27768 100644 --- a/PspTest.sln +++ b/PspTest.sln @@ -11,10 +11,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsvImage", "PsvImage\PsvIma EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PopsBuilder", "PopsBuilder\PopsBuilder.csproj", "{D1DF66DB-52BE-489C-A292-525BC71F2BB2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnicornManaged", "UnicornManaged\UnicornManaged.csproj", "{AF774802-8809-415F-AE18-437036630FEA}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnicornTest", "UnicornTest\UnicornTest.csproj", "{E7B6A043-340B-4BAF-99B6-B7AB903C817E}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -37,14 +33,6 @@ Global {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Release|Any CPU.Build.0 = Release|Any CPU - {AF774802-8809-415F-AE18-437036630FEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF774802-8809-415F-AE18-437036630FEA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF774802-8809-415F-AE18-437036630FEA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF774802-8809-415F-AE18-437036630FEA}.Release|Any CPU.Build.0 = Release|Any CPU - {E7B6A043-340B-4BAF-99B6-B7AB903C817E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E7B6A043-340B-4BAF-99B6-B7AB903C817E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E7B6A043-340B-4BAF-99B6-B7AB903C817E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E7B6A043-340B-4BAF-99B6-B7AB903C817E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9cb01e4a4b7de9ca0e3d5b09428806c1e4f241f6 Mon Sep 17 00:00:00 2001 From: Li Date: Mon, 17 Apr 2023 01:07:43 +1200 Subject: [PATCH 07/31] Add UMD Reader --- PbpResign/PbpResign.csproj | 2 +- PbpResign/Program.cs | 19 +- PopsBuilder/Atrac3/Atrac3ToolEncoder.cs | 2 +- PopsBuilder/Atrac3/IAtracEncoderBase.cs | 2 +- PopsBuilder/Cue/CueIndex.cs | 2 +- PopsBuilder/Cue/CueReader.cs | 2 +- PopsBuilder/Cue/CueStream.cs | 2 +- PopsBuilder/Cue/CueTrack.cs | 2 +- PopsBuilder/Cue/TrackType.cs | 2 +- ...{PopsBuilder.csproj => GameBuilder.csproj} | 5 + PopsBuilder/MathUtil.cs | 26 ++ PopsBuilder/Pops/DiscCompressor.cs | 8 +- PopsBuilder/Pops/DiscInfo.cs | 2 +- PopsBuilder/Pops/EccRemoverStream.cs | 4 +- PopsBuilder/Pops/PopsImg.cs | 6 +- PopsBuilder/Pops/PsIsoImg.cs | 8 +- PopsBuilder/Pops/PsTitleImg.cs | 6 +- PopsBuilder/Psp/NpDrmInfo.cs | 2 +- PopsBuilder/Psp/NpDrmPsar.cs | 2 +- PopsBuilder/Psp/NpUmdImg.cs | 44 ++-- PopsBuilder/Psp/PbpBuilder.cs | 6 +- PopsBuilder/Psp/Rng.cs | 2 +- PopsBuilder/Psp/Sfo.cs | 230 ++++++++++++++++++ PopsBuilder/Psp/UmdDisc.cs | 77 ++++++ PopsBuilder/Resources.Designer.cs | 4 +- PopsBuilder/StreamUtil.cs | 79 +++++- PopsBuilder/xXEccD3str0yerXx.cs | 4 +- PspTest.sln | 2 +- 28 files changed, 478 insertions(+), 74 deletions(-) rename PopsBuilder/{PopsBuilder.csproj => GameBuilder.csproj} (81%) create mode 100644 PopsBuilder/MathUtil.cs create mode 100644 PopsBuilder/Psp/Sfo.cs create mode 100644 PopsBuilder/Psp/UmdDisc.cs diff --git a/PbpResign/PbpResign.csproj b/PbpResign/PbpResign.csproj index c8fe916..a45a120 100644 --- a/PbpResign/PbpResign.csproj +++ b/PbpResign/PbpResign.csproj @@ -11,7 +11,7 @@ - + diff --git a/PbpResign/Program.cs b/PbpResign/Program.cs index b1eebac..80d71ea 100644 --- a/PbpResign/Program.cs +++ b/PbpResign/Program.cs @@ -1,7 +1,7 @@ using CommunityToolkit.HighPerformance; using Ionic.Zlib; -using PopsBuilder.Pops; -using PopsBuilder.Psp; +using GameBuilder.Pops; +using GameBuilder.Psp; using PspCrypto; using PsvImage; using System; @@ -11,6 +11,7 @@ using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using System.Collections.Generic; namespace PbpResign { @@ -1074,13 +1075,19 @@ namespace PbpResign Console.WriteLine("VersionKey: " + BitConverter.ToString(NewVersionKey.ToArray())); - NpUmdImg npumd = new NpUmdImg(new NpDrmInfo(NewVersionKey.ToArray(), CId, npHdr.NpFlags), - "fft.iso", "ULUS10297", File.ReadAllBytes("TEST\\PARAM.SFO"), true); + UmdDisc disc = new UmdDisc("fft.iso"); + NpUmdImg npumd = new NpUmdImg(new NpDrmInfo(NewVersionKey.ToArray(), CId, npHdr.NpFlags), disc, false); npumd.CreatePsar(); - byte[] paramFile = File.ReadAllBytes("TEST\\PARAM.SFO"); - PbpBuilder.CreatePbp(paramFile, File.ReadAllBytes("TEST\\ICON0.PNG"), null, File.ReadAllBytes("TEST\\PIC0.PNG"), File.ReadAllBytes("TEST\\PIC1.PNG"), null, npumd, "FFT.PBP"); + PbpBuilder.CreatePbp(disc.DataFiles["PARAM.SFO"], + disc.DataFiles["ICON0.PNG"], + disc.DataFiles["ICON1.PMF"], + disc.DataFiles["PIC0.PNG"], + disc.DataFiles["PIC1.PNG"], + disc.DataFiles["SND0.AT3"], + npumd, + "FFT.PBP"); return CopyNpUmdImg(input, output, pbpHdr, psarBuff, npHdr); } diff --git a/PopsBuilder/Atrac3/Atrac3ToolEncoder.cs b/PopsBuilder/Atrac3/Atrac3ToolEncoder.cs index ee8d10d..e29aec7 100644 --- a/PopsBuilder/Atrac3/Atrac3ToolEncoder.cs +++ b/PopsBuilder/Atrac3/Atrac3ToolEncoder.cs @@ -8,7 +8,7 @@ using System.Threading.Channels; using System.Threading.Tasks; using System.Xml.Linq; -namespace PopsBuilder.Atrac3 +namespace GameBuilder.Atrac3 { public class Atrac3ToolEncoder : IAtracEncoderBase { diff --git a/PopsBuilder/Atrac3/IAtracEncoderBase.cs b/PopsBuilder/Atrac3/IAtracEncoderBase.cs index a3fe97d..e34f7bc 100644 --- a/PopsBuilder/Atrac3/IAtracEncoderBase.cs +++ b/PopsBuilder/Atrac3/IAtracEncoderBase.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Atrac3 +namespace GameBuilder.Atrac3 { public interface IAtracEncoderBase { diff --git a/PopsBuilder/Cue/CueIndex.cs b/PopsBuilder/Cue/CueIndex.cs index d8be24b..6a27351 100644 --- a/PopsBuilder/Cue/CueIndex.cs +++ b/PopsBuilder/Cue/CueIndex.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Cue +namespace GameBuilder.Cue { public class CueIndex { diff --git a/PopsBuilder/Cue/CueReader.cs b/PopsBuilder/Cue/CueReader.cs index 9402bd0..9ba60f0 100644 --- a/PopsBuilder/Cue/CueReader.cs +++ b/PopsBuilder/Cue/CueReader.cs @@ -5,7 +5,7 @@ using System.Linq.Expressions; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Cue +namespace GameBuilder.Cue { public class CueReader : IDisposable { diff --git a/PopsBuilder/Cue/CueStream.cs b/PopsBuilder/Cue/CueStream.cs index 407696c..797b451 100644 --- a/PopsBuilder/Cue/CueStream.cs +++ b/PopsBuilder/Cue/CueStream.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Cue +namespace GameBuilder.Cue { public class CueStream : Stream { diff --git a/PopsBuilder/Cue/CueTrack.cs b/PopsBuilder/Cue/CueTrack.cs index df966e7..83848e8 100644 --- a/PopsBuilder/Cue/CueTrack.cs +++ b/PopsBuilder/Cue/CueTrack.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Cue +namespace GameBuilder.Cue { public class CueTrack { diff --git a/PopsBuilder/Cue/TrackType.cs b/PopsBuilder/Cue/TrackType.cs index 582478c..064b89f 100644 --- a/PopsBuilder/Cue/TrackType.cs +++ b/PopsBuilder/Cue/TrackType.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Cue +namespace GameBuilder.Cue { public enum TrackType { diff --git a/PopsBuilder/PopsBuilder.csproj b/PopsBuilder/GameBuilder.csproj similarity index 81% rename from PopsBuilder/PopsBuilder.csproj rename to PopsBuilder/GameBuilder.csproj index 6e883ae..34888e1 100644 --- a/PopsBuilder/PopsBuilder.csproj +++ b/PopsBuilder/GameBuilder.csproj @@ -6,6 +6,11 @@ enable + + + + + diff --git a/PopsBuilder/MathUtil.cs b/PopsBuilder/MathUtil.cs new file mode 100644 index 0000000..5354d7e --- /dev/null +++ b/PopsBuilder/MathUtil.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GameBuilder +{ + public static class MathUtil + { + public static int CalculateDifference(int val1, int val2) + { + int smaller = Convert.ToInt32(Math.Min(val1, val2)); + int larger = Convert.ToInt32(Math.Max(val1, val2)); + + return larger - smaller; + } + public static int CalculatePaddingAmount(int total, int alignTo) + { + int remainder = total % alignTo; + int padAmt = alignTo - (remainder); + if ((remainder) == 0) return 0; + return padAmt; + } + } +} diff --git a/PopsBuilder/Pops/DiscCompressor.cs b/PopsBuilder/Pops/DiscCompressor.cs index 486124b..3ddab68 100644 --- a/PopsBuilder/Pops/DiscCompressor.cs +++ b/PopsBuilder/Pops/DiscCompressor.cs @@ -1,6 +1,6 @@ -using PopsBuilder.Atrac3; -using PopsBuilder.Cue; -using PopsBuilder.Psp; +using GameBuilder.Atrac3; +using GameBuilder.Cue; +using GameBuilder.Psp; using PspCrypto; using System; using System.Collections.Generic; @@ -9,7 +9,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Pops +namespace GameBuilder.Pops { public class DiscCompressor { diff --git a/PopsBuilder/Pops/DiscInfo.cs b/PopsBuilder/Pops/DiscInfo.cs index 13a0baf..229709a 100644 --- a/PopsBuilder/Pops/DiscInfo.cs +++ b/PopsBuilder/Pops/DiscInfo.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Pops +namespace GameBuilder.Pops { public class DiscInfo { diff --git a/PopsBuilder/Pops/EccRemoverStream.cs b/PopsBuilder/Pops/EccRemoverStream.cs index f1a6d6f..bde762d 100644 --- a/PopsBuilder/Pops/EccRemoverStream.cs +++ b/PopsBuilder/Pops/EccRemoverStream.cs @@ -1,11 +1,11 @@ -using PopsBuilder.Cue; +using GameBuilder.Cue; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Pops +namespace GameBuilder.Pops { public class EccRemoverStream : Stream { diff --git a/PopsBuilder/Pops/PopsImg.cs b/PopsBuilder/Pops/PopsImg.cs index 77a914b..872a014 100644 --- a/PopsBuilder/Pops/PopsImg.cs +++ b/PopsBuilder/Pops/PopsImg.cs @@ -1,5 +1,5 @@ -using Org.BouncyCastle.Crypto.Paddings; -using PopsBuilder.Psp; +using GameBuilder; +using GameBuilder.Psp; using PspCrypto; using System; using System.Collections.Generic; @@ -8,7 +8,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Pops +namespace GameBuilder.Pops { public class PopsImg : NpDrmPsar { diff --git a/PopsBuilder/Pops/PsIsoImg.cs b/PopsBuilder/Pops/PsIsoImg.cs index 16d1a7f..b14ce0d 100644 --- a/PopsBuilder/Pops/PsIsoImg.cs +++ b/PopsBuilder/Pops/PsIsoImg.cs @@ -1,14 +1,14 @@ using Org.BouncyCastle.Crypto.Paddings; -using PopsBuilder.Atrac3; -using PopsBuilder.Cue; -using PopsBuilder.Psp; +using GameBuilder.Atrac3; +using GameBuilder.Cue; +using GameBuilder.Psp; using PspCrypto; using System; using System.Net; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; -namespace PopsBuilder.Pops +namespace GameBuilder.Pops { public class PsIsoImg : PopsImg { diff --git a/PopsBuilder/Pops/PsTitleImg.cs b/PopsBuilder/Pops/PsTitleImg.cs index 89c2a36..255c056 100644 --- a/PopsBuilder/Pops/PsTitleImg.cs +++ b/PopsBuilder/Pops/PsTitleImg.cs @@ -1,5 +1,5 @@ -using PopsBuilder.Atrac3; -using PopsBuilder.Psp; +using GameBuilder.Atrac3; +using GameBuilder.Psp; using PspCrypto; using System; using System.Collections.Generic; @@ -8,7 +8,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Pops +namespace GameBuilder.Pops { public class PsTitleImg : PopsImg { diff --git a/PopsBuilder/Psp/NpDrmInfo.cs b/PopsBuilder/Psp/NpDrmInfo.cs index d740f0f..655464d 100644 --- a/PopsBuilder/Psp/NpDrmInfo.cs +++ b/PopsBuilder/Psp/NpDrmInfo.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Psp +namespace GameBuilder.Psp { public class NpDrmInfo { diff --git a/PopsBuilder/Psp/NpDrmPsar.cs b/PopsBuilder/Psp/NpDrmPsar.cs index 7cef126..3c3ede6 100644 --- a/PopsBuilder/Psp/NpDrmPsar.cs +++ b/PopsBuilder/Psp/NpDrmPsar.cs @@ -7,7 +7,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Psp +namespace GameBuilder.Psp { public abstract class NpDrmPsar : IDisposable { diff --git a/PopsBuilder/Psp/NpUmdImg.cs b/PopsBuilder/Psp/NpUmdImg.cs index b7e3f79..343660e 100644 --- a/PopsBuilder/Psp/NpUmdImg.cs +++ b/PopsBuilder/Psp/NpUmdImg.cs @@ -1,6 +1,6 @@ using Org.BouncyCastle.Crypto.Paddings; using Org.BouncyCastle.Ocsp; -using PopsBuilder.Pops; +using GameBuilder.Pops; using PspCrypto; using System; using System.Collections.Generic; @@ -10,7 +10,7 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Psp +namespace GameBuilder.Psp { public class NpUmdImg : NpDrmPsar { @@ -18,9 +18,8 @@ namespace PopsBuilder.Psp const int BLOCK_BASIS = 0x10; const int SECTOR_SZ = 2048; const int BLOCK_SZ = BLOCK_BASIS * SECTOR_SZ; - public NpUmdImg(NpDrmInfo drmInfo, string iso, string discId, byte[] paramSfo, bool compress) : base(drmInfo) + public NpUmdImg(NpDrmInfo drmInfo, UmdDisc umdImage, bool compress) : base(drmInfo) { - this.isoStream = File.OpenRead(iso); this.compress = compress; this.npHdr = new MemoryStream(); @@ -37,13 +36,18 @@ namespace PopsBuilder.Psp this.headerKey = Rng.RandomBytes(0x10); - this.discId = discId.ToUpperInvariant().Replace("-", "").Replace("_", ""); - this.paramSfo = paramSfo; - - isoBlocks = Convert.ToInt64((isoStream.Length + BLOCK_SZ - 1) / BLOCK_SZ); + this.umdImage = umdImage; + isoBlocks = Convert.ToInt64((umdImage.IsoStream.Length + BLOCK_SZ - 1) / BLOCK_SZ); } + private void patchSfo() + { + Sfo sfoKeys = Sfo.ReadSfo(umdImage.DataFiles["PARAM.SFO"]); + sfoKeys["DISC_ID"] = DrmInfo.ContentId.Substring(7, 9); + umdImage.DataFiles["PARAM.SFO"] = sfoKeys.WriteSfo(); + + } private void createNpHdr() { npHdrUtil.WriteStr("NPUMDIMG"); @@ -69,6 +73,8 @@ namespace PopsBuilder.Psp public void CreatePsar() { + patchSfo(); + createNpUmdTbl(); byte[] tbl = encryptTable(); this.dataKey = hashBlock(tbl); @@ -102,7 +108,7 @@ namespace PopsBuilder.Psp using (MemoryStream dataPsp = new MemoryStream()) { StreamUtil dataPspUtil = new StreamUtil(dataPsp); - byte[] signature = signParamSfo(paramSfo); + byte[] signature = signParamSfo(umdImage.DataFiles["PARAM.SFO"]); dataPspUtil.WriteBytes(signature); dataPspUtil.WritePadding(0x00, 0x530); dataPspUtil.WriteStrWithPadding(DrmInfo.ContentId, 0x00, 0x30); @@ -118,14 +124,14 @@ namespace PopsBuilder.Psp private void createNpUmdTbl() { Int64 tableSz = isoBlocks * 0x20; - Int64 isoSz = this.isoStream.Length; + Int64 isoSz = umdImage.IsoStream.Length; int wsize = 0; Int64 isoOffset = 0x100 + tableSz; for (int i = 0; i < isoBlocks; i++) { byte[] isoBuf = new byte[BLOCK_SZ]; - wsize = isoStream.Read(isoBuf, 0x00, BLOCK_SZ); + wsize = umdImage.IsoStream.Read(isoBuf, 0x00, BLOCK_SZ); byte[] wbuf = isoBuf; @@ -140,13 +146,14 @@ namespace PopsBuilder.Psp { wbuf = lzRcBuf; - wsize = (lzRcBuf.Length + 15) & ~15; + wsize = lzRcBuf.Length; + wsize += MathUtil.CalculatePaddingAmount(wsize, 16); Array.Resize(ref lzRcBuf, wsize); } } int unpaddedSz = wsize; - wsize = (wsize + 15) & ~15; + wsize += MathUtil.CalculatePaddingAmount(wsize, 16); Array.Resize(ref wbuf, wsize); encryptBlock(wbuf, Convert.ToInt32(isoOffset)); byte[] hash = hashBlock(wbuf); @@ -162,7 +169,7 @@ namespace PopsBuilder.Psp isoOffset += wsize; - Console.Write(Convert.ToInt32(Math.Floor((Convert.ToDouble(isoStream.Position) / Convert.ToDouble(isoStream.Length)) * 100.0)) + "%\r"); + Console.Write(Convert.ToInt32(Math.Floor((Convert.ToDouble(umdImage.IsoStream.Position) / Convert.ToDouble(umdImage.IsoStream.Length)) * 100.0)) + "%\r"); } } @@ -215,7 +222,7 @@ namespace PopsBuilder.Psp { npHdrBodyUtil.WriteUInt16(SECTOR_SZ); // sector_sz - if (isoStream.Length > 0x40000000) + if (umdImage.IsoStream.Length > 0x40000000) npHdrBodyUtil.WriteUInt16(0xE001); // unk_2 else npHdrBodyUtil.WriteUInt16(0xE000); //unk_2 @@ -236,7 +243,7 @@ namespace PopsBuilder.Psp npHdrBodyUtil.WriteUInt32(0x01003FFE); // unk_40 npHdrBodyUtil.WriteUInt32(0x100); // block_entry_offset - npHdrBodyUtil.WriteStrWithPadding(this.discId.Substring(0, 4) + "-" + this.discId.Substring(4, 5), 0x00, 0x10); + npHdrBodyUtil.WriteStrWithPadding(umdImage.DiscIdSeperated, 0x00, 0x10); npHdrBodyUtil.WriteInt32(0); // header_start_offset npHdrBodyUtil.WriteInt32(0); // unk_68 @@ -266,14 +273,11 @@ namespace PopsBuilder.Psp private Int64 isoBlocks; private bool compress; - private string discId; - - private byte[] paramSfo; + UmdDisc umdImage; private byte[] headerKey; private byte[] dataKey; - private FileStream isoStream; private MemoryStream npHdr; private StreamUtil npHdrUtil; diff --git a/PopsBuilder/Psp/PbpBuilder.cs b/PopsBuilder/Psp/PbpBuilder.cs index 2460f67..d13e501 100644 --- a/PopsBuilder/Psp/PbpBuilder.cs +++ b/PopsBuilder/Psp/PbpBuilder.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Psp +namespace GameBuilder.Psp { public static class PbpBuilder { @@ -16,7 +16,9 @@ namespace PopsBuilder.Psp using (FileStream pbpStream = File.Open(outputFile, FileMode.Create)) { byte[] dataPsp = dataPsar.GenerateDataPsp(); - int padLen = Convert.ToInt32(0x100 - (dataPsp.Length % 0x100)); + + int padLen = MathUtil.CalculatePaddingAmount(dataPsp.Length, 0x100); + Array.Resize(ref dataPsp, dataPsp.Length + padLen); StreamUtil pbpUtil = new StreamUtil(pbpStream); diff --git a/PopsBuilder/Psp/Rng.cs b/PopsBuilder/Psp/Rng.cs index c7cbace..4824236 100644 --- a/PopsBuilder/Psp/Rng.cs +++ b/PopsBuilder/Psp/Rng.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder.Psp +namespace GameBuilder.Psp { public static class Rng { diff --git a/PopsBuilder/Psp/Sfo.cs b/PopsBuilder/Psp/Sfo.cs new file mode 100644 index 0000000..e99e853 --- /dev/null +++ b/PopsBuilder/Psp/Sfo.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.IO; +using System.Runtime.CompilerServices; + +// A Sfo Parser Written by SilicaAndPina +// Because all the others are overly-complicated for no reason! +// MIT Licensed. + +namespace GameBuilder.Psp +{ + + public class Sfo + { + + private struct SfoEntry + { + internal string keyName; + internal byte type; + internal UInt32 valueSize; + internal UInt32 totalSize; + internal byte align; + internal object value; + } + + const int SFO_MAGIC = 0x46535000; + const byte PSF_TYPE_BIN = 0; + const byte PSF_TYPE_STR = 2; + const byte PSF_TYPE_VAL = 4; + + private Dictionary sfoEntries; + public Object this[string index] + { + get + { + return sfoEntries[index].value; + } + set + { + SfoEntry sfoEnt = sfoEntries[index]; + sfoEnt.value = value; + + // update sz + sfoEnt.valueSize = getObjectSz(sfoEnt.value); + + if (sfoEnt.valueSize > sfoEnt.totalSize) + sfoEnt.totalSize = Convert.ToUInt32(MathUtil.CalculatePaddingAmount(Convert.ToInt32(sfoEnt.valueSize), sfoEnt.align)); + + // update type + sfoEnt.type = getPsfType(sfoEnt.value); + + sfoEntries[index] = sfoEnt; + } + } + public Sfo() + { + sfoEntries = new Dictionary(); + } + + + private static UInt32 getObjectSz(Object obj) + { + if (obj is Int32) return 4; + if (obj is UInt32) return 4; + if (obj is String) return Convert.ToUInt32((obj as String).Length+1); + if (obj is Byte[]) return Convert.ToUInt32((obj as Byte[]).Length); + throw new Exception("Object is of unsupported type: " + obj.GetType()); + } + + private static byte getPsfType(Object obj) + { + if (obj is Int32 || obj is UInt32) return PSF_TYPE_VAL; + if (obj is String) return PSF_TYPE_STR; + if (obj is Byte[]) return PSF_TYPE_BIN; + throw new Exception("Object is of unsupported type: " + obj.GetType()); + } + + public byte[] WriteSfo(UInt32 version = 0x101, Byte align = 0x4) + { + using (MemoryStream sfoStream = new MemoryStream()) + { + WriteSfo(sfoStream, version, align); + byte[] sfoBytes = sfoStream.ToArray(); + return sfoBytes; + } + } + public void WriteSfo(Stream SfoStream, UInt32 version=0x101, Byte align=0x4) + { + using(MemoryStream sfoStream = new MemoryStream()) + { + StreamUtil sfoUtil = new StreamUtil(sfoStream); + + sfoUtil.WriteUInt32(SFO_MAGIC); + sfoUtil.WriteUInt32(version); + + sfoUtil.WriteUInt32(0xFFFFFFFF); // key offset + sfoUtil.WriteUInt32(0xFFFFFFFF); // value offset + // (will fill these in after the file is created) + + sfoUtil.WriteInt32(sfoEntries.Count); + + using (MemoryStream keyTable = new MemoryStream()) + { + StreamUtil keyUtils = new StreamUtil(keyTable); + using (MemoryStream valueTable = new MemoryStream()) + { + StreamUtil valueUtils = new StreamUtil(valueTable); + foreach (SfoEntry entry in sfoEntries.Values) + { + // write name + sfoUtil.WriteUInt16(Convert.ToUInt16(keyTable.Position)); + keyUtils.WriteCStr(entry.keyName); + + + + // write entry + + sfoUtil.WriteByte(align); // align + sfoUtil.WriteByte(entry.type); // type + sfoUtil.WriteUInt32(entry.valueSize); // valueSize + sfoUtil.WriteUInt32(entry.totalSize); // totalSize + + // write data + sfoUtil.WriteUInt32(Convert.ToUInt32(valueTable.Position)); // dataOffset + + switch (entry.type) + { + case PSF_TYPE_VAL: + valueUtils.WriteUInt32(Convert.ToUInt32(entry.value)); + valueUtils.WritePadding(0x00, Convert.ToInt32(entry.totalSize - entry.valueSize)); + break; + case PSF_TYPE_STR: + valueUtils.WriteStrWithPadding(entry.value as String, 0x00, Convert.ToInt32(entry.totalSize)); + break; + case PSF_TYPE_BIN: + valueUtils.WriteBytesWithPadding(entry.value as Byte[], 0x00, Convert.ToInt32(entry.totalSize)); + break; + } + } + + + keyUtils.AlignTo(0x00, align); + UInt32 keyOffset = Convert.ToUInt32(sfoStream.Position); + keyTable.Seek(0x00, SeekOrigin.Begin); + keyTable.CopyTo(sfoStream); + + UInt32 valueOffset = Convert.ToUInt32(sfoStream.Position); + valueTable.Seek(0x00, SeekOrigin.Begin); + valueTable.CopyTo(sfoStream); + + sfoStream.Seek(0x8, SeekOrigin.Begin); + sfoUtil.WriteUInt32(keyOffset); // key offset + sfoUtil.WriteUInt32(valueOffset); // value offset + } + } + + sfoStream.Seek(0x0, SeekOrigin.Begin); + sfoStream.CopyTo(SfoStream); + } + + } + + public static Sfo ReadSfo(Stream SfoStream) + { + Sfo sfoFile = new Sfo(); + StreamUtil DataUtils = new StreamUtil(SfoStream); + + // Read Sfo Header + UInt32 magic = DataUtils.ReadUInt32(); + UInt32 version = DataUtils.ReadUInt32(); + UInt32 keyOffset = DataUtils.ReadUInt32(); + UInt32 valueOffset = DataUtils.ReadUInt32(); + UInt32 count = DataUtils.ReadUInt32(); + + if (magic == SFO_MAGIC) //\x00PSF + { + for(int i = 0; i < count; i++) + { + SfoEntry entry = new SfoEntry(); + + UInt16 nameOffset = DataUtils.ReadUInt16(); + entry.align = DataUtils.ReadByte(); + entry.type = DataUtils.ReadByte(); + entry.valueSize = DataUtils.ReadUInt32(); + entry.totalSize = DataUtils.ReadUInt32(); + UInt32 dataOffset = DataUtils.ReadUInt32(); + + int keyLocation = Convert.ToInt32(keyOffset + nameOffset); + entry.keyName = DataUtils.ReadStringAt(keyLocation); + int valueLocation = Convert.ToInt32(valueOffset + dataOffset); + + + switch (entry.type) + { + case PSF_TYPE_STR: + entry.value = DataUtils.ReadStringAt(valueLocation); + break; + + case PSF_TYPE_VAL: + entry.value = DataUtils.ReadUint32At(valueLocation); + break; + + case PSF_TYPE_BIN: + entry.value = DataUtils.ReadBytesAt(valueLocation, Convert.ToInt32(entry.valueSize)); + break; + } + + + sfoFile.sfoEntries[entry.keyName] = entry; + } + + } + else + { + throw new InvalidDataException("Sfo Magic is Invalid."); + } + + return sfoFile; + } + + public static Sfo ReadSfo(byte[] Sfo) + { + using (MemoryStream SfoStream = new MemoryStream(Sfo)) + { + return ReadSfo(SfoStream); + } + } + } +} diff --git a/PopsBuilder/Psp/UmdDisc.cs b/PopsBuilder/Psp/UmdDisc.cs new file mode 100644 index 0000000..d5b1c0b --- /dev/null +++ b/PopsBuilder/Psp/UmdDisc.cs @@ -0,0 +1,77 @@ +using DiscUtils.Iso9660; +using DiscUtils.Streams; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GameBuilder.Psp +{ + public class UmdDisc : IDisposable + { + private string[] filesList = new string[] + { + "PSP_GAME\\ICON0.PNG", + "PSP_GAME\\ICON1.PMF", + "PSP_GAME\\PARAM.SFO", + "PSP_GAME\\PIC0.PNG", + "PSP_GAME\\PIC1.PNG", + "PSP_GAME\\SND0.AT3" + }; + + public Dictionary DataFiles = new Dictionary(); + public UmdDisc(string isoFile) + { + this.IsoFile = isoFile; + this.IsoStream = File.OpenRead(isoFile); + using (CDReader cdReader = new CDReader(this.IsoStream, true, true)) + { + foreach (string file in filesList) + { + string fname = Path.GetFileName(file).ToUpperInvariant(); + if (cdReader.FileExists(file)) + { + using (SparseStream s = cdReader.OpenFile(file, FileMode.Open)) + { + byte[] data = new byte[s.Length]; + + s.Read(data, 0x00, data.Length); + + DataFiles[fname] = data; + } + } + else + { + DataFiles[fname] = null; + } + } + } + + + if (DataFiles["PARAM.SFO"] is null) throw new Exception("ISO contains no PARAM.SFO file, so this is not a valid PSP game."); + + Sfo sfo = Sfo.ReadSfo(DataFiles["PARAM.SFO"]); + this.DiscId = sfo["DISC_ID"] as String; + + IsoStream.Seek(0x00, SeekOrigin.Begin); + } + + + public string IsoFile; + public FileStream IsoStream; + public string DiscId; + public string DiscIdSeperated + { + get + { + return this.DiscId.Substring(0, 4) + "-" + this.DiscId.Substring(4, 5); + } + } + + public void Dispose() + { + IsoStream.Dispose(); + } + } +} diff --git a/PopsBuilder/Resources.Designer.cs b/PopsBuilder/Resources.Designer.cs index 1b359db..9f5d581 100644 --- a/PopsBuilder/Resources.Designer.cs +++ b/PopsBuilder/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace PopsBuilder { +namespace GameBuilder { using System; @@ -39,7 +39,7 @@ namespace PopsBuilder { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PopsBuilder.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("GameBuilder.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; diff --git a/PopsBuilder/StreamUtil.cs b/PopsBuilder/StreamUtil.cs index 3819312..2d851f6 100644 --- a/PopsBuilder/StreamUtil.cs +++ b/PopsBuilder/StreamUtil.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder +namespace GameBuilder { public class StreamUtil { @@ -14,21 +14,50 @@ namespace PopsBuilder { this.s = s; } - - public void WriteStrWithPadding(string str, byte b, int len) + public string ReadString() { - byte[] sdata = Encoding.UTF8.GetBytes(str); - if (len < sdata.Length) + using (MemoryStream ms = new MemoryStream()) { - s.Write(sdata, 0, len); - return; - } - else - { - WriteBytes(sdata); - WritePadding(b, (len - sdata.Length)); + while (true) + { + byte c = (byte)s.ReadByte(); + if (c == 0) + break; + ms.WriteByte(c); + } + return Encoding.UTF8.GetString(ms.ToArray()); } } + public UInt32 ReadUint32At(int location) + { + long oldPos = s.Position; + s.Seek(location, SeekOrigin.Begin); + UInt32 outp = ReadUInt32(); + s.Seek(oldPos, SeekOrigin.Begin); + return outp; + } + + public byte[] ReadBytesAt(int location, int length) + { + long oldPos = s.Position; + s.Seek(location, SeekOrigin.Begin); + byte[] work_buf = ReadBytes(length); + s.Seek(oldPos, SeekOrigin.Begin); + return work_buf; + } + + public string ReadStringAt(int location) + { + long oldPos = s.Position; + s.Seek(location, SeekOrigin.Begin); + string outp = ReadString(); + s.Seek(oldPos, SeekOrigin.Begin); + return outp; + } + public byte ReadByte() + { + return (byte)s.ReadByte(); + } public byte[] ReadBytes(int len) { byte[] data = new byte[len]; @@ -55,6 +84,23 @@ namespace PopsBuilder byte[] vbytes = ReadBytes(0x4); return BitConverter.ToInt32(vbytes); } + public void WriteBytesWithPadding(byte[] data, byte b, int len) + { + if (len < data.Length) + { + s.Write(data, 0, len); + return; + } + else + { + WriteBytes(data); + WritePadding(b, (len - data.Length)); + } + } + public void WriteStrWithPadding(string str, byte b, int len) + { + WriteBytesWithPadding(Encoding.UTF8.GetBytes(str), b, len); + } public void WriteInt64(Int64 v) { WriteBytes(BitConverter.GetBytes(v)); @@ -80,6 +126,11 @@ namespace PopsBuilder { WriteBytes(BitConverter.GetBytes(v)); } + public void WriteCStr(string str) + { + WriteStr(str); + WriteByte(0x00); + } public void WriteStr(string str) { WriteBytes(Encoding.UTF8.GetBytes(str)); @@ -87,6 +138,7 @@ namespace PopsBuilder public void WritePadding(byte b, int len) { + if (len < 0) return; for(int i = 0; i < len; i++) { WriteByte(b); @@ -95,7 +147,8 @@ namespace PopsBuilder public void AlignTo(byte padByte, int align) { - int padAmt = Convert.ToInt32(align - (s.Position % align)); + int padAmt = MathUtil.CalculatePaddingAmount(Convert.ToInt32(s.Position), align); + this.WritePadding(padByte, padAmt); } public void PadUntil(byte b, int len) diff --git a/PopsBuilder/xXEccD3str0yerXx.cs b/PopsBuilder/xXEccD3str0yerXx.cs index db65b96..d0914a8 100644 --- a/PopsBuilder/xXEccD3str0yerXx.cs +++ b/PopsBuilder/xXEccD3str0yerXx.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace PopsBuilder +namespace GameBuilder { public class xXEccD3str0yerXx { @@ -42,7 +42,7 @@ namespace PopsBuilder } // extend ISO image to compress block sz - int padLen = COMPRESS_BLOCK_SZ - (Convert.ToInt32(noEccIso.Length) % COMPRESS_BLOCK_SZ); + int padLen = MathUtil.CalculatePaddingAmount(Convert.ToInt32(noEccIso.Length), COMPRESS_BLOCK_SZ); byte[] padding = new byte[padLen]; noEccIso.Write(padding, 0x00, padding.Length); diff --git a/PspTest.sln b/PspTest.sln index 1f27768..560a122 100644 --- a/PspTest.sln +++ b/PspTest.sln @@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PbpResign", "PbpResign\PbpR EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsvImage", "PsvImage\PsvImage.csproj", "{0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PopsBuilder", "PopsBuilder\PopsBuilder.csproj", "{D1DF66DB-52BE-489C-A292-525BC71F2BB2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameBuilder", "PopsBuilder\GameBuilder.csproj", "{D1DF66DB-52BE-489C-A292-525BC71F2BB2}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From f5e9d5eaee33f05f93430bbdde278573a12a5eef Mon Sep 17 00:00:00 2001 From: Li Date: Mon, 17 Apr 2023 07:48:32 +1200 Subject: [PATCH 08/31] add chovysign-cli --- .gitignore | 7 +- ChovySign-CLI/ChovySign-CLI.csproj | 30 + ChovySign-CLI/Program.cs | 272 ++++++ ChovySign-CLI/Resources.Designer.cs | 73 ++ ChovySign-CLI/Resources.resx | 124 +++ ChovySign-CLI/Resources/PIC1.PNG | Bin 0 -> 2651 bytes DiscUtils/Core/ApplePartitionMap/BlockZero.cs | 61 ++ .../Core/ApplePartitionMap/PartitionMap.cs | 153 ++++ .../ApplePartitionMap/PartitionMapEntry.cs | 112 +++ .../ApplePartitionMap/PartitionMapFactory.cs | 66 ++ DiscUtils/Core/Archives/FileRecord.cs | 38 + DiscUtils/Core/Archives/TarFile.cs | 144 ++++ DiscUtils/Core/Archives/TarFileBuilder.cs | 122 +++ DiscUtils/Core/Archives/TarHeader.cs | 102 +++ DiscUtils/Core/Archives/TarHeaderExtent.cs | 68 ++ .../Core/Archives/UnixBuildFileRecord.cs | 75 ++ DiscUtils/Core/ChsAddress.cs | 99 +++ DiscUtils/Core/ClusterMap.cs | 75 ++ DiscUtils/Core/ClusterRoles.cs | 50 ++ DiscUtils/Core/Compression/Adler32.cs | 93 ++ .../Core/Compression/BZip2BlockDecoder.cs | 141 +++ .../Compression/BZip2CombinedHuffmanTrees.cs | 134 +++ .../Core/Compression/BZip2DecoderStream.cs | 348 ++++++++ DiscUtils/Core/Compression/BZip2Randomizer.cs | 124 +++ DiscUtils/Core/Compression/BZip2RleStream.cs | 157 ++++ .../Core/Compression/BigEndianBitStream.cs | 94 ++ DiscUtils/Core/Compression/BitStream.cs | 60 ++ DiscUtils/Core/Compression/BlockCompressor.cs | 64 ++ .../Core/Compression/CompressionResult.cs | 50 ++ .../Core/Compression/DataBlockTransform.cs | 70 ++ DiscUtils/Core/Compression/HuffmanTree.cs | 102 +++ .../Core/Compression/InverseBurrowsWheeler.cs | 97 +++ DiscUtils/Core/Compression/MoveToFront.cs | 68 ++ .../Core/Compression/SizedDeflateStream.cs | 64 ++ DiscUtils/Core/Compression/ZlibBuffer.cs | 86 ++ DiscUtils/Core/Compression/ZlibStream.cs | 240 ++++++ DiscUtils/Core/CoreCompat/EncodingHelper.cs | 23 + DiscUtils/Core/CoreCompat/ReflectionHelper.cs | 91 ++ DiscUtils/Core/CoreCompat/StringExtensions.cs | 15 + DiscUtils/Core/DiscDirectoryInfo.cs | 190 +++++ DiscUtils/Core/DiscFileInfo.cs | 200 +++++ DiscUtils/Core/DiscFileLocator.cs | 93 ++ DiscUtils/Core/DiscFileSystem.cs | 509 +++++++++++ DiscUtils/Core/DiscFileSystemChecker.cs | 43 + DiscUtils/Core/DiscFileSystemInfo.cs | 219 +++++ DiscUtils/Core/DiscFileSystemOptions.cs | 41 + DiscUtils/Core/DiscUtils.Core.csproj | 15 + DiscUtils/Core/DiskImageBuilder.cs | 126 +++ DiscUtils/Core/DiskImageFileSpecification.cs | 54 ++ DiscUtils/Core/FileLocator.cs | 72 ++ DiscUtils/Core/FileSystemInfo.cs | 90 ++ DiscUtils/Core/FileSystemManager.cs | 121 +++ DiscUtils/Core/FileSystemParameters.cs | 49 ++ DiscUtils/Core/FileTransport.cs | 73 ++ DiscUtils/Core/FloppyDiskType.cs | 45 + DiscUtils/Core/GenericDiskAdapterType.cs | 40 + DiscUtils/Core/Geometry.cs | 477 +++++++++++ DiscUtils/Core/GeometryCalculation.cs | 9 + DiscUtils/Core/GeometryTranslation.cs | 50 ++ DiscUtils/Core/IClusterBasedFileSystem.cs | 81 ++ DiscUtils/Core/IDiagnosticTraceable.cs | 39 + DiscUtils/Core/IFileSystem.cs | 367 ++++++++ DiscUtils/Core/IUnixFileSystem.cs | 38 + DiscUtils/Core/IWindowsFileSystem.cs | 129 +++ DiscUtils/Core/Internal/Crc32.cs | 43 + DiscUtils/Core/Internal/Crc32Algorithm.cs | 47 + DiscUtils/Core/Internal/Crc32BigEndian.cs | 94 ++ DiscUtils/Core/Internal/Crc32LittleEndian.cs | 98 +++ DiscUtils/Core/Internal/LocalFileLocator.cs | 108 +++ .../Core/Internal/LogicalVolumeFactory.cs | 33 + .../Internal/LogicalVolumeFactoryAttribute.cs | 29 + DiscUtils/Core/Internal/ObjectCache.cs | 149 ++++ DiscUtils/Core/Internal/Utilities.cs | 467 ++++++++++ DiscUtils/Core/Internal/VirtualDiskFactory.cs | 56 ++ .../Internal/VirtualDiskFactoryAttribute.cs | 40 + .../Core/Internal/VirtualDiskTransport.cs | 50 ++ .../Internal/VirtualDiskTransportAttribute.cs | 37 + DiscUtils/Core/InvalidFileSystemException.cs | 72 ++ .../LogicalDiskManager/ComponentRecord.cs | 64 ++ DiscUtils/Core/LogicalDiskManager/Database.cs | 178 ++++ .../Core/LogicalDiskManager/DatabaseHeader.cs | 94 ++ .../Core/LogicalDiskManager/DatabaseRecord.cs | 154 ++++ .../LogicalDiskManager/DiskGroupRecord.cs | 49 ++ .../Core/LogicalDiskManager/DiskRecord.cs | 47 + .../Core/LogicalDiskManager/DynamicDisk.cs | 144 ++++ .../LogicalDiskManager/DynamicDiskGroup.cs | 322 +++++++ .../LogicalDiskManager/DynamicDiskManager.cs | 153 ++++ .../DynamicDiskManagerFactory.cs | 54 ++ .../Core/LogicalDiskManager/DynamicVolume.cs | 70 ++ .../LogicalDiskManager/ExtentMergeType.cs | 31 + .../Core/LogicalDiskManager/ExtentRecord.cs | 60 ++ .../Core/LogicalDiskManager/PrivateHeader.cs | 90 ++ .../Core/LogicalDiskManager/RecordType.cs | 34 + DiscUtils/Core/LogicalDiskManager/TocBlock.cs | 72 ++ .../Core/LogicalDiskManager/VolumeRecord.cs | 78 ++ DiscUtils/Core/LogicalVolumeInfo.cs | 120 +++ DiscUtils/Core/LogicalVolumeStatus.cs | 23 + DiscUtils/Core/NativeFileSystem.cs | 804 ++++++++++++++++++ .../Partitions/BiosExtendedPartitionTable.cs | 119 +++ .../Core/Partitions/BiosPartitionInfo.cs | 136 +++ .../Core/Partitions/BiosPartitionRecord.cs | 107 +++ .../Core/Partitions/BiosPartitionTable.cs | 734 ++++++++++++++++ .../Core/Partitions/BiosPartitionTypes.cs | 197 +++++ .../Partitions/BiosPartitionedDiskBuilder.cs | 166 ++++ .../DefaultPartitionTableFactory.cs | 49 ++ DiscUtils/Core/Partitions/GptEntry.cs | 146 ++++ DiscUtils/Core/Partitions/GptHeader.cs | 146 ++++ .../Core/Partitions/GuidPartitionInfo.cs | 120 +++ .../Core/Partitions/GuidPartitionTable.cs | 657 ++++++++++++++ .../Core/Partitions/GuidPartitionTypes.cs | 94 ++ DiscUtils/Core/Partitions/PartitionInfo.cs | 93 ++ DiscUtils/Core/Partitions/PartitionTable.cs | 211 +++++ .../Core/Partitions/PartitionTableFactory.cs | 33 + .../PartitionTableFactoryAttribute.cs | 29 + .../Core/Partitions/WellKnownPartitionType.cs | 55 ++ DiscUtils/Core/PhysicalVolumeInfo.cs | 202 +++++ DiscUtils/Core/PhysicalVolumeType.cs | 33 + DiscUtils/Core/Plist.cs | 208 +++++ DiscUtils/Core/Raw/Disk.cs | 222 +++++ DiscUtils/Core/Raw/DiskFactory.cs | 84 ++ DiscUtils/Core/Raw/DiskImageFile.cs | 246 ++++++ DiscUtils/Core/ReadOnlyDiscFileSystem.cs | 166 ++++ DiscUtils/Core/ReparsePoint.cs | 51 ++ DiscUtils/Core/ReportLevels.cs | 36 + DiscUtils/Core/Setup/FileOpenEventArgs.cs | 83 ++ DiscUtils/Core/Setup/SetupHelper.cs | 55 ++ .../Core/System/DateTimeOffsetExtensions.cs | 42 + DiscUtils/Core/System/ExtensionAttribute.cs | 11 + DiscUtils/Core/System/HashSet.cs | 125 +++ DiscUtils/Core/System/Tuple.cs | 116 +++ DiscUtils/Core/TimeConverter.cs | 12 + DiscUtils/Core/UnixFilePermissions.cs | 113 +++ DiscUtils/Core/UnixFileSystemInfo.cs | 65 ++ DiscUtils/Core/UnixFileType.cs | 70 ++ DiscUtils/Core/Vfs/IVfsDirectory.cs | 60 ++ DiscUtils/Core/Vfs/IVfsFile.cs | 69 ++ DiscUtils/Core/Vfs/IVfsFileWithStreams.cs | 26 + DiscUtils/Core/Vfs/IVfsSymlink.cs | 39 + DiscUtils/Core/Vfs/VfsContext.cs | 29 + DiscUtils/Core/Vfs/VfsDirEntry.cs | 128 +++ DiscUtils/Core/Vfs/VfsFileSystem.cs | 749 ++++++++++++++++ DiscUtils/Core/Vfs/VfsFileSystemFacade.cs | 567 ++++++++++++ DiscUtils/Core/Vfs/VfsFileSystemFactory.cs | 63 ++ .../Core/Vfs/VfsFileSystemFactoryAttribute.cs | 32 + DiscUtils/Core/Vfs/VfsFileSystemInfo.cs | 79 ++ DiscUtils/Core/Vfs/VfsFileSystemOpener.cs | 14 + DiscUtils/Core/Vfs/VfsReadOnlyFileSystem.cs | 169 ++++ DiscUtils/Core/VirtualDisk.cs | 641 ++++++++++++++ DiscUtils/Core/VirtualDiskClass.cs | 50 ++ DiscUtils/Core/VirtualDiskExtent.cs | 78 ++ DiscUtils/Core/VirtualDiskLayer.cs | 126 +++ DiscUtils/Core/VirtualDiskManager.cs | 70 ++ DiscUtils/Core/VirtualDiskParameters.cs | 65 ++ DiscUtils/Core/VirtualDiskTypeInfo.cs | 60 ++ DiscUtils/Core/VolumeInfo.cs | 80 ++ DiscUtils/Core/VolumeManager.cs | 343 ++++++++ DiscUtils/Core/WindowsFileInformation.cs | 58 ++ DiscUtils/DiscUtils.csproj | 9 + DiscUtils/Iso9660/BaseVolumeDescriptor.cs | 60 ++ DiscUtils/Iso9660/BootDeviceEmulation.cs | 55 ++ DiscUtils/Iso9660/BootInitialEntry.cs | 60 ++ DiscUtils/Iso9660/BootValidationEntry.cs | 88 ++ DiscUtils/Iso9660/BootVolumeDescriptor.cs | 56 ++ .../Iso9660/BootVolumeDescriptorRegion.cs | 42 + DiscUtils/Iso9660/BuildDirectoryInfo.cs | 219 +++++ DiscUtils/Iso9660/BuildDirectoryMember.cs | 155 ++++ DiscUtils/Iso9660/BuildFileInfo.cs | 136 +++ DiscUtils/Iso9660/BuildParameters.cs | 40 + DiscUtils/Iso9660/CDBuilder.cs | 518 +++++++++++ DiscUtils/Iso9660/CDReader.cs | 229 +++++ DiscUtils/Iso9660/CommonVolumeDescriptor.cs | 141 +++ DiscUtils/Iso9660/DirectoryExtent.cs | 71 ++ DiscUtils/Iso9660/DirectoryRecord.cs | 114 +++ DiscUtils/Iso9660/DiscUtils.Iso9660.csproj | 14 + DiscUtils/Iso9660/ExtentStream.cs | 125 +++ DiscUtils/Iso9660/File.cs | 119 +++ DiscUtils/Iso9660/FileExtent.cs | 85 ++ DiscUtils/Iso9660/FileFlags.cs | 16 + DiscUtils/Iso9660/Iso9660Variant.cs | 60 ++ DiscUtils/Iso9660/IsoContext.cs | 49 ++ DiscUtils/Iso9660/IsoUtilities.cs | 467 ++++++++++ DiscUtils/Iso9660/PathTable.cs | 100 +++ DiscUtils/Iso9660/PathTableRecord.cs | 71 ++ DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs | 76 ++ .../Iso9660/PrimaryVolumeDescriptorRegion.cs | 42 + DiscUtils/Iso9660/ReaderDirEntry.cs | 195 +++++ DiscUtils/Iso9660/ReaderDirectory.cs | 130 +++ .../RockRidge/ChildLinkSystemUseEntry.cs | 36 + .../RockRidge/FileTimeSystemUseEntry.cs | 94 ++ .../RockRidge/PosixFileInfoSystemUseEntry.cs | 48 ++ .../RockRidge/PosixNameSystemUseEntry.cs | 40 + .../Iso9660/RockRidge/RockRidgeExtension.cs | 57 ++ .../Iso9660/SupplementaryVolumeDescriptor.cs | 80 ++ .../SupplementaryVolumeDescriptorRegion.cs | 42 + .../Susp/ContinuationSystemUseEntry.cs | 40 + .../Susp/ExtensionSelectSystemUseEntry.cs | 36 + .../Iso9660/Susp/ExtensionSystemUseEntry.cs | 56 ++ .../Iso9660/Susp/GenericSuspExtension.cs | 41 + .../Iso9660/Susp/GenericSystemUseEntry.cs | 39 + .../Iso9660/Susp/PaddingSystemUseEntry.cs | 32 + .../Susp/SharingProtocolSystemUseEntry.cs | 43 + DiscUtils/Iso9660/Susp/SuspExtension.cs | 33 + DiscUtils/Iso9660/Susp/SuspRecords.cs | 182 ++++ DiscUtils/Iso9660/Susp/SystemUseEntry.cs | 105 +++ DiscUtils/Iso9660/VfsCDReader.cs | 530 ++++++++++++ .../Iso9660/VolumeDescriptorDiskRegion.cs | 60 ++ .../Iso9660/VolumeDescriptorSetTerminator.cs | 30 + .../VolumeDescriptorSetTerminatorRegion.cs | 42 + DiscUtils/Iso9660/VolumeDescriptorType.cs | 11 + DiscUtils/Streams/AligningStream.cs | 180 ++++ DiscUtils/Streams/Block/Block.cs | 38 + DiscUtils/Streams/Block/BlockCache.cs | 132 +++ DiscUtils/Streams/Block/BlockCacheSettings.cs | 77 ++ .../Streams/Block/BlockCacheStatistics.cs | 79 ++ DiscUtils/Streams/Block/BlockCacheStream.cs | 461 ++++++++++ DiscUtils/Streams/Buffer/Buffer.cs | 121 +++ DiscUtils/Streams/Buffer/BufferStream.cs | 213 +++++ DiscUtils/Streams/Buffer/IBuffer.cs | 112 +++ DiscUtils/Streams/Buffer/IMappedBuffer.cs | 29 + DiscUtils/Streams/Buffer/SubBuffer.cs | 173 ++++ .../Streams/Builder/BuilderBufferExtent.cs | 73 ++ .../Builder/BuilderBufferExtentSource.cs | 39 + .../Streams/Builder/BuilderBytesExtent.cs | 56 ++ DiscUtils/Streams/Builder/BuilderExtent.cs | 57 ++ .../Streams/Builder/BuilderExtentSource.cs | 29 + .../Builder/BuilderSparseStreamExtent.cs | 66 ++ .../Streams/Builder/BuilderStreamExtent.cs | 61 ++ .../Builder/BuilderStreamExtentSource.cs | 41 + .../Builder/PassthroughStreamBuilder.cs | 46 + DiscUtils/Streams/Builder/StreamBuilder.cs | 76 ++ DiscUtils/Streams/BuiltStream.cs | 325 +++++++ DiscUtils/Streams/CircularStream.cs | 74 ++ DiscUtils/Streams/ConcatStream.cs | 286 +++++++ DiscUtils/Streams/DiscUtils.Streams.csproj | 10 + DiscUtils/Streams/IByteArraySerializable.cs | 50 ++ DiscUtils/Streams/LengthWrappingStream.cs | 47 + DiscUtils/Streams/MappedStream.cs | 83 ++ DiscUtils/Streams/MirrorStream.cs | 170 ++++ DiscUtils/Streams/PositionWrappingStream.cs | 102 +++ DiscUtils/Streams/PumpProgressEventArgs.cs | 52 ++ .../ReaderWriter/BigEndianDataReader.cs | 63 ++ .../ReaderWriter/BigEndianDataWriter.cs | 68 ++ DiscUtils/Streams/ReaderWriter/DataReader.cs | 84 ++ DiscUtils/Streams/ReaderWriter/DataWriter.cs | 79 ++ .../ReaderWriter/LittleEndianDataReader.cs | 66 ++ DiscUtils/Streams/SnapshotStream.cs | 410 +++++++++ DiscUtils/Streams/SparseMemoryBuffer.cs | 252 ++++++ DiscUtils/Streams/SparseMemoryStream.cs | 47 + DiscUtils/Streams/SparseStream.cs | 324 +++++++ DiscUtils/Streams/SparseStreamOpenDelegate.cs | 4 + DiscUtils/Streams/StreamBuffer.cs | 164 ++++ DiscUtils/Streams/StreamExtent.cs | 493 +++++++++++ DiscUtils/Streams/StreamPump.cs | 270 ++++++ DiscUtils/Streams/StripedStream.cs | 221 +++++ DiscUtils/Streams/SubStream.cs | 212 +++++ DiscUtils/Streams/System/Func.cs | 7 + DiscUtils/Streams/ThreadSafeStream.cs | 336 ++++++++ DiscUtils/Streams/Util/BitCounter.cs | 80 ++ DiscUtils/Streams/Util/EndianUtilities.cs | 306 +++++++ DiscUtils/Streams/Util/MathUtilities.cs | 137 +++ DiscUtils/Streams/Util/Numbers.cs | 228 +++++ DiscUtils/Streams/Util/Ownership.cs | 40 + DiscUtils/Streams/Util/Range.cs | 131 +++ DiscUtils/Streams/Util/Sizes.cs | 33 + DiscUtils/Streams/Util/StreamUtilities.cs | 288 +++++++ DiscUtils/Streams/WrappingMappedStream.cs | 165 ++++ DiscUtils/Streams/WrappingStream.cs | 127 +++ DiscUtils/Streams/ZeroStream.cs | 144 ++++ PbpResign/Program.cs | 6 +- PopsBuilder/GameBuilder.csproj | 6 +- PopsBuilder/Pops/DiscCompressor.cs | 14 +- PopsBuilder/Pops/DiscInfo.cs | 40 +- PopsBuilder/Pops/PopsImg.cs | 2 +- PopsBuilder/Pops/PsIsoImg.cs | 10 + PopsBuilder/Pops/PsTitleImg.cs | 22 +- PopsBuilder/Progress/ProgressInfo.cs | 62 ++ PopsBuilder/Progress/ProgressTracker.cs | 25 + PopsBuilder/Psp/NpDrmPsar.cs | 7 +- PopsBuilder/Psp/NpUmdImg.cs | 23 +- PopsBuilder/Psp/PbpBuilder.cs | 4 +- PopsBuilder/Psp/Sfo.cs | 50 +- PopsBuilder/Psp/{UmdDisc.cs => UmdInfo.cs} | 11 +- PopsBuilder/StreamUtil.cs | 15 +- PopsBuilder/VersionKey/ActRifMethod.cs | 24 + PopsBuilder/VersionKey/EbootPbpMethod.cs | 80 ++ PspCrypto/Lz.cs | 2 - PspCrypto/PspCrypto.csproj | 1 - PspTest.sln | 18 + 288 files changed, 33274 insertions(+), 57 deletions(-) create mode 100644 ChovySign-CLI/ChovySign-CLI.csproj create mode 100644 ChovySign-CLI/Program.cs create mode 100644 ChovySign-CLI/Resources.Designer.cs create mode 100644 ChovySign-CLI/Resources.resx create mode 100644 ChovySign-CLI/Resources/PIC1.PNG create mode 100644 DiscUtils/Core/ApplePartitionMap/BlockZero.cs create mode 100644 DiscUtils/Core/ApplePartitionMap/PartitionMap.cs create mode 100644 DiscUtils/Core/ApplePartitionMap/PartitionMapEntry.cs create mode 100644 DiscUtils/Core/ApplePartitionMap/PartitionMapFactory.cs create mode 100644 DiscUtils/Core/Archives/FileRecord.cs create mode 100644 DiscUtils/Core/Archives/TarFile.cs create mode 100644 DiscUtils/Core/Archives/TarFileBuilder.cs create mode 100644 DiscUtils/Core/Archives/TarHeader.cs create mode 100644 DiscUtils/Core/Archives/TarHeaderExtent.cs create mode 100644 DiscUtils/Core/Archives/UnixBuildFileRecord.cs create mode 100644 DiscUtils/Core/ChsAddress.cs create mode 100644 DiscUtils/Core/ClusterMap.cs create mode 100644 DiscUtils/Core/ClusterRoles.cs create mode 100644 DiscUtils/Core/Compression/Adler32.cs create mode 100644 DiscUtils/Core/Compression/BZip2BlockDecoder.cs create mode 100644 DiscUtils/Core/Compression/BZip2CombinedHuffmanTrees.cs create mode 100644 DiscUtils/Core/Compression/BZip2DecoderStream.cs create mode 100644 DiscUtils/Core/Compression/BZip2Randomizer.cs create mode 100644 DiscUtils/Core/Compression/BZip2RleStream.cs create mode 100644 DiscUtils/Core/Compression/BigEndianBitStream.cs create mode 100644 DiscUtils/Core/Compression/BitStream.cs create mode 100644 DiscUtils/Core/Compression/BlockCompressor.cs create mode 100644 DiscUtils/Core/Compression/CompressionResult.cs create mode 100644 DiscUtils/Core/Compression/DataBlockTransform.cs create mode 100644 DiscUtils/Core/Compression/HuffmanTree.cs create mode 100644 DiscUtils/Core/Compression/InverseBurrowsWheeler.cs create mode 100644 DiscUtils/Core/Compression/MoveToFront.cs create mode 100644 DiscUtils/Core/Compression/SizedDeflateStream.cs create mode 100644 DiscUtils/Core/Compression/ZlibBuffer.cs create mode 100644 DiscUtils/Core/Compression/ZlibStream.cs create mode 100644 DiscUtils/Core/CoreCompat/EncodingHelper.cs create mode 100644 DiscUtils/Core/CoreCompat/ReflectionHelper.cs create mode 100644 DiscUtils/Core/CoreCompat/StringExtensions.cs create mode 100644 DiscUtils/Core/DiscDirectoryInfo.cs create mode 100644 DiscUtils/Core/DiscFileInfo.cs create mode 100644 DiscUtils/Core/DiscFileLocator.cs create mode 100644 DiscUtils/Core/DiscFileSystem.cs create mode 100644 DiscUtils/Core/DiscFileSystemChecker.cs create mode 100644 DiscUtils/Core/DiscFileSystemInfo.cs create mode 100644 DiscUtils/Core/DiscFileSystemOptions.cs create mode 100644 DiscUtils/Core/DiscUtils.Core.csproj create mode 100644 DiscUtils/Core/DiskImageBuilder.cs create mode 100644 DiscUtils/Core/DiskImageFileSpecification.cs create mode 100644 DiscUtils/Core/FileLocator.cs create mode 100644 DiscUtils/Core/FileSystemInfo.cs create mode 100644 DiscUtils/Core/FileSystemManager.cs create mode 100644 DiscUtils/Core/FileSystemParameters.cs create mode 100644 DiscUtils/Core/FileTransport.cs create mode 100644 DiscUtils/Core/FloppyDiskType.cs create mode 100644 DiscUtils/Core/GenericDiskAdapterType.cs create mode 100644 DiscUtils/Core/Geometry.cs create mode 100644 DiscUtils/Core/GeometryCalculation.cs create mode 100644 DiscUtils/Core/GeometryTranslation.cs create mode 100644 DiscUtils/Core/IClusterBasedFileSystem.cs create mode 100644 DiscUtils/Core/IDiagnosticTraceable.cs create mode 100644 DiscUtils/Core/IFileSystem.cs create mode 100644 DiscUtils/Core/IUnixFileSystem.cs create mode 100644 DiscUtils/Core/IWindowsFileSystem.cs create mode 100644 DiscUtils/Core/Internal/Crc32.cs create mode 100644 DiscUtils/Core/Internal/Crc32Algorithm.cs create mode 100644 DiscUtils/Core/Internal/Crc32BigEndian.cs create mode 100644 DiscUtils/Core/Internal/Crc32LittleEndian.cs create mode 100644 DiscUtils/Core/Internal/LocalFileLocator.cs create mode 100644 DiscUtils/Core/Internal/LogicalVolumeFactory.cs create mode 100644 DiscUtils/Core/Internal/LogicalVolumeFactoryAttribute.cs create mode 100644 DiscUtils/Core/Internal/ObjectCache.cs create mode 100644 DiscUtils/Core/Internal/Utilities.cs create mode 100644 DiscUtils/Core/Internal/VirtualDiskFactory.cs create mode 100644 DiscUtils/Core/Internal/VirtualDiskFactoryAttribute.cs create mode 100644 DiscUtils/Core/Internal/VirtualDiskTransport.cs create mode 100644 DiscUtils/Core/Internal/VirtualDiskTransportAttribute.cs create mode 100644 DiscUtils/Core/InvalidFileSystemException.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/ComponentRecord.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/Database.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/DatabaseHeader.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/DatabaseRecord.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/DiskGroupRecord.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/DiskRecord.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/DynamicDisk.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/DynamicDiskGroup.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/DynamicDiskManager.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/DynamicDiskManagerFactory.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/DynamicVolume.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/ExtentMergeType.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/ExtentRecord.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/PrivateHeader.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/RecordType.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/TocBlock.cs create mode 100644 DiscUtils/Core/LogicalDiskManager/VolumeRecord.cs create mode 100644 DiscUtils/Core/LogicalVolumeInfo.cs create mode 100644 DiscUtils/Core/LogicalVolumeStatus.cs create mode 100644 DiscUtils/Core/NativeFileSystem.cs create mode 100644 DiscUtils/Core/Partitions/BiosExtendedPartitionTable.cs create mode 100644 DiscUtils/Core/Partitions/BiosPartitionInfo.cs create mode 100644 DiscUtils/Core/Partitions/BiosPartitionRecord.cs create mode 100644 DiscUtils/Core/Partitions/BiosPartitionTable.cs create mode 100644 DiscUtils/Core/Partitions/BiosPartitionTypes.cs create mode 100644 DiscUtils/Core/Partitions/BiosPartitionedDiskBuilder.cs create mode 100644 DiscUtils/Core/Partitions/DefaultPartitionTableFactory.cs create mode 100644 DiscUtils/Core/Partitions/GptEntry.cs create mode 100644 DiscUtils/Core/Partitions/GptHeader.cs create mode 100644 DiscUtils/Core/Partitions/GuidPartitionInfo.cs create mode 100644 DiscUtils/Core/Partitions/GuidPartitionTable.cs create mode 100644 DiscUtils/Core/Partitions/GuidPartitionTypes.cs create mode 100644 DiscUtils/Core/Partitions/PartitionInfo.cs create mode 100644 DiscUtils/Core/Partitions/PartitionTable.cs create mode 100644 DiscUtils/Core/Partitions/PartitionTableFactory.cs create mode 100644 DiscUtils/Core/Partitions/PartitionTableFactoryAttribute.cs create mode 100644 DiscUtils/Core/Partitions/WellKnownPartitionType.cs create mode 100644 DiscUtils/Core/PhysicalVolumeInfo.cs create mode 100644 DiscUtils/Core/PhysicalVolumeType.cs create mode 100644 DiscUtils/Core/Plist.cs create mode 100644 DiscUtils/Core/Raw/Disk.cs create mode 100644 DiscUtils/Core/Raw/DiskFactory.cs create mode 100644 DiscUtils/Core/Raw/DiskImageFile.cs create mode 100644 DiscUtils/Core/ReadOnlyDiscFileSystem.cs create mode 100644 DiscUtils/Core/ReparsePoint.cs create mode 100644 DiscUtils/Core/ReportLevels.cs create mode 100644 DiscUtils/Core/Setup/FileOpenEventArgs.cs create mode 100644 DiscUtils/Core/Setup/SetupHelper.cs create mode 100644 DiscUtils/Core/System/DateTimeOffsetExtensions.cs create mode 100644 DiscUtils/Core/System/ExtensionAttribute.cs create mode 100644 DiscUtils/Core/System/HashSet.cs create mode 100644 DiscUtils/Core/System/Tuple.cs create mode 100644 DiscUtils/Core/TimeConverter.cs create mode 100644 DiscUtils/Core/UnixFilePermissions.cs create mode 100644 DiscUtils/Core/UnixFileSystemInfo.cs create mode 100644 DiscUtils/Core/UnixFileType.cs create mode 100644 DiscUtils/Core/Vfs/IVfsDirectory.cs create mode 100644 DiscUtils/Core/Vfs/IVfsFile.cs create mode 100644 DiscUtils/Core/Vfs/IVfsFileWithStreams.cs create mode 100644 DiscUtils/Core/Vfs/IVfsSymlink.cs create mode 100644 DiscUtils/Core/Vfs/VfsContext.cs create mode 100644 DiscUtils/Core/Vfs/VfsDirEntry.cs create mode 100644 DiscUtils/Core/Vfs/VfsFileSystem.cs create mode 100644 DiscUtils/Core/Vfs/VfsFileSystemFacade.cs create mode 100644 DiscUtils/Core/Vfs/VfsFileSystemFactory.cs create mode 100644 DiscUtils/Core/Vfs/VfsFileSystemFactoryAttribute.cs create mode 100644 DiscUtils/Core/Vfs/VfsFileSystemInfo.cs create mode 100644 DiscUtils/Core/Vfs/VfsFileSystemOpener.cs create mode 100644 DiscUtils/Core/Vfs/VfsReadOnlyFileSystem.cs create mode 100644 DiscUtils/Core/VirtualDisk.cs create mode 100644 DiscUtils/Core/VirtualDiskClass.cs create mode 100644 DiscUtils/Core/VirtualDiskExtent.cs create mode 100644 DiscUtils/Core/VirtualDiskLayer.cs create mode 100644 DiscUtils/Core/VirtualDiskManager.cs create mode 100644 DiscUtils/Core/VirtualDiskParameters.cs create mode 100644 DiscUtils/Core/VirtualDiskTypeInfo.cs create mode 100644 DiscUtils/Core/VolumeInfo.cs create mode 100644 DiscUtils/Core/VolumeManager.cs create mode 100644 DiscUtils/Core/WindowsFileInformation.cs create mode 100644 DiscUtils/DiscUtils.csproj create mode 100644 DiscUtils/Iso9660/BaseVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660/BootDeviceEmulation.cs create mode 100644 DiscUtils/Iso9660/BootInitialEntry.cs create mode 100644 DiscUtils/Iso9660/BootValidationEntry.cs create mode 100644 DiscUtils/Iso9660/BootVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660/BootVolumeDescriptorRegion.cs create mode 100644 DiscUtils/Iso9660/BuildDirectoryInfo.cs create mode 100644 DiscUtils/Iso9660/BuildDirectoryMember.cs create mode 100644 DiscUtils/Iso9660/BuildFileInfo.cs create mode 100644 DiscUtils/Iso9660/BuildParameters.cs create mode 100644 DiscUtils/Iso9660/CDBuilder.cs create mode 100644 DiscUtils/Iso9660/CDReader.cs create mode 100644 DiscUtils/Iso9660/CommonVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660/DirectoryExtent.cs create mode 100644 DiscUtils/Iso9660/DirectoryRecord.cs create mode 100644 DiscUtils/Iso9660/DiscUtils.Iso9660.csproj create mode 100644 DiscUtils/Iso9660/ExtentStream.cs create mode 100644 DiscUtils/Iso9660/File.cs create mode 100644 DiscUtils/Iso9660/FileExtent.cs create mode 100644 DiscUtils/Iso9660/FileFlags.cs create mode 100644 DiscUtils/Iso9660/Iso9660Variant.cs create mode 100644 DiscUtils/Iso9660/IsoContext.cs create mode 100644 DiscUtils/Iso9660/IsoUtilities.cs create mode 100644 DiscUtils/Iso9660/PathTable.cs create mode 100644 DiscUtils/Iso9660/PathTableRecord.cs create mode 100644 DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660/PrimaryVolumeDescriptorRegion.cs create mode 100644 DiscUtils/Iso9660/ReaderDirEntry.cs create mode 100644 DiscUtils/Iso9660/ReaderDirectory.cs create mode 100644 DiscUtils/Iso9660/RockRidge/ChildLinkSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/RockRidge/FileTimeSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/RockRidge/PosixFileInfoSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/RockRidge/PosixNameSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/RockRidge/RockRidgeExtension.cs create mode 100644 DiscUtils/Iso9660/SupplementaryVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660/SupplementaryVolumeDescriptorRegion.cs create mode 100644 DiscUtils/Iso9660/Susp/ContinuationSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/Susp/ExtensionSelectSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/Susp/ExtensionSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/Susp/GenericSuspExtension.cs create mode 100644 DiscUtils/Iso9660/Susp/GenericSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/Susp/PaddingSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/Susp/SharingProtocolSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/Susp/SuspExtension.cs create mode 100644 DiscUtils/Iso9660/Susp/SuspRecords.cs create mode 100644 DiscUtils/Iso9660/Susp/SystemUseEntry.cs create mode 100644 DiscUtils/Iso9660/VfsCDReader.cs create mode 100644 DiscUtils/Iso9660/VolumeDescriptorDiskRegion.cs create mode 100644 DiscUtils/Iso9660/VolumeDescriptorSetTerminator.cs create mode 100644 DiscUtils/Iso9660/VolumeDescriptorSetTerminatorRegion.cs create mode 100644 DiscUtils/Iso9660/VolumeDescriptorType.cs create mode 100644 DiscUtils/Streams/AligningStream.cs create mode 100644 DiscUtils/Streams/Block/Block.cs create mode 100644 DiscUtils/Streams/Block/BlockCache.cs create mode 100644 DiscUtils/Streams/Block/BlockCacheSettings.cs create mode 100644 DiscUtils/Streams/Block/BlockCacheStatistics.cs create mode 100644 DiscUtils/Streams/Block/BlockCacheStream.cs create mode 100644 DiscUtils/Streams/Buffer/Buffer.cs create mode 100644 DiscUtils/Streams/Buffer/BufferStream.cs create mode 100644 DiscUtils/Streams/Buffer/IBuffer.cs create mode 100644 DiscUtils/Streams/Buffer/IMappedBuffer.cs create mode 100644 DiscUtils/Streams/Buffer/SubBuffer.cs create mode 100644 DiscUtils/Streams/Builder/BuilderBufferExtent.cs create mode 100644 DiscUtils/Streams/Builder/BuilderBufferExtentSource.cs create mode 100644 DiscUtils/Streams/Builder/BuilderBytesExtent.cs create mode 100644 DiscUtils/Streams/Builder/BuilderExtent.cs create mode 100644 DiscUtils/Streams/Builder/BuilderExtentSource.cs create mode 100644 DiscUtils/Streams/Builder/BuilderSparseStreamExtent.cs create mode 100644 DiscUtils/Streams/Builder/BuilderStreamExtent.cs create mode 100644 DiscUtils/Streams/Builder/BuilderStreamExtentSource.cs create mode 100644 DiscUtils/Streams/Builder/PassthroughStreamBuilder.cs create mode 100644 DiscUtils/Streams/Builder/StreamBuilder.cs create mode 100644 DiscUtils/Streams/BuiltStream.cs create mode 100644 DiscUtils/Streams/CircularStream.cs create mode 100644 DiscUtils/Streams/ConcatStream.cs create mode 100644 DiscUtils/Streams/DiscUtils.Streams.csproj create mode 100644 DiscUtils/Streams/IByteArraySerializable.cs create mode 100644 DiscUtils/Streams/LengthWrappingStream.cs create mode 100644 DiscUtils/Streams/MappedStream.cs create mode 100644 DiscUtils/Streams/MirrorStream.cs create mode 100644 DiscUtils/Streams/PositionWrappingStream.cs create mode 100644 DiscUtils/Streams/PumpProgressEventArgs.cs create mode 100644 DiscUtils/Streams/ReaderWriter/BigEndianDataReader.cs create mode 100644 DiscUtils/Streams/ReaderWriter/BigEndianDataWriter.cs create mode 100644 DiscUtils/Streams/ReaderWriter/DataReader.cs create mode 100644 DiscUtils/Streams/ReaderWriter/DataWriter.cs create mode 100644 DiscUtils/Streams/ReaderWriter/LittleEndianDataReader.cs create mode 100644 DiscUtils/Streams/SnapshotStream.cs create mode 100644 DiscUtils/Streams/SparseMemoryBuffer.cs create mode 100644 DiscUtils/Streams/SparseMemoryStream.cs create mode 100644 DiscUtils/Streams/SparseStream.cs create mode 100644 DiscUtils/Streams/SparseStreamOpenDelegate.cs create mode 100644 DiscUtils/Streams/StreamBuffer.cs create mode 100644 DiscUtils/Streams/StreamExtent.cs create mode 100644 DiscUtils/Streams/StreamPump.cs create mode 100644 DiscUtils/Streams/StripedStream.cs create mode 100644 DiscUtils/Streams/SubStream.cs create mode 100644 DiscUtils/Streams/System/Func.cs create mode 100644 DiscUtils/Streams/ThreadSafeStream.cs create mode 100644 DiscUtils/Streams/Util/BitCounter.cs create mode 100644 DiscUtils/Streams/Util/EndianUtilities.cs create mode 100644 DiscUtils/Streams/Util/MathUtilities.cs create mode 100644 DiscUtils/Streams/Util/Numbers.cs create mode 100644 DiscUtils/Streams/Util/Ownership.cs create mode 100644 DiscUtils/Streams/Util/Range.cs create mode 100644 DiscUtils/Streams/Util/Sizes.cs create mode 100644 DiscUtils/Streams/Util/StreamUtilities.cs create mode 100644 DiscUtils/Streams/WrappingMappedStream.cs create mode 100644 DiscUtils/Streams/WrappingStream.cs create mode 100644 DiscUtils/Streams/ZeroStream.cs create mode 100644 PopsBuilder/Progress/ProgressInfo.cs create mode 100644 PopsBuilder/Progress/ProgressTracker.cs rename PopsBuilder/Psp/{UmdDisc.cs => UmdInfo.cs} (86%) create mode 100644 PopsBuilder/VersionKey/ActRifMethod.cs create mode 100644 PopsBuilder/VersionKey/EbootPbpMethod.cs diff --git a/.gitignore b/.gitignore index df81f71..9d86576 100644 --- a/.gitignore +++ b/.gitignore @@ -8,10 +8,15 @@ PbpResign/bin/* PbpResign/obj/* +ChovySign-CLI/bin/* +ChovySign-CLI/obj/* + +DiscUtils/bin/* +DiscUtils/obj/* + PopsBuilder/bin/* PopsBuilder/obj/* - PspCrypto/bin/* PspCrypto/obj/* diff --git a/ChovySign-CLI/ChovySign-CLI.csproj b/ChovySign-CLI/ChovySign-CLI.csproj new file mode 100644 index 0000000..3a86ec8 --- /dev/null +++ b/ChovySign-CLI/ChovySign-CLI.csproj @@ -0,0 +1,30 @@ + + + + Exe + net6.0 + ChovySign_CLI + enable + enable + + + + + + + + + True + True + Resources.resx + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/ChovySign-CLI/Program.cs b/ChovySign-CLI/Program.cs new file mode 100644 index 0000000..e766822 --- /dev/null +++ b/ChovySign-CLI/Program.cs @@ -0,0 +1,272 @@ +using GameBuilder.Pops; +using GameBuilder.Progress; +using GameBuilder.Psp; +using GameBuilder.VersionKey; + +namespace ChovySign_CLI +{ + internal class Program + { + private static ArgumentParsingMode mode = ArgumentParsingMode.ARG; + private static List parameters = new List(); + private static string[] discs; + private static bool pspCompress = false; + private static string? popsDiscName; + private static string? popsIcon0File; + private static string? popsPic0File; + private static PbpMode? pbpMode = null; + private static NpDrmInfo? drmInfo = null; + enum PbpMode + { + PSP = 0, + POPS = 1, + PCENGINE = 2, + NEOGEO = 3 + } + enum ArgumentParsingMode + { + ARG = 0, + POPS_DISC = 1, + PSP_UMD = 2, + VERSIONKEY = 3, + VERSIONKEY_EXTRACT = 4, + VERSIONKEY_GENERATOR = 5, + POPS_INFO = 6 + } + public static int Error(string errorMsg, int ret) + { + Console.Error.WriteLine("ERROR: "+errorMsg); + return ret; + } + public static byte[] StringToByteArray(string hex) + { + return Enumerable.Range(0, hex.Length).Where(x => x % 2 == 0).Select(x => Convert.ToByte(hex.Substring(x, 2), 16)).ToArray(); + } + + private static void onProgress(ProgressInfo info) + { + Console.Write(info.CurrentProcess + " " + info.ProgressInt.ToString() + "% (" + info.Done + "/" + info.Remain + ") \r"); + } + + private static int complete() + { + switch (mode) + { + case ArgumentParsingMode.POPS_DISC: + if (parameters.Count > 5) return Error("--pops: no more than 5 disc images allowed in a single game (sony's rules, not mine)", 5); + if (parameters.Count < 1) return Error("--pops: at least 1 disc image file is required.", 5); + discs = parameters.ToArray(); + break; + case ArgumentParsingMode.PSP_UMD: + if (parameters.Count < 1) return Error("--psp: a path to a disc image is required", 5); + if (parameters.Count > 2) return Error("--psp: no more than 2 arguments. ("+parameters.Count+" given)", 5); + discs = new string[1]; + discs[0] = parameters[0]; + + if (parameters.Count > 1) + pspCompress = parameters[1].ToLowerInvariant() == "true"; + else + pspCompress = false; + + break; + case ArgumentParsingMode.VERSIONKEY: + if (parameters.Count != 3) return Error("--vkey: expect 3 arguments. ("+parameters.Count+" given)", 4); + drmInfo = new NpDrmInfo(StringToByteArray(parameters[0]), parameters[1], int.Parse(parameters[2])); + break; + case ArgumentParsingMode.VERSIONKEY_EXTRACT: + if (parameters.Count != 1) return Error("--vkey-extract: expect 1 arguments. ("+parameters.Count+" given)", 4); + drmInfo = EbootPbpMethod.GetVersionKey(File.OpenRead(parameters[0])); + break; + case ArgumentParsingMode.VERSIONKEY_GENERATOR: + if(parameters.Count != 4) return Error("--vkey-gen: expect 4 arguments. ("+parameters.Count+" given)", 4); + drmInfo = ActRifMethod.GetVersionKey(File.ReadAllBytes(parameters[0]), File.ReadAllBytes(parameters[1]), StringToByteArray(parameters[2]), int.Parse(parameters[3])); + break; + case ArgumentParsingMode.POPS_INFO: + if (parameters.Count < 2) return Error("--pops-info takes at least 2 arguments ("+parameters.Count+" given)", 4); + if (parameters.Count > 3) return Error("--pops-info takes no more than 3 arguments("+parameters.Count+" given)", 4); + popsDiscName = parameters[0]; + popsIcon0File = parameters[1]; + if (parameters.Count > 2) + popsPic0File = parameters[2]; + break; + } + + mode = ArgumentParsingMode.ARG; + parameters.Clear(); + return 0; + } + public static int Main(string[] args) + { + if(args.Length == 0) + { + Console.WriteLine("Chovy-Sign v2 (CLI)"); + Console.WriteLine("--pops [disc1.cue] [disc2.cue] [disc3.cue] ... (up to 5)"); + Console.WriteLine("--pops-info [game title] [icon0.png] [pic1.png] (optional)"); + Console.WriteLine("--psp [umd.iso] [compress; true/false] (optional)"); + Console.WriteLine("--vkey [versionkey] [contentid] [key_index]"); + Console.WriteLine("--vkey-extract [eboot.pbp]"); + Console.WriteLine("--vkey-gen [act.dat] [license.rif] [console_id] [key_index]"); + } + + + foreach (string arg in args) + { + if (arg.StartsWith("--")) { int ret = complete(); if (ret != 0) return ret; } + + switch (mode) + { + case ArgumentParsingMode.ARG: + switch (arg) + { + case "--pops": + mode = ArgumentParsingMode.POPS_DISC; + + if (pbpMode is not null) + return Error("pbpMode is already set to: " + pbpMode.ToString() + " cannot do that *and* POPS", 2); + + pbpMode = PbpMode.POPS; + break; + case "--pops-info": + mode = ArgumentParsingMode.POPS_INFO; + break; + case "--psp": + mode = ArgumentParsingMode.PSP_UMD; + + if (pbpMode is not null) + return Error("pbpMode is already set to: " + pbpMode.ToString() + " cannot do that *and* PSP", 2); + + pbpMode = PbpMode.PSP; + break; + + case "--vkey": + mode = ArgumentParsingMode.VERSIONKEY; + + if (drmInfo is not null) + return Error("versionkey is already set", 3); + + break; + case "--vkey-extract": + mode = ArgumentParsingMode.VERSIONKEY_EXTRACT; + + if (drmInfo is not null) + return Error("versionkey is already set", 3); + + break; + case "--vkey-gen": + mode = ArgumentParsingMode.VERSIONKEY_GENERATOR; + + if (drmInfo is not null) + return Error("versionkey is already set", 3); + + break; + default: + return Error("Unknown argument: " + arg, 1); + } + break; + case ArgumentParsingMode.VERSIONKEY: + case ArgumentParsingMode.VERSIONKEY_GENERATOR: + case ArgumentParsingMode.VERSIONKEY_EXTRACT: + case ArgumentParsingMode.PSP_UMD: + case ArgumentParsingMode.POPS_DISC: + case ArgumentParsingMode.POPS_INFO: + default: + parameters.Add(arg); + break; + } + } + int res = complete(); + if(res != 0) return res; + + if (drmInfo is null) return Error("no versionkey was found, exiting", 6); + if (pbpMode is null) return Error("no pbp mode was set, exiting", 7); + + if (pbpMode == PbpMode.PSP && drmInfo.KeyType != 2) + return Error("KeyType is "+drmInfo.KeyType+", but PBP mode is PSP, you cant do that .. please use a type 1 versionkey.", 8); + + if (pbpMode == PbpMode.POPS && drmInfo.KeyType != 1) + return Error("KeyType is " + drmInfo.KeyType + ", but PBP mode is POPS, you cant do that .. please use a type 1 versionkey.", 8); + + if (pbpMode == PbpMode.POPS && (popsDiscName is null || popsIcon0File is null)) return Error("pbp mode is POPS, but you have not specified a disc title or icon file using --pops-info.", 9); + + if (pbpMode == PbpMode.POPS) + { + + DiscInfo[] discInfs = new DiscInfo[discs.Length]; + for (int i = 0; i < discInfs.Length; i++) + discInfs[i] = new DiscInfo(discs[i], popsDiscName); + + Sfo psfo = new Sfo(); + psfo.AddKey("BOOTABLE", 1, 4); + psfo.AddKey("CATEGORY", "ME", 4); + psfo.AddKey("DISC_ID", discInfs.First().DiscId, 16); + psfo.AddKey("DISC_VERSION", "1.00", 8); + psfo.AddKey("LICENSE", "Chovy-Sign is licensed under the GPLv3, POPS stuff was done by Li and SquallATF.", 512); + psfo.AddKey("PARENTAL_LEVEL", 0, 4); + psfo.AddKey("PSP_SYSTEM_VER", "6.60", 8); + psfo.AddKey("REGION", 32768, 4); + psfo.AddKey("TITLE", popsDiscName, 128); + + + if (discs.Length == 1) + { + using (PsIsoImg psIsoImg = new PsIsoImg(drmInfo, discInfs.First())) + { + psIsoImg.RegisterCallback(onProgress); + psIsoImg.CreatePsar(); + + PbpBuilder.CreatePbp(psfo.WriteSfo(), + File.ReadAllBytes(popsIcon0File), + null, + (popsPic0File is not null) ? File.ReadAllBytes(popsPic0File) : null, + Resources.PIC1, + null, + psIsoImg, + "EBOOT.PBP", + 0); + } + } + else + { + using (PsTitleImg psIsoImg = new PsTitleImg(drmInfo, discInfs)) + { + psIsoImg.RegisterCallback(onProgress); + psIsoImg.CreatePsar(); + + PbpBuilder.CreatePbp(psfo.WriteSfo(), + File.ReadAllBytes(popsIcon0File), + null, + (popsPic0File is not null) ? File.ReadAllBytes(popsPic0File) : null, + Resources.PIC1, + null, + psIsoImg, + "EBOOT.PBP", + 0); + } + } + } + else if(pbpMode == PbpMode.PSP) + { + using (UmdInfo umd = new UmdInfo(discs.First())) + { + using (NpUmdImg npUmd = new NpUmdImg(drmInfo, umd, pspCompress)) + { + npUmd.RegisterCallback(onProgress); + npUmd.CreatePsar(); + + PbpBuilder.CreatePbp(umd.DataFiles["PARAM.SFO"], + umd.DataFiles["ICON0.PNG"], + umd.DataFiles["ICON1.PMF"], + umd.DataFiles["PIC0.PNG"], + umd.DataFiles["PIC1.PNG"], + umd.DataFiles["SND0.AT3"], + npUmd, + "EBOOT.PBP", + 1); + + } + } + } + return 0; + } + } +} \ No newline at end of file diff --git a/ChovySign-CLI/Resources.Designer.cs b/ChovySign-CLI/Resources.Designer.cs new file mode 100644 index 0000000..9624b0c --- /dev/null +++ b/ChovySign-CLI/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace ChovySign_CLI { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ChovySign_CLI.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + public static byte[] PIC1 { + get { + object obj = ResourceManager.GetObject("PIC1", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/ChovySign-CLI/Resources.resx b/ChovySign-CLI/Resources.resx new file mode 100644 index 0000000..58f7389 --- /dev/null +++ b/ChovySign-CLI/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Resources\PIC1.PNG;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ChovySign-CLI/Resources/PIC1.PNG b/ChovySign-CLI/Resources/PIC1.PNG new file mode 100644 index 0000000000000000000000000000000000000000..4435ec7c5b394cd9fb96be92d3ba095bab8c3b8e GIT binary patch literal 2651 zcmX|@dpuO@8pqd~F>d3swKFP}E?cM5xYofziiFCMt}{z+E0SuM>4G8C7E?8w)bEP7c&KuX?Umlc7BM#8f2 z4#%G2kn_$aW*|w4f5)lN*5QBVSUB$ExM@sQ=%3=4ejg$H4I~HtCyjuPurB|Et2S*_3sfYx-mYF& z;sJW2echoEF=7c!q}46g7@~gxci0|PSmtGjX#2xh$&ygxS~#IiQ$xh-y$sL>#~Z+je_vp${uC#nLP$*qweph?h3S#)2aaG29Du4tiun&#)WIZ zNvNAmhih12H{hYW`Y6W`PhpB2QI3kO81WZ&9>#EpGP$LXV}s*HyA1?Mf*{1&Iq*D4 zTRWSKR`ftf6H~ushx z#uRY~{2*Abh+xKf$U5v&m^n=3qi|HP9s)HPqKn6_LnObmmA;+eKq|UV(G(U{>tOC@ z8Zj-{Vh=>L;O~Ri!W2I=(R>bPHXt+mdQ}fgAe!w|B&bFNzTsEPd?J z&IER1Ygfdj&!Mq_c5WP%)cEzGPH!An)Tv1WeKt?i$e>_r*A>R%VX&mARv~AyXK-fJ z^g`OyH3PKj;-t6}LM#KmJ_gx=YOpC4J^d+#1!p8vM>Ynm84YpL;%hRJsrp?#4;OG| z%z4U}r6_tbKH+W8hDi?W>5-$tjA&J+zvIh8hUy7Wq0YMQdzu>@HVYPy?r=Ty_QduH zuUSLi%7Vh`2WnC~cI}n+Ozh0y%3eDMCxz-)Z16eb<5dt}K4+{BeS+j_aZk?YG5;)Ykx7J8fmnk?$p!i$TW_&FkS81-o;mi&n(Dxxo!ZHu>2 zz7!RGvWRosz~}XDUO3}k<@7xQmuydqdXj1xuX-kWk;*7<<7sluB#n`sJo=jW@`mYH zgFqZPW$c3wovJ1!b+OK^Hb8!VcgrO3{kC}3M~TnYC*i3ZK%e8|sa>kx5U|5|COal= zYJ)_ShMnnXZy0K?}p+`0wG$M;?XM7yG)QA8kGML z(>#MiK8)V&u?=Fu+*30ogcl&ue{O_rNgA6A39hc*DmlZkX33k9!WD0n5>_J-bt*U* zvAWVP2Fprfyb)&Gz`--4)~s;HZIWe3vohTMIn}@_C)=8(kB`}jBO6P?LQN1nqvPC6 zAv_gh&$a-O?}YU#KQlwncybBQd+r$){5zW~uVC{LGq*9GOtYeWjmJ8Cxg&#V)Kns>-K9 z79O>}WEtB2nl9`hQJK>>?9QT>KSXSEXH0%`4KT#E*35mp)>(Nmb$eK;4 zcBf=xjH`ybO>j8-iXG1+g}#Y#9cyv32_0+D?&%xQU9m!(pLK2`OSyK;J+tOGeopKt zvaNmEM5jPz5pFgGsvmofLc@RNWi{p6*L>C`%?L*~F>*C6tL>e;Ya@mIb;=~2c@g~* zQ>?R8I6Oa=I_OR@YJH_t*Ge6b_S*zY%RPnfL!#@|+^rYi7IQ6%?2n);<@KJT7mII0 zY_8ySK34qv<~6ge@J8VkAUdO0X?fMTc|`e5@o_xz|v?8pypUs*RC95^O8hT3l@3EvnJkW)bO%d;YB0i)`ZggHa`Y@Q~eZ z^z=|d^`pMWnG7sD~FeYdDbn$=M`B^eRvnCR2ANv z=T4rhOdSQcX|PVa-PAm6qPHPn7sGFAd`o<@LY?P{yhxBfixPNE6^zDrCnvt_7GzR2 z!jR%297#az*ld1h=y1m}rV4X!rIc#WLR_lJL!m_#uf!8syWbjW=2OIU(ykJMP8bpEb@9Y~i>#)E}Z4@Kp*Z&V%uhOx3O)xBWJP3xtKOo0tL3*LhUwi$EI!#h4s;znixUP53VZ%oivC literal 0 HcmV?d00001 diff --git a/DiscUtils/Core/ApplePartitionMap/BlockZero.cs b/DiscUtils/Core/ApplePartitionMap/BlockZero.cs new file mode 100644 index 0000000..1ed9c8e --- /dev/null +++ b/DiscUtils/Core/ApplePartitionMap/BlockZero.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.ApplePartitionMap +{ + internal sealed class BlockZero : IByteArraySerializable + { + public uint BlockCount; + public ushort BlockSize; + public ushort DeviceId; + public ushort DeviceType; + public ushort DriverCount; + public uint DriverData; + public ushort Signature; + + public int Size + { + get { return 512; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Signature = EndianUtilities.ToUInt16BigEndian(buffer, offset + 0); + BlockSize = EndianUtilities.ToUInt16BigEndian(buffer, offset + 2); + BlockCount = EndianUtilities.ToUInt32BigEndian(buffer, offset + 4); + DeviceType = EndianUtilities.ToUInt16BigEndian(buffer, offset + 8); + DeviceId = EndianUtilities.ToUInt16BigEndian(buffer, offset + 10); + DriverData = EndianUtilities.ToUInt32BigEndian(buffer, offset + 12); + DriverCount = EndianUtilities.ToUInt16LittleEndian(buffer, offset + 16); + + return 512; + } + + public void WriteTo(byte[] buffer, int offset) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/ApplePartitionMap/PartitionMap.cs b/DiscUtils/Core/ApplePartitionMap/PartitionMap.cs new file mode 100644 index 0000000..fb62304 --- /dev/null +++ b/DiscUtils/Core/ApplePartitionMap/PartitionMap.cs @@ -0,0 +1,153 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.ObjectModel; +using System.IO; +using DiscUtils.Partitions; +using DiscUtils.Streams; + +namespace DiscUtils.ApplePartitionMap +{ + /// + /// Interprets Apple Partition Map structures that partition a disk. + /// + public sealed class PartitionMap : PartitionTable + { + private readonly PartitionMapEntry[] _partitions; + private readonly Stream _stream; + + /// + /// Initializes a new instance of the PartitionMap class. + /// + /// Stream containing the contents of a disk. + public PartitionMap(Stream stream) + { + _stream = stream; + + stream.Position = 0; + byte[] initialBytes = StreamUtilities.ReadExact(stream, 1024); + + BlockZero b0 = new BlockZero(); + b0.ReadFrom(initialBytes, 0); + + PartitionMapEntry initialPart = new PartitionMapEntry(_stream); + initialPart.ReadFrom(initialBytes, 512); + + byte[] partTableData = StreamUtilities.ReadExact(stream, (int)(initialPart.MapEntries - 1) * 512); + + _partitions = new PartitionMapEntry[initialPart.MapEntries - 1]; + for (uint i = 0; i < initialPart.MapEntries - 1; ++i) + { + _partitions[i] = new PartitionMapEntry(_stream); + _partitions[i].ReadFrom(partTableData, (int)(512 * i)); + } + } + + /// + /// Gets the GUID of the disk, always returns Guid.Empty. + /// + public override Guid DiskGuid + { + get { return Guid.Empty; } + } + + /// + /// Gets the partitions present on the disk. + /// + public override ReadOnlyCollection Partitions + { + get { return new ReadOnlyCollection(_partitions); } + } + + /// + /// Creates a new partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + public override int Create(WellKnownPartitionType type, bool active) + { + throw new NotImplementedException(); + } + + /// + /// Creates a new partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the new partition. + public override int Create(long size, WellKnownPartitionType type, bool active) + { + throw new NotImplementedException(); + } + + /// + /// Creates a new aligned partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in byte). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is acheived by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(WellKnownPartitionType type, bool active, int alignment) + { + throw new NotImplementedException(); + } + + /// + /// Creates a new aligned partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in byte). + /// The index of the new partition. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is achieved by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(long size, WellKnownPartitionType type, bool active, int alignment) + { + throw new NotImplementedException(); + } + + /// + /// Deletes a partition at a given index. + /// + /// The index of the partition. + public override void Delete(int index) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/ApplePartitionMap/PartitionMapEntry.cs b/DiscUtils/Core/ApplePartitionMap/PartitionMapEntry.cs new file mode 100644 index 0000000..30e00b1 --- /dev/null +++ b/DiscUtils/Core/ApplePartitionMap/PartitionMapEntry.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Partitions; +using DiscUtils.Streams; + +namespace DiscUtils.ApplePartitionMap +{ + internal sealed class PartitionMapEntry : PartitionInfo, IByteArraySerializable + { + private readonly Stream _diskStream; + public uint BootBlock; + public uint BootBytes; + public uint Flags; + public uint LogicalBlocks; + public uint LogicalBlockStart; + public uint MapEntries; + public string Name; + public uint PhysicalBlocks; + public uint PhysicalBlockStart; + public ushort Signature; + public string Type; + + public PartitionMapEntry(Stream diskStream) + { + _diskStream = diskStream; + } + + public override byte BiosType + { + get { return 0xAF; } + } + + public override long FirstSector + { + get { return PhysicalBlockStart; } + } + + public override Guid GuidType + { + get { return Guid.Empty; } + } + + public override long LastSector + { + get { return PhysicalBlockStart + PhysicalBlocks - 1; } + } + + public override string TypeAsString + { + get { return Type; } + } + + internal override PhysicalVolumeType VolumeType + { + get { return PhysicalVolumeType.ApplePartition; } + } + + public int Size + { + get { return 512; } + } + + public int ReadFrom(byte[] buffer, int offset) + { + Signature = EndianUtilities.ToUInt16BigEndian(buffer, offset + 0); + MapEntries = EndianUtilities.ToUInt32BigEndian(buffer, offset + 4); + PhysicalBlockStart = EndianUtilities.ToUInt32BigEndian(buffer, offset + 8); + PhysicalBlocks = EndianUtilities.ToUInt32BigEndian(buffer, offset + 12); + Name = EndianUtilities.BytesToString(buffer, offset + 16, 32).TrimEnd('\0'); + Type = EndianUtilities.BytesToString(buffer, offset + 48, 32).TrimEnd('\0'); + LogicalBlockStart = EndianUtilities.ToUInt32BigEndian(buffer, offset + 80); + LogicalBlocks = EndianUtilities.ToUInt32BigEndian(buffer, offset + 84); + Flags = EndianUtilities.ToUInt32BigEndian(buffer, offset + 88); + BootBlock = EndianUtilities.ToUInt32BigEndian(buffer, offset + 92); + BootBytes = EndianUtilities.ToUInt32BigEndian(buffer, offset + 96); + + return 512; + } + + public void WriteTo(byte[] buffer, int offset) + { + throw new NotImplementedException(); + } + + public override SparseStream Open() + { + return new SubStream(_diskStream, PhysicalBlockStart * 512, PhysicalBlocks * 512); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/ApplePartitionMap/PartitionMapFactory.cs b/DiscUtils/Core/ApplePartitionMap/PartitionMapFactory.cs new file mode 100644 index 0000000..d5971ce --- /dev/null +++ b/DiscUtils/Core/ApplePartitionMap/PartitionMapFactory.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Partitions; +using DiscUtils.Streams; + +namespace DiscUtils.ApplePartitionMap +{ + [PartitionTableFactory] + internal sealed class PartitionMapFactory : PartitionTableFactory + { + public override bool DetectIsPartitioned(Stream s) + { + if (s.Length < 1024) + { + return false; + } + + s.Position = 0; + + byte[] initialBytes = StreamUtilities.ReadExact(s, 1024); + + BlockZero b0 = new BlockZero(); + b0.ReadFrom(initialBytes, 0); + if (b0.Signature != 0x4552) + { + return false; + } + + PartitionMapEntry initialPart = new PartitionMapEntry(s); + initialPart.ReadFrom(initialBytes, 512); + + return initialPart.Signature == 0x504d; + } + + public override PartitionTable DetectPartitionTable(VirtualDisk disk) + { + if (!DetectIsPartitioned(disk.Content)) + { + return null; + } + + return new PartitionMap(disk.Content); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Archives/FileRecord.cs b/DiscUtils/Core/Archives/FileRecord.cs new file mode 100644 index 0000000..f7178f8 --- /dev/null +++ b/DiscUtils/Core/Archives/FileRecord.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Archives +{ + internal sealed class FileRecord + { + public long Length; + public string Name; + public long Start; + + public FileRecord(string name, long start, long length) + { + Name = name; + Start = start; + Length = length; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Archives/TarFile.cs b/DiscUtils/Core/Archives/TarFile.cs new file mode 100644 index 0000000..e91a1f4 --- /dev/null +++ b/DiscUtils/Core/Archives/TarFile.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Streams; + +namespace DiscUtils.Archives +{ + /// + /// Minimal tar file format implementation. + /// + public sealed class TarFile + { + private readonly Dictionary _files; + private readonly Stream _fileStream; + + /// + /// Initializes a new instance of the TarFile class. + /// + /// The Tar file. + public TarFile(Stream fileStream) + { + _fileStream = fileStream; + _files = new Dictionary(); + + TarHeader hdr = new TarHeader(); + byte[] hdrBuf = StreamUtilities.ReadExact(_fileStream, TarHeader.Length); + hdr.ReadFrom(hdrBuf, 0); + while (hdr.FileLength != 0 || !string.IsNullOrEmpty(hdr.FileName)) + { + FileRecord record = new FileRecord(hdr.FileName, _fileStream.Position, hdr.FileLength); + _files.Add(record.Name, record); + _fileStream.Position += (hdr.FileLength + 511) / 512 * 512; + + hdrBuf = StreamUtilities.ReadExact(_fileStream, TarHeader.Length); + hdr.ReadFrom(hdrBuf, 0); + } + } + + /// + /// Tries to open a file contained in the archive, if it exists. + /// + /// The path to the file within the archive. + /// A stream containing the file contents, or null. + /// true if the file could be opened, else false. + public bool TryOpenFile(string path, out Stream stream) + { + if (_files.ContainsKey(path)) + { + FileRecord file = _files[path]; + stream = new SubStream(_fileStream, file.Start, file.Length); + return true; + } + + stream = null; + return false; + } + + /// + /// Open a file contained in the archive. + /// + /// The path to the file within the archive. + /// A stream containing the file contents. + /// Thrown if the file is not found. + public Stream OpenFile(string path) + { + if (_files.ContainsKey(path)) + { + FileRecord file = _files[path]; + return new SubStream(_fileStream, file.Start, file.Length); + } + + throw new FileNotFoundException("File is not in archive", path); + } + + /// + /// Determines if a given file exists in the archive. + /// + /// The file path to test. + /// true if the file is present, else false. + public bool FileExists(string path) + { + return _files.ContainsKey(path); + } + + /// + /// Determines if a given directory exists in the archive. + /// + /// The file path to test. + /// true if the directory is present, else false. + public bool DirExists(string path) + { + string searchStr = path; + searchStr = searchStr.Replace(@"\", "/"); + searchStr = searchStr.EndsWith(@"/", StringComparison.Ordinal) ? searchStr : searchStr + @"/"; + + foreach (string filePath in _files.Keys) + { + if (filePath.StartsWith(searchStr, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + internal IEnumerable GetFiles(string dir) + { + string searchStr = dir; + searchStr = searchStr.Replace(@"\", "/"); + searchStr = searchStr.EndsWith(@"/", StringComparison.Ordinal) ? searchStr : searchStr + @"/"; + + foreach (string filePath in _files.Keys) + { + if (filePath.StartsWith(searchStr, StringComparison.Ordinal)) + { + yield return _files[filePath]; + } + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Archives/TarFileBuilder.cs b/DiscUtils/Core/Archives/TarFileBuilder.cs new file mode 100644 index 0000000..1883877 --- /dev/null +++ b/DiscUtils/Core/Archives/TarFileBuilder.cs @@ -0,0 +1,122 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Streams; + +namespace DiscUtils.Archives +{ + /// + /// Builder to create UNIX Tar archive files. + /// + public sealed class TarFileBuilder : StreamBuilder + { + private readonly List _files; + + /// + /// Initializes a new instance of the class. + /// + public TarFileBuilder() + { + _files = new List(); + } + + /// + /// Add a file to the tar archive. + /// + /// The name of the file. + /// The file data. + public void AddFile(string name, byte[] buffer) + { + _files.Add(new UnixBuildFileRecord(name, buffer)); + } + + /// + /// Add a file to the tar archive. + /// + /// The name of the file. + /// The file data. + /// The access mode of the file. + /// The uid of the owner. + /// The gid of the owner. + /// The modification time for the file. + public void AddFile( + string name, byte[] buffer, UnixFilePermissions fileMode, int ownerId, int groupId, + DateTime modificationTime) + { + _files.Add(new UnixBuildFileRecord(name, buffer, fileMode, ownerId, groupId, modificationTime)); + } + + /// + /// Add a file to the tar archive. + /// + /// The name of the file. + /// The file data. + public void AddFile(string name, Stream stream) + { + _files.Add(new UnixBuildFileRecord(name, stream)); + } + + /// + /// Add a file to the tar archive. + /// + /// The name of the file. + /// The file data. + /// The access mode of the file. + /// The uid of the owner. + /// The gid of the owner. + /// The modification time for the file. + public void AddFile( + string name, Stream stream, UnixFilePermissions fileMode, int ownerId, int groupId, + DateTime modificationTime) + { + _files.Add(new UnixBuildFileRecord(name, stream, fileMode, ownerId, groupId, modificationTime)); + } + + protected override List FixExtents(out long totalLength) + { + List result = new List(_files.Count * 2 + 2); + long pos = 0; + + foreach (UnixBuildFileRecord file in _files) + { + BuilderExtent fileContentExtent = file.Fix(pos + TarHeader.Length); + + result.Add(new TarHeaderExtent( + pos, file.Name, fileContentExtent.Length, file.FileMode, file.OwnerId, file.GroupId, + file.ModificationTime)); + pos += TarHeader.Length; + + result.Add(fileContentExtent); + pos += MathUtilities.RoundUp(fileContentExtent.Length, 512); + } + + // Two empty 512-byte blocks at end of tar file. + result.Add(new BuilderBufferExtent(pos, new byte[1024])); + + totalLength = pos + 1024; + return result; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Archives/TarHeader.cs b/DiscUtils/Core/Archives/TarHeader.cs new file mode 100644 index 0000000..22ec87a --- /dev/null +++ b/DiscUtils/Core/Archives/TarHeader.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Archives +{ + internal sealed class TarHeader + { + public const int Length = 512; + public long FileLength; + public UnixFilePermissions FileMode; + + public string FileName; + public int GroupId; + public DateTime ModificationTime; + public int OwnerId; + + public void ReadFrom(byte[] buffer, int offset) + { + FileName = ReadNullTerminatedString(buffer, offset + 0, 100); + FileMode = (UnixFilePermissions)OctalToLong(ReadNullTerminatedString(buffer, offset + 100, 8)); + OwnerId = (int)OctalToLong(ReadNullTerminatedString(buffer, offset + 108, 8)); + GroupId = (int)OctalToLong(ReadNullTerminatedString(buffer, offset + 116, 8)); + FileLength = OctalToLong(ReadNullTerminatedString(buffer, offset + 124, 12)); + ModificationTime = OctalToLong(ReadNullTerminatedString(buffer, offset + 136, 12)).FromUnixTimeSeconds().DateTime; + } + + public void WriteTo(byte[] buffer, int offset) + { + Array.Clear(buffer, offset, Length); + + EndianUtilities.StringToBytes(FileName, buffer, offset, 99); + EndianUtilities.StringToBytes(LongToOctal((long)FileMode, 7), buffer, offset + 100, 7); + EndianUtilities.StringToBytes(LongToOctal(OwnerId, 7), buffer, offset + 108, 7); + EndianUtilities.StringToBytes(LongToOctal(GroupId, 7), buffer, offset + 116, 7); + EndianUtilities.StringToBytes(LongToOctal(FileLength, 11), buffer, offset + 124, 11); + EndianUtilities.StringToBytes(LongToOctal(Convert.ToUInt32((new DateTimeOffset(ModificationTime)).ToUnixTimeSeconds()), 11), buffer, offset + 136, 11); + + // Checksum + EndianUtilities.StringToBytes(new string(' ', 8), buffer, offset + 148, 8); + long checkSum = 0; + for (int i = 0; i < 512; ++i) + { + checkSum += buffer[offset + i]; + } + + EndianUtilities.StringToBytes(LongToOctal(checkSum, 7), buffer, offset + 148, 7); + buffer[155] = 0; + } + + private static string ReadNullTerminatedString(byte[] buffer, int offset, int length) + { + return EndianUtilities.BytesToString(buffer, offset, length).TrimEnd('\0'); + } + + private static long OctalToLong(string value) + { + long result = 0; + + for (int i = 0; i < value.Length; ++i) + { + result = result * 8 + (value[i] - '0'); + } + + return result; + } + + private static string LongToOctal(long value, int length) + { + string result = string.Empty; + + while (value > 0) + { + result = (char)('0' + value % 8) + result; + value = value / 8; + } + + return new string('0', length - result.Length) + result; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Archives/TarHeaderExtent.cs b/DiscUtils/Core/Archives/TarHeaderExtent.cs new file mode 100644 index 0000000..df089a2 --- /dev/null +++ b/DiscUtils/Core/Archives/TarHeaderExtent.cs @@ -0,0 +1,68 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Archives +{ + internal sealed class TarHeaderExtent : BuilderBufferExtent + { + private readonly long _fileLength; + private readonly string _fileName; + private readonly int _groupId; + private readonly UnixFilePermissions _mode; + private readonly DateTime _modificationTime; + private readonly int _ownerId; + + public TarHeaderExtent(long start, string fileName, long fileLength, UnixFilePermissions mode, int ownerId, + int groupId, DateTime modificationTime) + : base(start, 512) + { + _fileName = fileName; + _fileLength = fileLength; + _mode = mode; + _ownerId = ownerId; + _groupId = groupId; + _modificationTime = modificationTime; + } + + public TarHeaderExtent(long start, string fileName, long fileLength) + : this(start, fileName, fileLength, 0, 0, 0, DateTimeOffsetExtensions.UnixEpoch) {} + + protected override byte[] GetBuffer() + { + byte[] buffer = new byte[TarHeader.Length]; + + TarHeader header = new TarHeader(); + header.FileName = _fileName; + header.FileLength = _fileLength; + header.FileMode = _mode; + header.OwnerId = _ownerId; + header.GroupId = _groupId; + header.ModificationTime = _modificationTime; + header.WriteTo(buffer, 0); + + return buffer; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Archives/UnixBuildFileRecord.cs b/DiscUtils/Core/Archives/UnixBuildFileRecord.cs new file mode 100644 index 0000000..fb1e3f4 --- /dev/null +++ b/DiscUtils/Core/Archives/UnixBuildFileRecord.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Archives +{ + internal sealed class UnixBuildFileRecord + { + private readonly BuilderExtentSource _source; + + public UnixBuildFileRecord(string name, byte[] buffer) + : this(name, new BuilderBufferExtentSource(buffer), 0, 0, 0, DateTimeOffsetExtensions.UnixEpoch) {} + + public UnixBuildFileRecord(string name, Stream stream) + : this(name, new BuilderStreamExtentSource(stream), 0, 0, 0, DateTimeOffsetExtensions.UnixEpoch) {} + + public UnixBuildFileRecord( + string name, byte[] buffer, UnixFilePermissions fileMode, int ownerId, int groupId, + DateTime modificationTime) + : this(name, new BuilderBufferExtentSource(buffer), fileMode, ownerId, groupId, modificationTime) {} + + public UnixBuildFileRecord( + string name, Stream stream, UnixFilePermissions fileMode, int ownerId, int groupId, + DateTime modificationTime) + : this(name, new BuilderStreamExtentSource(stream), fileMode, ownerId, groupId, modificationTime) {} + + public UnixBuildFileRecord(string name, BuilderExtentSource fileSource, UnixFilePermissions fileMode, + int ownerId, int groupId, DateTime modificationTime) + { + Name = name; + _source = fileSource; + FileMode = fileMode; + OwnerId = ownerId; + GroupId = groupId; + ModificationTime = modificationTime; + } + + public UnixFilePermissions FileMode { get; } + + public int GroupId { get; } + + public DateTime ModificationTime { get; } + + public string Name { get; } + + public int OwnerId { get; } + + public BuilderExtent Fix(long pos) + { + return _source.Fix(pos); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/ChsAddress.cs b/DiscUtils/Core/ChsAddress.cs new file mode 100644 index 0000000..1bb74e1 --- /dev/null +++ b/DiscUtils/Core/ChsAddress.cs @@ -0,0 +1,99 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// Class whose instances represent a CHS (Cylinder, Head, Sector) address on a disk. + /// + /// Instances of this class are immutable. + public sealed class ChsAddress + { + /// + /// The address of the first sector on any disk. + /// + public static readonly ChsAddress First = new ChsAddress(0, 0, 1); + + /// + /// Initializes a new instance of the ChsAddress class. + /// + /// The number of cylinders of the disk. + /// The number of heads (aka platters) of the disk. + /// The number of sectors per track/cylinder of the disk. + public ChsAddress(int cylinder, int head, int sector) + { + Cylinder = cylinder; + Head = head; + Sector = sector; + } + + /// + /// Gets the cylinder number (zero-based). + /// + public int Cylinder { get; } + + /// + /// Gets the head (zero-based). + /// + public int Head { get; } + + /// + /// Gets the sector number (one-based). + /// + public int Sector { get; } + + /// + /// Determines if this object is equivalent to another. + /// + /// The object to test against. + /// true if the is equivalent, else false. + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != GetType()) + { + return false; + } + + ChsAddress other = (ChsAddress)obj; + + return Cylinder == other.Cylinder && Head == other.Head && Sector == other.Sector; + } + + /// + /// Calculates the hash code for this object. + /// + /// The hash code. + public override int GetHashCode() + { + return Cylinder.GetHashCode() ^ Head.GetHashCode() ^ Sector.GetHashCode(); + } + + /// + /// Gets a string representation of this object, in the form (C/H/S). + /// + /// The string representation. + public override string ToString() + { + return "(" + Cylinder + "/" + Head + "/" + Sector + ")"; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/ClusterMap.cs b/DiscUtils/Core/ClusterMap.cs new file mode 100644 index 0000000..e501f64 --- /dev/null +++ b/DiscUtils/Core/ClusterMap.cs @@ -0,0 +1,75 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; + +namespace DiscUtils +{ + /// + /// Class that identifies the role of each cluster in a file system. + /// + public sealed class ClusterMap + { + private readonly object[] _clusterToFileId; + private readonly ClusterRoles[] _clusterToRole; + private readonly Dictionary _fileIdToPaths; + + public ClusterMap(ClusterRoles[] clusterToRole, object[] clusterToFileId, + Dictionary fileIdToPaths) + { + _clusterToRole = clusterToRole; + _clusterToFileId = clusterToFileId; + _fileIdToPaths = fileIdToPaths; + } + + /// + /// Gets the role of a cluster within the file system. + /// + /// The cluster to inspect. + /// The clusters role (or roles). + public ClusterRoles GetRole(long cluster) + { + if (_clusterToRole == null || _clusterToRole.Length < cluster) + { + return ClusterRoles.None; + } + return _clusterToRole[cluster]; + } + + /// + /// Converts a cluster to a list of file names. + /// + /// The cluster to inspect. + /// A list of paths that map to the cluster. + /// A list is returned because on file systems with the notion of + /// hard links, a cluster may correspond to multiple directory entries. + public string[] ClusterToPaths(long cluster) + { + if ((GetRole(cluster) & (ClusterRoles.DataFile | ClusterRoles.SystemFile)) != 0) + { + object fileId = _clusterToFileId[cluster]; + return _fileIdToPaths[fileId]; + } + return new string[0]; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/ClusterRoles.cs b/DiscUtils/Core/ClusterRoles.cs new file mode 100644 index 0000000..130a290 --- /dev/null +++ b/DiscUtils/Core/ClusterRoles.cs @@ -0,0 +1,50 @@ +using System; + +namespace DiscUtils +{ + /// + /// Enumeration of possible cluster roles. + /// + /// A cluster may be in more than one role. + [Flags] + public enum ClusterRoles + { + /// + /// Unknown, or unspecified role. + /// + None = 0x00, + + /// + /// Cluster is free. + /// + Free = 0x01, + + /// + /// Cluster is in use by a normal file. + /// + DataFile = 0x02, + + /// + /// Cluster is in use by a system file. + /// + /// This isn't a file marked with the 'system' attribute, + /// rather files that form part of the file system namespace but also + /// form part of the file system meta-data. + SystemFile = 0x04, + + /// + /// Cluster is in use for meta-data. + /// + Metadata = 0x08, + + /// + /// Cluster contains the boot region. + /// + BootArea = 0x10, + + /// + /// Cluster is marked bad. + /// + Bad = 0x20 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/Adler32.cs b/DiscUtils/Core/Compression/Adler32.cs new file mode 100644 index 0000000..630afc1 --- /dev/null +++ b/DiscUtils/Core/Compression/Adler32.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Compression +{ + /// + /// Implementation of the Adler-32 checksum algorithm. + /// + public class Adler32 + { + private uint _a; + private uint _b; + + /// + /// Initializes a new instance of the Adler32 class. + /// + public Adler32() + { + _a = 1; + } + + /// + /// Gets the checksum of all data processed so far. + /// + public int Value + { + get { return (int)(_b << 16 | _a); } + } + + /// + /// Provides data that should be checksummed. + /// + /// Buffer containing the data to checksum. + /// Offset of the first byte to checksum. + /// The number of bytes to checksum. + /// + /// Call this method repeatedly until all checksummed + /// data has been processed. + /// + public void Process(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentException("Offset outside of array bounds", nameof(offset)); + } + + if (count < 0 || offset + count > buffer.Length) + { + throw new ArgumentException("Array index out of bounds", nameof(count)); + } + + int processed = 0; + while (processed < count) + { + int innerEnd = Math.Min(count, processed + 2000); + while (processed < innerEnd) + { + _a += buffer[processed++]; + _b += _a; + } + + _a %= 65521; + _b %= 65521; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/BZip2BlockDecoder.cs b/DiscUtils/Core/Compression/BZip2BlockDecoder.cs new file mode 100644 index 0000000..188a4a5 --- /dev/null +++ b/DiscUtils/Core/Compression/BZip2BlockDecoder.cs @@ -0,0 +1,141 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +using System.IO; + +namespace DiscUtils.Compression +{ + internal class BZip2BlockDecoder + { + private readonly InverseBurrowsWheeler _inverseBurrowsWheeler; + + public BZip2BlockDecoder(int blockSize) + { + _inverseBurrowsWheeler = new InverseBurrowsWheeler(blockSize); + } + + public uint Crc { get; private set; } + + public int Process(BitStream bitstream, byte[] outputBuffer, int outputBufferOffset) + { + Crc = 0; + for (int i = 0; i < 4; ++i) + { + Crc = (Crc << 8) | bitstream.Read(8); + } + + bool rand = bitstream.Read(1) != 0; + int origPtr = (int)bitstream.Read(24); + + int thisBlockSize = ReadBuffer(bitstream, outputBuffer, outputBufferOffset); + + _inverseBurrowsWheeler.OriginalIndex = origPtr; + _inverseBurrowsWheeler.Process(outputBuffer, outputBufferOffset, thisBlockSize, outputBuffer, + outputBufferOffset); + + if (rand) + { + BZip2Randomizer randomizer = new BZip2Randomizer(); + randomizer.Process(outputBuffer, outputBufferOffset, thisBlockSize, outputBuffer, outputBufferOffset); + } + + return thisBlockSize; + } + + private static int ReadBuffer(BitStream bitstream, byte[] buffer, int offset) + { + // The MTF state + int numInUse = 0; + MoveToFront moveFrontTransform = new MoveToFront(); + bool[] inUseGroups = new bool[16]; + for (int i = 0; i < 16; ++i) + { + inUseGroups[i] = bitstream.Read(1) != 0; + } + + for (int i = 0; i < 256; ++i) + { + if (inUseGroups[i / 16]) + { + if (bitstream.Read(1) != 0) + { + moveFrontTransform.Set(numInUse, (byte)i); + numInUse++; + } + } + } + + // Initialize 'virtual' Huffman tree from bitstream + BZip2CombinedHuffmanTrees huffmanTree = new BZip2CombinedHuffmanTrees(bitstream, numInUse + 2); + + // Main loop reading data + int readBytes = 0; + while (true) + { + uint symbol = huffmanTree.NextSymbol(); + + if (symbol < 2) + { + // RLE, with length stored in a binary-style format + uint runLength = 0; + int bitShift = 0; + while (symbol < 2) + { + runLength += (symbol + 1) << bitShift; + bitShift++; + + symbol = huffmanTree.NextSymbol(); + } + + byte b = moveFrontTransform.Head; + while (runLength > 0) + { + buffer[offset + readBytes] = b; + ++readBytes; + --runLength; + } + } + + if (symbol <= numInUse) + { + // Single byte + byte b = moveFrontTransform.GetAndMove((int)symbol - 1); + buffer[offset + readBytes] = b; + ++readBytes; + } + else if (symbol == numInUse + 1) + { + // End of block marker + return readBytes; + } + else + { + throw new InvalidDataException("Invalid symbol from Huffman table"); + } + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/BZip2CombinedHuffmanTrees.cs b/DiscUtils/Core/Compression/BZip2CombinedHuffmanTrees.cs new file mode 100644 index 0000000..4af2295 --- /dev/null +++ b/DiscUtils/Core/Compression/BZip2CombinedHuffmanTrees.cs @@ -0,0 +1,134 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +using System.IO; + +namespace DiscUtils.Compression +{ + /// + /// Represents scheme used by BZip2 where multiple Huffman trees are used as a + /// virtual Huffman tree, with a logical selector every 50 bits in the bit stream. + /// + internal class BZip2CombinedHuffmanTrees + { + private HuffmanTree _activeTree; + private readonly BitStream _bitstream; + private int _nextSelector; + private byte[] _selectors; + private int _symbolsToNextSelector; + private HuffmanTree[] _trees; + + public BZip2CombinedHuffmanTrees(BitStream bitstream, int maxSymbols) + { + _bitstream = bitstream; + + Initialize(maxSymbols); + } + + public uint NextSymbol() + { + if (_symbolsToNextSelector == 0) + { + _symbolsToNextSelector = 50; + _activeTree = _trees[_selectors[_nextSelector]]; + _nextSelector++; + } + + _symbolsToNextSelector--; + + return _activeTree.NextSymbol(_bitstream); + } + + private void Initialize(int maxSymbols) + { + int numTrees = (int)_bitstream.Read(3); + if (numTrees < 2 || numTrees > 6) + { + throw new InvalidDataException("Invalid number of tables"); + } + + int numSelectors = (int)_bitstream.Read(15); + if (numSelectors < 1) + { + throw new InvalidDataException("Invalid number of selectors"); + } + + _selectors = new byte[numSelectors]; + MoveToFront mtf = new MoveToFront(numTrees, true); + for (int i = 0; i < numSelectors; ++i) + { + _selectors[i] = mtf.GetAndMove(CountSetBits(numTrees)); + } + + _trees = new HuffmanTree[numTrees]; + for (int t = 0; t < numTrees; ++t) + { + uint[] lengths = new uint[maxSymbols]; + + uint len = _bitstream.Read(5); + for (int i = 0; i < maxSymbols; ++i) + { + if (len < 1 || len > 20) + { + throw new InvalidDataException("Invalid length constructing Huffman tree"); + } + + while (_bitstream.Read(1) != 0) + { + len = _bitstream.Read(1) == 0 ? len + 1 : len - 1; + + if (len < 1 || len > 20) + { + throw new InvalidDataException("Invalid length constructing Huffman tree"); + } + } + + lengths[i] = len; + } + + _trees[t] = new HuffmanTree(lengths); + } + + _symbolsToNextSelector = 0; + _nextSelector = 0; + } + + private byte CountSetBits(int max) + { + byte val = 0; + while (_bitstream.Read(1) != 0) + { + val++; + if (val >= max) + { + throw new InvalidDataException("Exceeded max number of consecutive bits"); + } + } + + return val; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/BZip2DecoderStream.cs b/DiscUtils/Core/Compression/BZip2DecoderStream.cs new file mode 100644 index 0000000..0d22745 --- /dev/null +++ b/DiscUtils/Core/Compression/BZip2DecoderStream.cs @@ -0,0 +1,348 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +using System; +using System.IO; +using DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils.Compression +{ + /// + /// Implementation of a BZip2 decoder. + /// + public sealed class BZip2DecoderStream : Stream + { + private readonly BitStream _bitstream; + + private readonly byte[] _blockBuffer; + private uint _blockCrc; + private readonly BZip2BlockDecoder _blockDecoder; + private Crc32 _calcBlockCrc; + private uint _calcCompoundCrc; + private uint _compoundCrc; + private Stream _compressedStream; + + private bool _eof; + private readonly Ownership _ownsCompressed; + private long _position; + private BZip2RleStream _rleStream; + + /// + /// Initializes a new instance of the BZip2DecoderStream class. + /// + /// The compressed input stream. + /// Whether ownership of stream passes to the new instance. + public BZip2DecoderStream(Stream stream, Ownership ownsStream) + { + _compressedStream = stream; + _ownsCompressed = ownsStream; + + _bitstream = new BigEndianBitStream(new BufferedStream(stream)); + + // The Magic BZh + byte[] magic = new byte[3]; + magic[0] = (byte)_bitstream.Read(8); + magic[1] = (byte)_bitstream.Read(8); + magic[2] = (byte)_bitstream.Read(8); + if (magic[0] != 0x42 || magic[1] != 0x5A || magic[2] != 0x68) + { + throw new InvalidDataException("Bad magic at start of stream"); + } + + // The size of the decompression blocks in multiples of 100,000 + int blockSize = (int)_bitstream.Read(8) - 0x30; + if (blockSize < 1 || blockSize > 9) + { + throw new InvalidDataException("Unexpected block size in header: " + blockSize); + } + + blockSize *= 100000; + + _rleStream = new BZip2RleStream(); + _blockDecoder = new BZip2BlockDecoder(blockSize); + _blockBuffer = new byte[blockSize]; + + if (ReadBlock() == 0) + { + _eof = true; + } + } + + /// + /// Gets an indication of whether read access is permitted. + /// + public override bool CanRead + { + get { return true; } + } + + /// + /// Gets an indication of whether seeking is permitted. + /// + public override bool CanSeek + { + get { return false; } + } + + /// + /// Gets an indication of whether write access is permitted. + /// + public override bool CanWrite + { + get { return false; } + } + + /// + /// Gets the length of the stream (the capacity of the underlying buffer). + /// + public override long Length + { + get { throw new NotSupportedException(); } + } + + /// + /// Gets and sets the current position within the stream. + /// + public override long Position + { + get { return _position; } + set { throw new NotSupportedException(); } + } + + /// + /// Flushes all data to the underlying storage. + /// + public override void Flush() + { + throw new NotSupportedException(); + } + + /// + /// Reads a number of bytes from the stream. + /// + /// The destination buffer. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (buffer.Length < offset + count) + { + throw new ArgumentException("Buffer smaller than declared"); + } + + if (offset < 0) + { + throw new ArgumentException("Offset less than zero", nameof(offset)); + } + + if (count < 0) + { + throw new ArgumentException("Count less than zero", nameof(count)); + } + + if (_eof) + { + throw new IOException("Attempt to read beyond end of stream"); + } + + if (count == 0) + { + return 0; + } + + int numRead = _rleStream.Read(buffer, offset, count); + if (numRead == 0) + { + // If there was an existing block, check it's crc. + if (_calcBlockCrc != null) + { + if (_blockCrc != _calcBlockCrc.Value) + { + throw new InvalidDataException("Decompression failed - block CRC mismatch"); + } + + _calcCompoundCrc = ((_calcCompoundCrc << 1) | (_calcCompoundCrc >> 31)) ^ _blockCrc; + } + + // Read a new block (if any), if none - check the overall CRC before returning + if (ReadBlock() == 0) + { + _eof = true; + if (_calcCompoundCrc != _compoundCrc) + { + throw new InvalidDataException("Decompression failed - compound CRC"); + } + + return 0; + } + + numRead = _rleStream.Read(buffer, offset, count); + } + + _calcBlockCrc.Process(buffer, offset, numRead); + + // Pre-read next block, so a client that knows the decompressed length will still + // have the overall CRC calculated. + if (_rleStream.AtEof) + { + // If there was an existing block, check it's crc. + if (_calcBlockCrc != null) + { + if (_blockCrc != _calcBlockCrc.Value) + { + throw new InvalidDataException("Decompression failed - block CRC mismatch"); + } + } + + _calcCompoundCrc = ((_calcCompoundCrc << 1) | (_calcCompoundCrc >> 31)) ^ _blockCrc; + if (ReadBlock() == 0) + { + _eof = true; + if (_calcCompoundCrc != _compoundCrc) + { + throw new InvalidDataException("Decompression failed - compound CRC mismatch"); + } + + return numRead; + } + } + + _position += numRead; + return numRead; + } + + /// + /// Changes the current stream position. + /// + /// The origin-relative stream position. + /// The origin for the stream position. + /// The new stream position. + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + /// Sets the length of the stream (the underlying buffer's capacity). + /// + /// The new length of the stream. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Writes a buffer to the stream. + /// + /// The buffer to write. + /// The starting offset within buffer. + /// The number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + /// + /// Releases underlying resources. + /// + /// Whether this method is called from Dispose. + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_compressedStream != null && _ownsCompressed == Ownership.Dispose) + { + _compressedStream.Dispose(); + } + + _compressedStream = null; + + if (_rleStream != null) + { + _rleStream.Dispose(); + _rleStream = null; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + private int ReadBlock() + { + ulong marker = ReadMarker(); + if (marker == 0x314159265359) + { + int blockSize = _blockDecoder.Process(_bitstream, _blockBuffer, 0); + _rleStream.Reset(_blockBuffer, 0, blockSize); + _blockCrc = _blockDecoder.Crc; + _calcBlockCrc = new Crc32BigEndian(Crc32Algorithm.Common); + return blockSize; + } + if (marker == 0x177245385090) + { + _compoundCrc = ReadUint(); + return 0; + } + throw new InvalidDataException("Found invalid marker in stream"); + } + + private uint ReadUint() + { + uint val = 0; + + for (int i = 0; i < 4; ++i) + { + val = (val << 8) | _bitstream.Read(8); + } + + return val; + } + + private ulong ReadMarker() + { + ulong marker = 0; + + for (int i = 0; i < 6; ++i) + { + marker = (marker << 8) | _bitstream.Read(8); + } + + return marker; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/BZip2Randomizer.cs b/DiscUtils/Core/Compression/BZip2Randomizer.cs new file mode 100644 index 0000000..1d72dc7 --- /dev/null +++ b/DiscUtils/Core/Compression/BZip2Randomizer.cs @@ -0,0 +1,124 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +using System; + +namespace DiscUtils.Compression +{ + internal class BZip2Randomizer : DataBlockTransform + { + private static readonly int[] RandomVals = + { + 619, 720, 127, 481, 931, 816, 813, 233, 566, 247, + 985, 724, 205, 454, 863, 491, 741, 242, 949, 214, + 733, 859, 335, 708, 621, 574, 73, 654, 730, 472, + 419, 436, 278, 496, 867, 210, 399, 680, 480, 51, + 878, 465, 811, 169, 869, 675, 611, 697, 867, 561, + 862, 687, 507, 283, 482, 129, 807, 591, 733, 623, + 150, 238, 59, 379, 684, 877, 625, 169, 643, 105, + 170, 607, 520, 932, 727, 476, 693, 425, 174, 647, + 73, 122, 335, 530, 442, 853, 695, 249, 445, 515, + 909, 545, 703, 919, 874, 474, 882, 500, 594, 612, + 641, 801, 220, 162, 819, 984, 589, 513, 495, 799, + 161, 604, 958, 533, 221, 400, 386, 867, 600, 782, + 382, 596, 414, 171, 516, 375, 682, 485, 911, 276, + 98, 553, 163, 354, 666, 933, 424, 341, 533, 870, + 227, 730, 475, 186, 263, 647, 537, 686, 600, 224, + 469, 68, 770, 919, 190, 373, 294, 822, 808, 206, + 184, 943, 795, 384, 383, 461, 404, 758, 839, 887, + 715, 67, 618, 276, 204, 918, 873, 777, 604, 560, + 951, 160, 578, 722, 79, 804, 96, 409, 713, 940, + 652, 934, 970, 447, 318, 353, 859, 672, 112, 785, + 645, 863, 803, 350, 139, 93, 354, 99, 820, 908, + 609, 772, 154, 274, 580, 184, 79, 626, 630, 742, + 653, 282, 762, 623, 680, 81, 927, 626, 789, 125, + 411, 521, 938, 300, 821, 78, 343, 175, 128, 250, + 170, 774, 972, 275, 999, 639, 495, 78, 352, 126, + 857, 956, 358, 619, 580, 124, 737, 594, 701, 612, + 669, 112, 134, 694, 363, 992, 809, 743, 168, 974, + 944, 375, 748, 52, 600, 747, 642, 182, 862, 81, + 344, 805, 988, 739, 511, 655, 814, 334, 249, 515, + 897, 955, 664, 981, 649, 113, 974, 459, 893, 228, + 433, 837, 553, 268, 926, 240, 102, 654, 459, 51, + 686, 754, 806, 760, 493, 403, 415, 394, 687, 700, + 946, 670, 656, 610, 738, 392, 760, 799, 887, 653, + 978, 321, 576, 617, 626, 502, 894, 679, 243, 440, + 680, 879, 194, 572, 640, 724, 926, 56, 204, 700, + 707, 151, 457, 449, 797, 195, 791, 558, 945, 679, + 297, 59, 87, 824, 713, 663, 412, 693, 342, 606, + 134, 108, 571, 364, 631, 212, 174, 643, 304, 329, + 343, 97, 430, 751, 497, 314, 983, 374, 822, 928, + 140, 206, 73, 263, 980, 736, 876, 478, 430, 305, + 170, 514, 364, 692, 829, 82, 855, 953, 676, 246, + 369, 970, 294, 750, 807, 827, 150, 790, 288, 923, + 804, 378, 215, 828, 592, 281, 565, 555, 710, 82, + 896, 831, 547, 261, 524, 462, 293, 465, 502, 56, + 661, 821, 976, 991, 658, 869, 905, 758, 745, 193, + 768, 550, 608, 933, 378, 286, 215, 979, 792, 961, + 61, 688, 793, 644, 986, 403, 106, 366, 905, 644, + 372, 567, 466, 434, 645, 210, 389, 550, 919, 135, + 780, 773, 635, 389, 707, 100, 626, 958, 165, 504, + 920, 176, 193, 713, 857, 265, 203, 50, 668, 108, + 645, 990, 626, 197, 510, 357, 358, 850, 858, 364, + 936, 638 + }; + + protected override bool BuffersMustNotOverlap + { + get { return false; } + } + + protected override int DoProcess(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + { + if (input != output || inputOffset != outputOffset) + { + Array.Copy(input, inputOffset, output, outputOffset, inputCount); + } + + int randIndex = 1; + int nextByte = RandomVals[0] - 2; + + while (nextByte < inputCount) + { + output[nextByte] ^= 1; + nextByte += RandomVals[randIndex++]; + randIndex &= 0x1FF; + } + + return inputCount; + } + + protected override int MaxOutputCount(int inputCount) + { + return inputCount; + } + + protected override int MinOutputCount(int inputCount) + { + return inputCount; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/BZip2RleStream.cs b/DiscUtils/Core/Compression/BZip2RleStream.cs new file mode 100644 index 0000000..a400bd3 --- /dev/null +++ b/DiscUtils/Core/Compression/BZip2RleStream.cs @@ -0,0 +1,157 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +// +// Based on "libbzip2", Copyright (C) 1996-2007 Julian R Seward. +// + +using System; +using System.IO; + +namespace DiscUtils.Compression +{ + internal class BZip2RleStream : Stream + { + private byte[] _blockBuffer; + private int _blockOffset; + private int _blockRemaining; + private byte _lastByte; + + private int _numSame; + private long _position; + private int _runBytesOutstanding; + + public bool AtEof + { + get { return _runBytesOutstanding == 0 && _blockRemaining == 0; } + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return false; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { throw new NotSupportedException(); } + } + + public override long Position + { + get { return _position; } + set { throw new NotSupportedException(); } + } + + public void Reset(byte[] buffer, int offset, int count) + { + _position = 0; + _blockBuffer = buffer; + _blockOffset = offset; + _blockRemaining = count; + _numSame = -1; + _lastByte = 0; + _runBytesOutstanding = 0; + } + + public override void Flush() + { + throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int numRead = 0; + + while (numRead < count && _runBytesOutstanding > 0) + { + int runCount = Math.Min(_runBytesOutstanding, count); + for (int i = 0; i < runCount; ++i) + { + buffer[offset + numRead] = _lastByte; + } + + _runBytesOutstanding -= runCount; + numRead += runCount; + } + + while (numRead < count && _blockRemaining > 0) + { + byte b = _blockBuffer[_blockOffset]; + ++_blockOffset; + --_blockRemaining; + + if (_numSame == 4) + { + int runCount = Math.Min(b, count - numRead); + for (int i = 0; i < runCount; ++i) + { + buffer[offset + numRead] = _lastByte; + numRead++; + } + + _runBytesOutstanding = b - runCount; + _numSame = 0; + } + else + { + if (b != _lastByte || _numSame <= 0) + { + _lastByte = b; + _numSame = 0; + } + + buffer[offset + numRead] = b; + numRead++; + _numSame++; + } + } + + _position += numRead; + return numRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/BigEndianBitStream.cs b/DiscUtils/Core/Compression/BigEndianBitStream.cs new file mode 100644 index 0000000..21c962e --- /dev/null +++ b/DiscUtils/Core/Compression/BigEndianBitStream.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Compression +{ + /// + /// Converts a byte stream into a bit stream. + /// + internal class BigEndianBitStream : BitStream + { + private uint _buffer; + private int _bufferAvailable; + private readonly Stream _byteStream; + + private readonly byte[] _readBuffer = new byte[2]; + + public BigEndianBitStream(Stream byteStream) + { + _byteStream = byteStream; + } + + public override int MaxReadAhead + { + get { return 16; } + } + + public override uint Read(int count) + { + if (count > 16) + { + uint result = Read(16) << (count - 16); + return result | Read(count - 16); + } + + EnsureBufferFilled(); + + _bufferAvailable -= count; + + uint mask = (uint)((1 << count) - 1); + + return (_buffer >> _bufferAvailable) & mask; + } + + public override uint Peek(int count) + { + EnsureBufferFilled(); + + uint mask = (uint)((1 << count) - 1); + + return (_buffer >> (_bufferAvailable - count)) & mask; + } + + public override void Consume(int count) + { + EnsureBufferFilled(); + + _bufferAvailable -= count; + } + + private void EnsureBufferFilled() + { + if (_bufferAvailable < 16) + { + _readBuffer[0] = 0; + _readBuffer[1] = 0; + _byteStream.Read(_readBuffer, 0, 2); + + _buffer = _buffer << 16 | (uint)(_readBuffer[0] << 8) | _readBuffer[1]; + _bufferAvailable += 16; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/BitStream.cs b/DiscUtils/Core/Compression/BitStream.cs new file mode 100644 index 0000000..89b0834 --- /dev/null +++ b/DiscUtils/Core/Compression/BitStream.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Compression +{ + /// + /// Base class for bit streams. + /// + /// + /// The rules for conversion of a byte stream to a bit stream vary + /// between implementations. + /// + internal abstract class BitStream + { + /// + /// Gets the maximum number of bits that can be peeked on the stream. + /// + public abstract int MaxReadAhead { get; } + + /// + /// Reads bits from the stream. + /// + /// The number of bits to read. + /// The bits as a UInt32. + public abstract uint Read(int count); + + /// + /// Queries data from the stream. + /// + /// The number of bits to query. + /// The bits as a UInt32. + /// This method does not consume the bits (i.e. move the file pointer). + public abstract uint Peek(int count); + + /// + /// Consumes bits from the stream without returning them. + /// + /// The number of bits to consume. + public abstract void Consume(int count); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/BlockCompressor.cs b/DiscUtils/Core/Compression/BlockCompressor.cs new file mode 100644 index 0000000..5bb1f39 --- /dev/null +++ b/DiscUtils/Core/Compression/BlockCompressor.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Compression +{ + /// + /// Base class for block compression algorithms. + /// + public abstract class BlockCompressor + { + /// + /// Gets or sets the block size parameter to the algorithm. + /// + /// + /// Some algorithms may use this to control both compression and decompression, others may + /// only use it to control compression. Some may ignore it entirely. + /// + public int BlockSize { get; set; } + + /// + /// Compresses some data. + /// + /// The uncompressed input. + /// Offset of the input data in source. + /// The amount of uncompressed data. + /// The destination for the output compressed data. + /// Offset for the output data in compressed. + /// The maximum size of the compressed data on input, and the actual size on output. + /// Indication of success, or indication the data could not compress into the requested space. + public abstract CompressionResult Compress(byte[] source, int sourceOffset, int sourceLength, byte[] compressed, + int compressedOffset, ref int compressedLength); + + /// + /// Decompresses some data. + /// + /// The compressed input. + /// Offset of the input data in source. + /// The amount of compressed data. + /// The destination for the output decompressed data. + /// Offset for the output data in decompressed. + /// The amount of decompressed data. + public abstract int Decompress(byte[] source, int sourceOffset, int sourceLength, byte[] decompressed, + int decompressedOffset); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/CompressionResult.cs b/DiscUtils/Core/Compression/CompressionResult.cs new file mode 100644 index 0000000..ee1c1e7 --- /dev/null +++ b/DiscUtils/Core/Compression/CompressionResult.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Compression +{ + /// + /// Possible results of attempting to compress data. + /// + /// + /// A compression routine may return Compressed, even if the data + /// was 'all zeros' or increased in size. The AllZeros and Incompressible + /// values are for algorithms that include special detection for these cases. + /// + public enum CompressionResult + { + /// + /// The data compressed succesfully. + /// + Compressed, + + /// + /// The data was all-zero's. + /// + AllZeros, + + /// + /// The data was incompressible (could not fit into destination buffer). + /// + Incompressible + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/DataBlockTransform.cs b/DiscUtils/Core/Compression/DataBlockTransform.cs new file mode 100644 index 0000000..946dbbe --- /dev/null +++ b/DiscUtils/Core/Compression/DataBlockTransform.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; + +namespace DiscUtils.Compression +{ + internal abstract class DataBlockTransform + { + protected abstract bool BuffersMustNotOverlap { get; } + + public int Process(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + { + if (output.Length < outputOffset + (long)MinOutputCount(inputCount)) + { + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "Output buffer to small, must be at least {0} bytes may need to be {1} bytes", + MinOutputCount(inputCount), + MaxOutputCount(inputCount))); + } + + if (BuffersMustNotOverlap) + { + int maxOut = MaxOutputCount(inputCount); + + if (input == output + && (inputOffset + (long)inputCount > outputOffset) + && (inputOffset <= outputOffset + (long)maxOut)) + { + byte[] tempBuffer = new byte[maxOut]; + + int outCount = DoProcess(input, inputOffset, inputCount, tempBuffer, 0); + Array.Copy(tempBuffer, 0, output, outputOffset, outCount); + + return outCount; + } + } + + return DoProcess(input, inputOffset, inputCount, output, outputOffset); + } + + protected abstract int DoProcess(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset); + + protected abstract int MaxOutputCount(int inputCount); + + protected abstract int MinOutputCount(int inputCount); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/HuffmanTree.cs b/DiscUtils/Core/Compression/HuffmanTree.cs new file mode 100644 index 0000000..e0fc50b --- /dev/null +++ b/DiscUtils/Core/Compression/HuffmanTree.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Compression +{ + /// + /// A canonical Huffman tree implementation. + /// + /// + /// A lookup table is created that will take any bit sequence (max tree depth in length), + /// indicating the output symbol. In WIM files, in practice, no chunk exceeds 32768 bytes + /// in length, so we often end up generating a bigger lookup table than the data it's + /// encoding. This makes for exceptionally fast symbol lookups O(1), but is inefficient + /// overall. + /// + internal sealed class HuffmanTree + { + private readonly uint[] _buffer; + private readonly int _numBits; // Max bits per symbol + private readonly int _numSymbols; // Max symbols + + public HuffmanTree(uint[] lengths) + { + Lengths = lengths; + _numSymbols = lengths.Length; + + uint maxLength = 0; + for (int i = 0; i < Lengths.Length; ++i) + { + if (Lengths[i] > maxLength) + { + maxLength = Lengths[i]; + } + } + + _numBits = (int)maxLength; + _buffer = new uint[1 << _numBits]; + + Build(); + } + + public uint[] Lengths { get; } + + public uint NextSymbol(BitStream bitStream) + { + uint symbol = _buffer[bitStream.Peek(_numBits)]; + + // We may have over-read, reset bitstream position + bitStream.Consume((int)Lengths[symbol]); + + return symbol; + } + + private void Build() + { + int position = 0; + + // For each bit-length... + for (int i = 1; i <= _numBits; ++i) + { + // Check each symbol + for (uint symbol = 0; symbol < _numSymbols; ++symbol) + { + if (Lengths[symbol] == i) + { + int numToFill = 1 << (_numBits - i); + for (int n = 0; n < numToFill; ++n) + { + _buffer[position + n] = symbol; + } + + position += numToFill; + } + } + } + + for (int i = position; i < _buffer.Length; ++i) + { + _buffer[i] = uint.MaxValue; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/InverseBurrowsWheeler.cs b/DiscUtils/Core/Compression/InverseBurrowsWheeler.cs new file mode 100644 index 0000000..f49804a --- /dev/null +++ b/DiscUtils/Core/Compression/InverseBurrowsWheeler.cs @@ -0,0 +1,97 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Compression +{ + internal sealed class InverseBurrowsWheeler : DataBlockTransform + { + private readonly int[] _nextPos; + private readonly int[] _pointers; + + public InverseBurrowsWheeler(int bufferSize) + { + _pointers = new int[bufferSize]; + _nextPos = new int[256]; + } + + protected override bool BuffersMustNotOverlap + { + get { return true; } + } + + public int OriginalIndex { get; set; } + + protected override int DoProcess(byte[] input, int inputOffset, int inputCount, byte[] output, int outputOffset) + { + int outputCount = inputCount; + + // First find the frequency of each value + Array.Clear(_nextPos, 0, _nextPos.Length); + for (int i = inputOffset; i < inputOffset + inputCount; ++i) + { + _nextPos[input[i]]++; + } + + // We know they're 'sorted' in the first column, so now can figure + // out the position of the first instance of each. + int sum = 0; + for (int i = 0; i < 256; ++i) + { + int tempSum = sum; + sum += _nextPos[i]; + _nextPos[i] = tempSum; + } + + // For each value in the final column, put a pointer to to the + // 'next' character in the first (sorted) column. + for (int i = 0; i < inputCount; ++i) + { + _pointers[_nextPos[input[inputOffset + i]]++] = i; + } + + // The 'next' character after the end of the original string is the + // first character of the original string. + int focus = _pointers[OriginalIndex]; + + // We can now just walk the pointers to reconstruct the original string + for (int i = 0; i < outputCount; ++i) + { + output[outputOffset + i] = input[inputOffset + focus]; + focus = _pointers[focus]; + } + + return outputCount; + } + + protected override int MaxOutputCount(int inputCount) + { + return inputCount; + } + + protected override int MinOutputCount(int inputCount) + { + return inputCount; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/MoveToFront.cs b/DiscUtils/Core/Compression/MoveToFront.cs new file mode 100644 index 0000000..cf34bbd --- /dev/null +++ b/DiscUtils/Core/Compression/MoveToFront.cs @@ -0,0 +1,68 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Compression +{ + internal class MoveToFront + { + private readonly byte[] _buffer; + + public MoveToFront() + : this(256, false) {} + + public MoveToFront(int size, bool autoInit) + { + _buffer = new byte[size]; + + if (autoInit) + { + for (byte i = 0; i < size; ++i) + { + _buffer[i] = i; + } + } + } + + public byte Head + { + get { return _buffer[0]; } + } + + public void Set(int pos, byte val) + { + _buffer[pos] = val; + } + + public byte GetAndMove(int pos) + { + byte val = _buffer[pos]; + + for (int i = pos; i > 0; --i) + { + _buffer[i] = _buffer[i - 1]; + } + + _buffer[0] = val; + return val; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/SizedDeflateStream.cs b/DiscUtils/Core/Compression/SizedDeflateStream.cs new file mode 100644 index 0000000..c367da5 --- /dev/null +++ b/DiscUtils/Core/Compression/SizedDeflateStream.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2014, Quamotion +// +// 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.IO.Compression; + +namespace DiscUtils.Compression +{ + internal class SizedDeflateStream : DeflateStream + { + private readonly int _length; + private int _position; + + public SizedDeflateStream(Stream stream, CompressionMode mode, bool leaveOpen, int length) + : base(stream, mode, leaveOpen) + { + _length = length; + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + set + { + if (value != Position) + { + throw new NotImplementedException(); + } + } + } + + public override int Read(byte[] array, int offset, int count) + { + int read = base.Read(array, offset, count); + _position += read; + return read; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/ZlibBuffer.cs b/DiscUtils/Core/Compression/ZlibBuffer.cs new file mode 100644 index 0000000..451f757 --- /dev/null +++ b/DiscUtils/Core/Compression/ZlibBuffer.cs @@ -0,0 +1,86 @@ +// +// Copyright (c) 2014, Quamotion +// +// 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.IO; +using DiscUtils.Streams; +using Buffer=DiscUtils.Streams.Buffer; + +namespace DiscUtils.Compression +{ + internal class ZlibBuffer : Buffer + { + private Ownership _ownership; + private readonly Stream _stream; + private int position; + + public ZlibBuffer(Stream stream, Ownership ownership) + { + _stream = stream; + _ownership = ownership; + position = 0; + } + + public override bool CanRead + { + get { return _stream.CanRead; } + } + + public override bool CanWrite + { + get { return _stream.CanWrite; } + } + + public override long Capacity + { + get { return _stream.Length; } + } + + public override int Read(long pos, byte[] buffer, int offset, int count) + { + if (pos != position) + { + throw new NotSupportedException(); + } + + int read = _stream.Read(buffer, offset, count); + position += read; + return read; + } + + public override void Write(long pos, byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override void SetCapacity(long value) + { + throw new NotImplementedException(); + } + + public override IEnumerable GetExtentsInRange(long start, long count) + { + yield return new StreamExtent(0, _stream.Length); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Compression/ZlibStream.cs b/DiscUtils/Core/Compression/ZlibStream.cs new file mode 100644 index 0000000..c69a171 --- /dev/null +++ b/DiscUtils/Core/Compression/ZlibStream.cs @@ -0,0 +1,240 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO.Compression; +using DiscUtils.Streams; + +namespace DiscUtils.Compression +{ + /// + /// Implementation of the Zlib compression algorithm. + /// + /// Only decompression is currently implemented. + public class ZlibStream : Stream + { + private readonly Adler32 _adler32; + private readonly DeflateStream _deflateStream; + private readonly CompressionMode _mode; + private readonly Stream _stream; + + /// + /// Initializes a new instance of the ZlibStream class. + /// + /// The stream to compress of decompress. + /// Whether to compress or decompress. + /// Whether closing this stream should leave stream open. + public ZlibStream(Stream stream, CompressionMode mode, bool leaveOpen) + { + _stream = stream; + _mode = mode; + + if (mode == CompressionMode.Decompress) + { + // We just sanity check against expected header values... + byte[] headerBuffer = StreamUtilities.ReadExact(stream, 2); + ushort header = EndianUtilities.ToUInt16BigEndian(headerBuffer, 0); + + if (header % 31 != 0) + { + throw new IOException("Invalid Zlib header found"); + } + + if ((header & 0x0F00) != 8 << 8) + { + throw new NotSupportedException("Zlib compression not using DEFLATE algorithm"); + } + + if ((header & 0x0020) != 0) + { + throw new NotSupportedException("Zlib compression using preset dictionary"); + } + } + else + { + ushort header = + (8 << 8) // DEFLATE + | (7 << 12) // 32K window size + | 0x80; // Default algorithm + header |= (ushort)(31 - header % 31); + + byte[] headerBuffer = new byte[2]; + EndianUtilities.WriteBytesBigEndian(header, headerBuffer, 0); + stream.Write(headerBuffer, 0, 2); + } + + _deflateStream = new DeflateStream(stream, mode, leaveOpen); + _adler32 = new Adler32(); + } + + /// + /// Gets whether the stream can be read. + /// + public override bool CanRead + { + get { return _deflateStream.CanRead; } + } + + /// + /// Gets whether the stream pointer can be changed. + /// + public override bool CanSeek + { + get { return false; } + } + + /// + /// Gets whether the stream can be written to. + /// + public override bool CanWrite + { + get { return _deflateStream.CanWrite; } + } + + /// + /// Gets the length of the stream. + /// + public override long Length + { + get { throw new NotSupportedException(); } + } + + /// + /// Gets and sets the stream position. + /// + public override long Position + { + get { throw new NotSupportedException(); } + set { throw new NotSupportedException(); } + } + + /// + /// Closes the stream. + /// + protected override void Dispose(bool disposing) + { + if (_mode == CompressionMode.Decompress) + { + // Can only check Adler checksum on seekable streams. Since DeflateStream + // aggresively caches input, it normally has already consumed the footer. + if (_stream.CanSeek) + { + _stream.Seek(-4, SeekOrigin.End); + byte[] footerBuffer = StreamUtilities.ReadExact(_stream, 4); + if (EndianUtilities.ToInt32BigEndian(footerBuffer, 0) != _adler32.Value) + { + throw new InvalidDataException("Corrupt decompressed data detected"); + } + } + + _deflateStream.Dispose(); + } + else + { + _deflateStream.Dispose(); + + byte[] footerBuffer = new byte[4]; + EndianUtilities.WriteBytesBigEndian(_adler32.Value, footerBuffer, 0); + _stream.Write(footerBuffer, 0, 4); + } + + base.Dispose(disposing); + } + + /// + /// Flushes the stream. + /// + public override void Flush() + { + _deflateStream.Flush(); + } + + /// + /// Reads data from the stream. + /// + /// The buffer to populate. + /// The first byte to write. + /// The number of bytes requested. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + CheckParams(buffer, offset, count); + + int numRead = _deflateStream.Read(buffer, offset, count); + _adler32.Process(buffer, offset, numRead); + return numRead; + } + + /// + /// Seeks to a new position. + /// + /// Relative position to seek to. + /// The origin of the seek. + /// The new position. + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException(); + } + + /// + /// Changes the length of the stream. + /// + /// The new desired length of the stream. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Writes data to the stream. + /// + /// Buffer containing the data to write. + /// Offset of the first byte to write. + /// Number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + CheckParams(buffer, offset, count); + + _adler32.Process(buffer, offset, count); + _deflateStream.Write(buffer, offset, count); + } + + private static void CheckParams(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0 || offset > buffer.Length) + { + throw new ArgumentException("Offset outside of array bounds", nameof(offset)); + } + + if (count < 0 || offset + count > buffer.Length) + { + throw new ArgumentException("Array index out of bounds", nameof(count)); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/CoreCompat/EncodingHelper.cs b/DiscUtils/Core/CoreCompat/EncodingHelper.cs new file mode 100644 index 0000000..71a81e3 --- /dev/null +++ b/DiscUtils/Core/CoreCompat/EncodingHelper.cs @@ -0,0 +1,23 @@ +#if NETSTANDARD +using System.Text; +#endif + +namespace DiscUtils.CoreCompat +{ + internal static class EncodingHelper + { + private static bool _registered; + + public static void RegisterEncodings() + { + if (_registered) + return; + + _registered = true; + +#if NETSTANDARD + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); +#endif + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/CoreCompat/ReflectionHelper.cs b/DiscUtils/Core/CoreCompat/ReflectionHelper.cs new file mode 100644 index 0000000..3a5ef30 --- /dev/null +++ b/DiscUtils/Core/CoreCompat/ReflectionHelper.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; + +namespace DiscUtils.CoreCompat +{ + internal static class ReflectionHelper + { + public static bool IsEnum(Type type) + { +#if NETSTANDARD1_5 + return type.GetTypeInfo().IsEnum; +#else + return type.IsEnum; +#endif + } + + public static Attribute GetCustomAttribute(PropertyInfo property, Type attributeType) + { +#if NETSTANDARD1_5 + return property.GetCustomAttribute(attributeType); +#else + return Attribute.GetCustomAttribute(property, attributeType); +#endif + } + + public static Attribute GetCustomAttribute(PropertyInfo property, Type attributeType, bool inherit) + { +#if NETSTANDARD1_5 + return property.GetCustomAttribute(attributeType, inherit); +#else + return Attribute.GetCustomAttribute(property, attributeType, inherit); +#endif + } + + public static Attribute GetCustomAttribute(FieldInfo field, Type attributeType) + { +#if NETSTANDARD1_5 + return field.GetCustomAttribute(attributeType); +#else + return Attribute.GetCustomAttribute(field, attributeType); +#endif + } + + public static Attribute GetCustomAttribute(Type type, Type attributeType) + { +#if NETSTANDARD1_5 + return type.GetTypeInfo().GetCustomAttribute(attributeType); +#else + return Attribute.GetCustomAttribute(type, attributeType); +#endif + } + + public static Attribute GetCustomAttribute(Type type, Type attributeType, bool inherit) + { +#if NETSTANDARD1_5 + return type.GetTypeInfo().GetCustomAttribute(attributeType, inherit); +#else + return Attribute.GetCustomAttribute(type, attributeType); +#endif + } + + public static IEnumerable GetCustomAttributes(Type type, Type attributeType, bool inherit) + { +#if NETSTANDARD1_5 + return type.GetTypeInfo().GetCustomAttributes(attributeType, inherit); +#else + return Attribute.GetCustomAttributes(type, attributeType); +#endif + } + + public static Assembly GetAssembly(Type type) + { +#if NETSTANDARD1_5 + return type.GetTypeInfo().Assembly; +#else + return type.Assembly; +#endif + } + + public static int SizeOf() + { +#if NETSTANDARD1_5 + return Marshal.SizeOf(); +#else + return Marshal.SizeOf(typeof(T)); +#endif + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/CoreCompat/StringExtensions.cs b/DiscUtils/Core/CoreCompat/StringExtensions.cs new file mode 100644 index 0000000..9fb1e7b --- /dev/null +++ b/DiscUtils/Core/CoreCompat/StringExtensions.cs @@ -0,0 +1,15 @@ +#if NETSTANDARD1_5 +using System.Globalization; + +namespace DiscUtils.CoreCompat +{ + internal static class StringExtensions + { + public static string ToUpper(this string value, CultureInfo culture) + { + return value.ToUpper(); + } + } +} + +#endif \ No newline at end of file diff --git a/DiscUtils/Core/DiscDirectoryInfo.cs b/DiscUtils/Core/DiscDirectoryInfo.cs new file mode 100644 index 0000000..0a19821 --- /dev/null +++ b/DiscUtils/Core/DiscDirectoryInfo.cs @@ -0,0 +1,190 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Internal; + +namespace DiscUtils +{ + /// + /// Provides information about a directory on a disc. + /// + /// + /// This class allows navigation of the disc directory/file hierarchy. + /// + public sealed class DiscDirectoryInfo : DiscFileSystemInfo + { + /// + /// Initializes a new instance of the DiscDirectoryInfo class. + /// + /// The file system the directory info relates to. + /// The path within the file system of the directory. + internal DiscDirectoryInfo(DiscFileSystem fileSystem, string path) + : base(fileSystem, path) {} + + /// + /// Gets a value indicating whether the directory exists. + /// + public override bool Exists + { + get { return FileSystem.DirectoryExists(Path); } + } + + /// + /// Gets the full path of the directory. + /// + public override string FullName + { + get { return base.FullName + @"\"; } + } + + /// + /// Creates a directory. + /// + public void Create() + { + FileSystem.CreateDirectory(Path); + } + + /// + /// Deletes a directory, even if it's not empty. + /// + public override void Delete() + { + FileSystem.DeleteDirectory(Path, false); + } + + /// + /// Deletes a directory, with the caller choosing whether to recurse. + /// + /// true to delete all child node, false to fail if the directory is not empty. + public void Delete(bool recursive) + { + FileSystem.DeleteDirectory(Path, recursive); + } + + /// + /// Moves a directory and it's contents to a new path. + /// + /// The destination directory name. + public void MoveTo(string destinationDirName) + { + FileSystem.MoveDirectory(Path, destinationDirName); + } + + /// + /// Gets all child directories. + /// + /// An array of child directories. + public DiscDirectoryInfo[] GetDirectories() + { + return Utilities.Map(FileSystem.GetDirectories(Path), + p => new DiscDirectoryInfo(FileSystem, p)); + } + + /// + /// Gets all child directories matching a search pattern. + /// + /// The search pattern. + /// An array of child directories, or empty if none match. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). + public DiscDirectoryInfo[] GetDirectories(string pattern) + { + return GetDirectories(pattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets all descendant directories matching a search pattern. + /// + /// The search pattern. + /// Whether to search just this directory, or all children. + /// An array of descendant directories, or empty if none match. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). The option parameter determines whether only immediate + /// children, or all children are returned. + public DiscDirectoryInfo[] GetDirectories(string pattern, SearchOption searchOption) + { + return Utilities.Map(FileSystem.GetDirectories(Path, pattern, searchOption), + p => new DiscDirectoryInfo(FileSystem, p)); + } + + /// + /// Gets all files. + /// + /// An array of files. + public DiscFileInfo[] GetFiles() + { + return Utilities.Map(FileSystem.GetFiles(Path), p => new DiscFileInfo(FileSystem, p)); + } + + /// + /// Gets all files matching a search pattern. + /// + /// The search pattern. + /// An array of files, or empty if none match. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). + public DiscFileInfo[] GetFiles(string pattern) + { + return GetFiles(pattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets all descendant files matching a search pattern. + /// + /// The search pattern. + /// Whether to search just this directory, or all children. + /// An array of descendant files, or empty if none match. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). The option parameter determines whether only immediate + /// children, or all children are returned. + public DiscFileInfo[] GetFiles(string pattern, SearchOption searchOption) + { + return Utilities.Map(FileSystem.GetFiles(Path, pattern, searchOption), + p => new DiscFileInfo(FileSystem, p)); + } + + /// + /// Gets all files and directories in this directory. + /// + /// An array of files and directories. + public DiscFileSystemInfo[] GetFileSystemInfos() + { + return Utilities.Map(FileSystem.GetFileSystemEntries(Path), + p => new DiscFileSystemInfo(FileSystem, p)); + } + + /// + /// Gets all files and directories in this directory. + /// + /// The search pattern. + /// An array of files and directories. + /// The search pattern can include the wildcards * (matching 0 or more characters) + /// and ? (matching 1 character). + public DiscFileSystemInfo[] GetFileSystemInfos(string pattern) + { + return Utilities.Map(FileSystem.GetFileSystemEntries(Path, pattern), + p => new DiscFileSystemInfo(FileSystem, p)); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/DiscFileInfo.cs b/DiscUtils/Core/DiscFileInfo.cs new file mode 100644 index 0000000..14d8845 --- /dev/null +++ b/DiscUtils/Core/DiscFileInfo.cs @@ -0,0 +1,200 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils +{ + /// + /// Provides information about a file on a disc. + /// + public sealed class DiscFileInfo : DiscFileSystemInfo + { + internal DiscFileInfo(DiscFileSystem fileSystem, string path) + : base(fileSystem, path) {} + + /// + /// Gets an instance of the parent directory. + /// + public DiscDirectoryInfo Directory + { + get { return Parent; } + } + + /// + /// Gets a string representing the directory's full path. + /// + public string DirectoryName + { + get { return Directory.FullName; } + } + + /// + /// Gets a value indicating whether the file exists. + /// + public override bool Exists + { + get { return FileSystem.FileExists(Path); } + } + + /// + /// Gets or sets a value indicating whether the file is read-only. + /// + public bool IsReadOnly + { + get { return (Attributes & FileAttributes.ReadOnly) != 0; } + + set + { + if (value) + { + Attributes = Attributes | FileAttributes.ReadOnly; + } + else + { + Attributes = Attributes & ~FileAttributes.ReadOnly; + } + } + } + + /// + /// Gets the length of the current file in bytes. + /// + public long Length + { + get { return FileSystem.GetFileLength(Path); } + } + + /// + /// Deletes a file. + /// + public override void Delete() + { + FileSystem.DeleteFile(Path); + } + + /// + /// Creates a that appends text to the file represented by this . + /// + /// The newly created writer. + public StreamWriter AppendText() + { + return new StreamWriter(Open(FileMode.Append)); + } + + /// + /// Copies an existing file to a new file. + /// + /// The destination file. + public void CopyTo(string destinationFileName) + { + CopyTo(destinationFileName, false); + } + + /// + /// Copies an existing file to a new file, allowing overwriting of an existing file. + /// + /// The destination file. + /// Whether to permit over-writing of an existing file. + public void CopyTo(string destinationFileName, bool overwrite) + { + FileSystem.CopyFile(Path, destinationFileName, overwrite); + } + + /// + /// Creates a new file for reading and writing. + /// + /// The newly created stream. + public Stream Create() + { + return Open(FileMode.Create); + } + + /// + /// Creates a new that writes a new text file. + /// + /// A new stream writer that can write to the file contents. + public StreamWriter CreateText() + { + return new StreamWriter(Open(FileMode.Create)); + } + + /// + /// Moves a file to a new location. + /// + /// The new name of the file. + public void MoveTo(string destinationFileName) + { + FileSystem.MoveFile(Path, destinationFileName); + } + + /// + /// Opens the current file. + /// + /// The file mode for the created stream. + /// The newly created stream. + /// Read-only file systems only support FileMode.Open. + public Stream Open(FileMode mode) + { + return FileSystem.OpenFile(Path, mode); + } + + /// + /// Opens the current file. + /// + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The newly created stream. + /// Read-only file systems only support FileMode.Open and FileAccess.Read. + public Stream Open(FileMode mode, FileAccess access) + { + return FileSystem.OpenFile(Path, mode, access); + } + + /// + /// Opens an existing file for read-only access. + /// + /// The newly created stream. + public Stream OpenRead() + { + return Open(FileMode.Open, FileAccess.Read); + } + + /// + /// Opens an existing file for reading as UTF-8 text. + /// + /// The newly created reader. + public StreamReader OpenText() + { + return new StreamReader(OpenRead()); + } + + /// + /// Opens a file for writing. + /// + /// The newly created stream. + public Stream OpenWrite() + { + return Open(FileMode.Open, FileAccess.Write); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/DiscFileLocator.cs b/DiscUtils/Core/DiscFileLocator.cs new file mode 100644 index 0000000..24b41bb --- /dev/null +++ b/DiscUtils/Core/DiscFileLocator.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Internal; + +namespace DiscUtils +{ + internal sealed class DiscFileLocator : FileLocator + { + private readonly string _basePath; + private readonly DiscFileSystem _fileSystem; + + public DiscFileLocator(DiscFileSystem fileSystem, string basePath) + { + _fileSystem = fileSystem; + _basePath = basePath; + } + + public override bool Exists(string fileName) + { + return _fileSystem.FileExists(Utilities.CombinePaths(_basePath, fileName)); + } + + protected override Stream OpenFile(string fileName, FileMode mode, FileAccess access, FileShare share) + { + return _fileSystem.OpenFile(Utilities.CombinePaths(_basePath, fileName), mode, access); + } + + public override FileLocator GetRelativeLocator(string path) + { + return new DiscFileLocator(_fileSystem, Utilities.CombinePaths(_basePath, path)); + } + + public override string GetFullPath(string path) + { + return Utilities.CombinePaths(_basePath, path); + } + + public override string GetDirectoryFromPath(string path) + { + return Utilities.GetDirectoryFromPath(path); + } + + public override string GetFileFromPath(string path) + { + return Utilities.GetFileFromPath(path); + } + + public override DateTime GetLastWriteTimeUtc(string path) + { + return _fileSystem.GetLastWriteTimeUtc(Utilities.CombinePaths(_basePath, path)); + } + + public override bool HasCommonRoot(FileLocator other) + { + DiscFileLocator otherDiscLocator = other as DiscFileLocator; + + if (otherDiscLocator == null) + { + return false; + } + + // Common root if the same file system instance. + return ReferenceEquals(otherDiscLocator._fileSystem, _fileSystem); + } + + public override string ResolveRelativePath(string path) + { + return Utilities.ResolveRelativePath(_basePath, path); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/DiscFileSystem.cs b/DiscUtils/Core/DiscFileSystem.cs new file mode 100644 index 0000000..ece371d --- /dev/null +++ b/DiscUtils/Core/DiscFileSystem.cs @@ -0,0 +1,509 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Provides the base class for all file systems. + /// + public abstract class DiscFileSystem : +#if !NETSTANDARD + MarshalByRefObject, +#endif + IFileSystem, IDisposable + { + /// + /// Initializes a new instance of the DiscFileSystem class. + /// + protected DiscFileSystem() + { + Options = new DiscFileSystemOptions(); + } + + /// + /// Initializes a new instance of the DiscFileSystem class. + /// + /// The options instance to use for this file system instance. + protected DiscFileSystem(DiscFileSystemOptions defaultOptions) + { + Options = defaultOptions; + } + + /// + /// Finalizes an instance of the DiscFileSystem class. + /// + ~DiscFileSystem() + { + Dispose(false); + } + + /// + /// Gets the file system options, which can be modified. + /// + public virtual DiscFileSystemOptions Options { get; } + + /// + /// Gets a friendly description of the file system type. + /// + public abstract string FriendlyName { get; } + + /// + /// Gets a value indicating whether the file system is read-only or read-write. + /// + /// true if the file system is read-write. + public abstract bool CanWrite { get; } + + /// + /// Gets the root directory of the file system. + /// + public virtual DiscDirectoryInfo Root + { + get { return new DiscDirectoryInfo(this, string.Empty); } + } + + /// + /// Gets the volume label. + /// + public virtual string VolumeLabel + { + get { return string.Empty; } + } + + /// + /// Gets a value indicating whether the file system is thread-safe. + /// + public virtual bool IsThreadSafe + { + get { return false; } + } + + /// + /// Copies an existing file to a new file. + /// + /// The source file. + /// The destination file. + public virtual void CopyFile(string sourceFile, string destinationFile) + { + CopyFile(sourceFile, destinationFile, false); + } + + /// + /// Copies an existing file to a new file, allowing overwriting of an existing file. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public abstract void CopyFile(string sourceFile, string destinationFile, bool overwrite); + + /// + /// Creates a directory. + /// + /// The path of the new directory. + public abstract void CreateDirectory(string path); + + /// + /// Deletes a directory. + /// + /// The path of the directory to delete. + public abstract void DeleteDirectory(string path); + + /// + /// Deletes a directory, optionally with all descendants. + /// + /// The path of the directory to delete. + /// Determines if the all descendants should be deleted. + public virtual void DeleteDirectory(string path, bool recursive) + { + if (recursive) + { + foreach (string dir in GetDirectories(path)) + { + DeleteDirectory(dir, true); + } + + foreach (string file in GetFiles(path)) + { + DeleteFile(file); + } + } + + DeleteDirectory(path); + } + + /// + /// Deletes a file. + /// + /// The path of the file to delete. + public abstract void DeleteFile(string path); + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + public abstract bool DirectoryExists(string path); + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + public abstract bool FileExists(string path); + + /// + /// Indicates if a file or directory exists. + /// + /// The path to test. + /// true if the file or directory exists. + public virtual bool Exists(string path) + { + return FileExists(path) || DirectoryExists(path); + } + + /// + /// Gets the names of subdirectories in a specified directory. + /// + /// The path to search. + /// Array of directories. + public virtual string[] GetDirectories(string path) + { + return GetDirectories(path, "*.*", SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of directories matching the search pattern. + public virtual string[] GetDirectories(string path, string searchPattern) + { + return GetDirectories(path, searchPattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + public abstract string[] GetDirectories(string path, string searchPattern, SearchOption searchOption); + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// Array of files. + public virtual string[] GetFiles(string path) + { + return GetFiles(path, "*.*", SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// The search string to match against. + /// Array of files matching the search pattern. + public virtual string[] GetFiles(string path, string searchPattern) + { + return GetFiles(path, searchPattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + public abstract string[] GetFiles(string path, string searchPattern, SearchOption searchOption); + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + public abstract string[] GetFileSystemEntries(string path); + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + public abstract string[] GetFileSystemEntries(string path, string searchPattern); + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + public abstract void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName); + + /// + /// Moves a file. + /// + /// The file to move. + /// The target file name. + public virtual void MoveFile(string sourceName, string destinationName) + { + MoveFile(sourceName, destinationName, false); + } + + /// + /// Moves a file, allowing an existing file to be overwritten. + /// + /// The file to move. + /// The target file name. + /// Whether to permit a destination file to be overwritten. + public abstract void MoveFile(string sourceName, string destinationName, bool overwrite); + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + public virtual SparseStream OpenFile(string path, FileMode mode) + { + return OpenFile(path, mode, FileAccess.ReadWrite); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The new stream. + public abstract SparseStream OpenFile(string path, FileMode mode, FileAccess access); + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + public abstract FileAttributes GetAttributes(string path); + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public abstract void SetAttributes(string path, FileAttributes newValue); + + /// + /// Gets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public virtual DateTime GetCreationTime(string path) + { + return GetCreationTimeUtc(path).ToLocalTime(); + } + + /// + /// Sets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public virtual void SetCreationTime(string path, DateTime newTime) + { + SetCreationTimeUtc(path, newTime.ToUniversalTime()); + } + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public abstract DateTime GetCreationTimeUtc(string path); + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public abstract void SetCreationTimeUtc(string path, DateTime newTime); + + /// + /// Gets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public virtual DateTime GetLastAccessTime(string path) + { + return GetLastAccessTimeUtc(path).ToLocalTime(); + } + + /// + /// Sets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public virtual void SetLastAccessTime(string path, DateTime newTime) + { + SetLastAccessTimeUtc(path, newTime.ToUniversalTime()); + } + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public abstract DateTime GetLastAccessTimeUtc(string path); + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public abstract void SetLastAccessTimeUtc(string path, DateTime newTime); + + /// + /// Gets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public virtual DateTime GetLastWriteTime(string path) + { + return GetLastWriteTimeUtc(path).ToLocalTime(); + } + + /// + /// Sets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public virtual void SetLastWriteTime(string path, DateTime newTime) + { + SetLastWriteTimeUtc(path, newTime.ToUniversalTime()); + } + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public abstract DateTime GetLastWriteTimeUtc(string path); + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public abstract void SetLastWriteTimeUtc(string path, DateTime newTime); + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + public abstract long GetFileLength(string path); + + /// + /// Gets an object representing a possible file. + /// + /// The file path. + /// The representing object. + /// The file does not need to exist. + public virtual DiscFileInfo GetFileInfo(string path) + { + return new DiscFileInfo(this, path); + } + + /// + /// Gets an object representing a possible directory. + /// + /// The directory path. + /// The representing object. + /// The directory does not need to exist. + public virtual DiscDirectoryInfo GetDirectoryInfo(string path) + { + return new DiscDirectoryInfo(this, path); + } + + /// + /// Gets an object representing a possible file system object (file or directory). + /// + /// The file system path. + /// The representing object. + /// The file system object does not need to exist. + public virtual DiscFileSystemInfo GetFileSystemInfo(string path) + { + return new DiscFileSystemInfo(this, path); + } + + /// + /// Reads the boot code of the file system into a byte array. + /// + /// The boot code, or null if not available. + public virtual byte[] ReadBootCode() + { + return null; + } + + /// + /// Size of the Filesystem in bytes + /// + public abstract long Size { get; } + + /// + /// Used space of the Filesystem in bytes + /// + public abstract long UsedSpace { get; } + + /// + /// Available space of the Filesystem in bytes + /// + public abstract long AvailableSpace { get; } + + #region IDisposable Members + + /// + /// Disposes of this instance, releasing all resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of this instance. + /// + /// The value true if Disposing. + protected virtual void Dispose(bool disposing) {} + + #endregion + } +} diff --git a/DiscUtils/Core/DiscFileSystemChecker.cs b/DiscUtils/Core/DiscFileSystemChecker.cs new file mode 100644 index 0000000..426f0f0 --- /dev/null +++ b/DiscUtils/Core/DiscFileSystemChecker.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils +{ + /// + /// Base class for objects that validate file system integrity. + /// + /// Instances of this class do not offer the ability to fix/correct + /// file system issues, just to perform a limited number of checks on + /// integrity of the file system. + public abstract class DiscFileSystemChecker + { + /// + /// Checks the integrity of a file system held in a stream. + /// + /// A report on issues found. + /// The amount of detail to report. + /// true if the file system appears valid, else false. + public abstract bool Check(TextWriter reportOutput, ReportLevels levels); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/DiscFileSystemInfo.cs b/DiscUtils/Core/DiscFileSystemInfo.cs new file mode 100644 index 0000000..d82e5c6 --- /dev/null +++ b/DiscUtils/Core/DiscFileSystemInfo.cs @@ -0,0 +1,219 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Internal; + +namespace DiscUtils +{ + /// + /// Provides the base class for both and objects. + /// + public class DiscFileSystemInfo + { + internal DiscFileSystemInfo(DiscFileSystem fileSystem, string path) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + FileSystem = fileSystem; + Path = path.Trim('\\'); + } + + /// + /// Gets or sets the of the current object. + /// + public virtual FileAttributes Attributes + { + get { return FileSystem.GetAttributes(Path); } + set { FileSystem.SetAttributes(Path, value); } + } + + /// + /// Gets or sets the creation time (in local time) of the current object. + /// + public virtual DateTime CreationTime + { + get { return CreationTimeUtc.ToLocalTime(); } + set { CreationTimeUtc = value.ToUniversalTime(); } + } + + /// + /// Gets or sets the creation time (in UTC) of the current object. + /// + public virtual DateTime CreationTimeUtc + { + get { return FileSystem.GetCreationTimeUtc(Path); } + set { FileSystem.SetCreationTimeUtc(Path, value); } + } + + /// + /// Gets a value indicating whether the file system object exists. + /// + public virtual bool Exists + { + get { return FileSystem.Exists(Path); } + } + + /// + /// Gets the extension part of the file or directory name. + /// + public virtual string Extension + { + get + { + string name = Name; + int sepIdx = name.LastIndexOf('.'); + if (sepIdx >= 0) + { + return name.Substring(sepIdx + 1); + } + + return string.Empty; + } + } + + /// + /// Gets the file system the referenced file or directory exists on. + /// + public DiscFileSystem FileSystem { get; } + + /// + /// Gets the full path of the file or directory. + /// + public virtual string FullName + { + get { return Path; } + } + + /// + /// Gets or sets the last time (in local time) the file or directory was accessed. + /// + /// Read-only file systems will never update this value, it will remain at a fixed value. + public virtual DateTime LastAccessTime + { + get { return LastAccessTimeUtc.ToLocalTime(); } + set { LastAccessTimeUtc = value.ToUniversalTime(); } + } + + /// + /// Gets or sets the last time (in UTC) the file or directory was accessed. + /// + /// Read-only file systems will never update this value, it will remain at a fixed value. + public virtual DateTime LastAccessTimeUtc + { + get { return FileSystem.GetLastAccessTimeUtc(Path); } + set { FileSystem.SetLastAccessTimeUtc(Path, value); } + } + + /// + /// Gets or sets the last time (in local time) the file or directory was written to. + /// + public virtual DateTime LastWriteTime + { + get { return LastWriteTimeUtc.ToLocalTime(); } + set { LastWriteTimeUtc = value.ToUniversalTime(); } + } + + /// + /// Gets or sets the last time (in UTC) the file or directory was written to. + /// + public virtual DateTime LastWriteTimeUtc + { + get { return FileSystem.GetLastWriteTimeUtc(Path); } + set { FileSystem.SetLastWriteTimeUtc(Path, value); } + } + + /// + /// Gets the name of the file or directory. + /// + public virtual string Name + { + get { return Utilities.GetFileFromPath(Path); } + } + + /// + /// Gets the of the directory containing the current object. + /// + public virtual DiscDirectoryInfo Parent + { + get + { + if (string.IsNullOrEmpty(Path)) + { + return null; + } + + return new DiscDirectoryInfo(FileSystem, Utilities.GetDirectoryFromPath(Path)); + } + } + + /// + /// Gets the path to the referenced file. + /// + protected string Path { get; } + + /// + /// Deletes a file or directory. + /// + public virtual void Delete() + { + if ((Attributes & FileAttributes.Directory) != 0) + { + FileSystem.DeleteDirectory(Path); + } + else + { + FileSystem.DeleteFile(Path); + } + } + + /// + /// Indicates if is equivalent to this object. + /// + /// The object to compare. + /// true if is equivalent, else false. + public override bool Equals(object obj) + { + DiscFileSystemInfo asInfo = obj as DiscFileSystemInfo; + if (obj == null) + { + return false; + } + + return string.Compare(Path, asInfo.Path, StringComparison.Ordinal) == 0 && + Equals(FileSystem, asInfo.FileSystem); + } + + /// + /// Gets the hash code for this object. + /// + /// The hash code. + public override int GetHashCode() + { + return Path.GetHashCode() ^ FileSystem.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/DiscFileSystemOptions.cs b/DiscUtils/Core/DiscFileSystemOptions.cs new file mode 100644 index 0000000..b7aed0f --- /dev/null +++ b/DiscUtils/Core/DiscFileSystemOptions.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils +{ + /// + /// Common file system options. + /// + /// Not all options are honoured by all file systems. + public class DiscFileSystemOptions + { + /// + /// Gets or sets the random number generator the file system should use. + /// + /// This option is normally null, which is fine for most purposes. + /// Use this option when you need to finely control the filesystem for + /// reproducibility of behaviour (for example in a test harness). + public Random RandomNumberGenerator { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/DiscUtils.Core.csproj b/DiscUtils/Core/DiscUtils.Core.csproj new file mode 100644 index 0000000..8abd0ad --- /dev/null +++ b/DiscUtils/Core/DiscUtils.Core.csproj @@ -0,0 +1,15 @@ + + + + + Implementation of the ISO, UDF, FAT and NTFS file systems is now fairly stable. VHD, XVA, VMDK and VDI disk formats are implemented, as well as read/write Registry support. The library also includes a simple iSCSI initiator, for accessing disks via iSCSI and an NFS client implementation. + DiscUtils (for .NET and .NET Core), core library that supports parts of DiscUtils + Kenneth Bell;Quamotion;LordMike + DiscUtils;VHD;VDI;XVA;VMDK;ISO;NTFS;EXT2FS + + + + + + + diff --git a/DiscUtils/Core/DiskImageBuilder.cs b/DiscUtils/Core/DiskImageBuilder.cs new file mode 100644 index 0000000..0cb2327 --- /dev/null +++ b/DiscUtils/Core/DiskImageBuilder.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using DiscUtils.CoreCompat; +using DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Base class for all disk image builders. + /// + public abstract class DiskImageBuilder + { + private static Dictionary _typeMap; + + /// + /// Gets or sets the geometry of this disk, as reported by the BIOS, will be implied from the content stream if not set. + /// + public Geometry BiosGeometry { get; set; } + + /// + /// Gets or sets the content for this disk, implying the size of the disk. + /// + public SparseStream Content { get; set; } + + /// + /// Gets or sets the adapter type for created virtual disk, for file formats that encode this information. + /// + public virtual GenericDiskAdapterType GenericAdapterType { get; set; } + + /// + /// Gets or sets the geometry of this disk, will be implied from the content stream if not set. + /// + public Geometry Geometry { get; set; } + + /// + /// Gets a value indicating whether this file format preserves BIOS geometry information. + /// + public virtual bool PreservesBiosGeometry + { + get { return false; } + } + + private static Dictionary TypeMap + { + get + { + if (_typeMap == null) + { + InitializeMaps(); + } + + return _typeMap; + } + } + + /// + /// Gets an instance that constructs the specified type (and variant) of virtual disk image. + /// + /// The type of image to build (VHD, VMDK, etc). + /// The variant type (differencing/dynamic, fixed/static, etc). + /// The builder instance. + public static DiskImageBuilder GetBuilder(string type, string variant) + { + VirtualDiskFactory factory; + if (!TypeMap.TryGetValue(type, out factory)) + { + throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Unknown disk type '{0}'", type), nameof(type)); + } + + return factory.GetImageBuilder(variant); + } + + /// + /// Initiates the construction of the disk image. + /// + /// The base name for the disk images. + /// A set of one or more logical files that constitute the + /// disk image. The first file is the 'primary' file that is normally attached to VMs. + /// The supplied baseName is the start of the file name, with no file + /// extension. The set of file specifications will indicate the actual name corresponding + /// to each logical file that comprises the disk image. For example, given a base name + /// 'foo', the files 'foo.vmdk' and 'foo-flat.vmdk' could be returned. + public abstract DiskImageFileSpecification[] Build(string baseName); + + private static void InitializeMaps() + { + Dictionary typeMap = new Dictionary(); + + foreach (Type type in ReflectionHelper.GetAssembly(typeof(VirtualDisk)).GetTypes()) + { + VirtualDiskFactoryAttribute attr = (VirtualDiskFactoryAttribute)ReflectionHelper.GetCustomAttribute(type, typeof(VirtualDiskFactoryAttribute), false); + if (attr != null) + { + VirtualDiskFactory factory = (VirtualDiskFactory)Activator.CreateInstance(type); + typeMap.Add(attr.Type, factory); + } + } + + _typeMap = typeMap; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/DiskImageFileSpecification.cs b/DiscUtils/Core/DiskImageFileSpecification.cs new file mode 100644 index 0000000..9e0d2f4 --- /dev/null +++ b/DiscUtils/Core/DiskImageFileSpecification.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Describes a particular file that is a constituent part of a virtual disk. + /// + public sealed class DiskImageFileSpecification + { + private readonly StreamBuilder _builder; + + internal DiskImageFileSpecification(string name, StreamBuilder builder) + { + Name = name; + _builder = builder; + } + + /// + /// Gets name of the file. + /// + public string Name { get; } + + /// + /// Gets the object that provides access to the file's content. + /// + /// A stream object that contains the file's content. + public SparseStream OpenStream() + { + return _builder.Build(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/FileLocator.cs b/DiscUtils/Core/FileLocator.cs new file mode 100644 index 0000000..a170298 --- /dev/null +++ b/DiscUtils/Core/FileLocator.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Internal; +using DiscUtils.Setup; + +namespace DiscUtils +{ + public abstract class FileLocator + { + public abstract bool Exists(string fileName); + + public Stream Open(string fileName, FileMode mode, FileAccess access, FileShare share) + { + var args = new FileOpenEventArgs(fileName, mode, access, share, OpenFile); + SetupHelper.OnOpeningFile(this, args); + if (args.Result != null) + return args.Result; + return OpenFile(args.FileName, args.FileMode, args.FileAccess, args.FileShare); + } + + protected abstract Stream OpenFile(string fileName, FileMode mode, FileAccess access, FileShare share); + + public abstract FileLocator GetRelativeLocator(string path); + + public abstract string GetFullPath(string path); + + public abstract string GetDirectoryFromPath(string path); + + public abstract string GetFileFromPath(string path); + + public abstract DateTime GetLastWriteTimeUtc(string path); + + public abstract bool HasCommonRoot(FileLocator other); + + public abstract string ResolveRelativePath(string path); + + internal string MakeRelativePath(FileLocator fileLocator, string path) + { + if (!HasCommonRoot(fileLocator)) + { + return null; + } + + string ourFullPath = GetFullPath(string.Empty) + @"\"; + string otherFullPath = fileLocator.GetFullPath(path); + + return Utilities.MakeRelativePath(otherFullPath, ourFullPath); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/FileSystemInfo.cs b/DiscUtils/Core/FileSystemInfo.cs new file mode 100644 index 0000000..5a439ad --- /dev/null +++ b/DiscUtils/Core/FileSystemInfo.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils +{ + /// + /// Base class holding information about a file system. + /// + /// + /// File system implementations derive from this class, to provide information about the file system. + /// + public abstract class FileSystemInfo + { + /// + /// Gets a one-line description of the file system. + /// + public abstract string Description { get; } + + /// + /// Gets the name of the file system. + /// + public abstract string Name { get; } + + /// + /// Opens a volume using the file system. + /// + /// The volume to access. + /// A file system instance. + public DiscFileSystem Open(VolumeInfo volume) + { + return Open(volume, null); + } + + /// + /// Opens a stream using the file system. + /// + /// The stream to access. + /// A file system instance. + public DiscFileSystem Open(Stream stream) + { + return Open(stream, null); + } + + /// + /// Opens a volume using the file system. + /// + /// The volume to access. + /// Parameters for the file system. + /// A file system instance. + public abstract DiscFileSystem Open(VolumeInfo volume, FileSystemParameters parameters); + + /// + /// Opens a stream using the file system. + /// + /// The stream to access. + /// Parameters for the file system. + /// A file system instance. + public abstract DiscFileSystem Open(Stream stream, FileSystemParameters parameters); + + /// + /// Gets the name of the file system. + /// + /// The file system name. + public override string ToString() + { + return Name; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/FileSystemManager.cs b/DiscUtils/Core/FileSystemManager.cs new file mode 100644 index 0000000..573e93a --- /dev/null +++ b/DiscUtils/Core/FileSystemManager.cs @@ -0,0 +1,121 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Reflection; +using DiscUtils.CoreCompat; +using DiscUtils.Vfs; + +namespace DiscUtils +{ + /// + /// FileSystemManager determines which file systems are present on a volume. + /// + /// + /// The static detection methods detect default file systems. To plug in additional + /// file systems, create an instance of this class and call RegisterFileSystems. + /// + public static class FileSystemManager + { + private static readonly List _factories; + + /// + /// Initializes a new instance of the FileSystemManager class. + /// + static FileSystemManager() + { + _factories = new List(); + } + + /// + /// Registers new file systems with an instance of this class. + /// + /// The detector for the new file systems. + public static void RegisterFileSystems(VfsFileSystemFactory factory) + { + _factories.Add(factory); + } + + /// + /// Registers new file systems detected in an assembly. + /// + /// The assembly to inspect. + /// + /// To be detected, the VfsFileSystemFactory instances must be marked with the + /// VfsFileSystemFactoryAttribute> attribute. + /// + public static void RegisterFileSystems(Assembly assembly) + { + _factories.AddRange(DetectFactories(assembly)); + } + + /// + /// Detect which file systems are present on a volume. + /// + /// The volume to inspect. + /// The list of file systems detected. + public static FileSystemInfo[] DetectFileSystems(VolumeInfo volume) + { + using (Stream s = volume.Open()) + { + return DoDetect(s, volume); + } + } + + /// + /// Detect which file systems are present in a stream. + /// + /// The stream to inspect. + /// The list of file systems detected. + public static FileSystemInfo[] DetectFileSystems(Stream stream) + { + return DoDetect(stream, null); + } + + private static IEnumerable DetectFactories(Assembly assembly) + { + foreach (Type type in assembly.GetTypes()) + { + Attribute attrib = ReflectionHelper.GetCustomAttribute(type, typeof(VfsFileSystemFactoryAttribute), false); + if (attrib == null) + continue; + + yield return (VfsFileSystemFactory)Activator.CreateInstance(type); + } + } + + private static FileSystemInfo[] DoDetect(Stream stream, VolumeInfo volume) + { + BufferedStream detectStream = new BufferedStream(stream); + List detected = new List(); + + foreach (VfsFileSystemFactory factory in _factories) + { + detected.AddRange(factory.Detect(detectStream, volume)); + } + + return detected.ToArray(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/FileSystemParameters.cs b/DiscUtils/Core/FileSystemParameters.cs new file mode 100644 index 0000000..b0daee8 --- /dev/null +++ b/DiscUtils/Core/FileSystemParameters.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils +{ + /// + /// Class with generic file system parameters. + /// + /// Note - not all parameters apply to all types of file system. + public sealed class FileSystemParameters + { + /// + /// Gets or sets the character encoding for file names, or null for default. + /// + /// Some file systems, such as FAT, don't specify a particular character set for + /// file names. This parameter determines the character set that will be used for such + /// file systems. + public Encoding FileNameEncoding { get; set; } + + /// + /// Gets or sets the algorithm to convert file system time to UTC. + /// + /// Some file system, such as FAT, don't have a defined way to convert from file system + /// time (local time where the file system is authored) to UTC time. This parameter determines + /// the algorithm to use. + public TimeConverter TimeConverter { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/FileTransport.cs b/DiscUtils/Core/FileTransport.cs new file mode 100644 index 0000000..d22a5ee --- /dev/null +++ b/DiscUtils/Core/FileTransport.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using DiscUtils.Internal; + +namespace DiscUtils +{ + [VirtualDiskTransport("file")] + internal sealed class FileTransport : VirtualDiskTransport + { + private string _extraInfo; + private string _path; + + public override bool IsRawDisk + { + get { return false; } + } + + public override void Connect(Uri uri, string username, string password) + { + _path = uri.LocalPath; + _extraInfo = uri.Fragment.TrimStart('#'); + + if (!Directory.Exists(Path.GetDirectoryName(_path))) + { + throw new FileNotFoundException( + string.Format(CultureInfo.InvariantCulture, "No such file '{0}'", uri.OriginalString), _path); + } + } + + public override VirtualDisk OpenDisk(FileAccess access) + { + throw new NotSupportedException(); + } + + public override FileLocator GetFileLocator() + { + return new LocalFileLocator(Path.GetDirectoryName(_path) + @"\"); + } + + public override string GetFileName() + { + return Path.GetFileName(_path); + } + + public override string GetExtraInfo() + { + return _extraInfo; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/FloppyDiskType.cs b/DiscUtils/Core/FloppyDiskType.cs new file mode 100644 index 0000000..708c847 --- /dev/null +++ b/DiscUtils/Core/FloppyDiskType.cs @@ -0,0 +1,45 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// The supported Floppy Disk logical formats. + /// + public enum FloppyDiskType + { + /// + /// 720KiB capacity disk. + /// + DoubleDensity = 0, + + /// + /// 1440KiB capacity disk. + /// + HighDensity = 1, + + /// + /// 2880KiB capacity disk. + /// + Extended = 2 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/GenericDiskAdapterType.cs b/DiscUtils/Core/GenericDiskAdapterType.cs new file mode 100644 index 0000000..7e3b2b4 --- /dev/null +++ b/DiscUtils/Core/GenericDiskAdapterType.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// Well known hard disk adaptor types. + /// + public enum GenericDiskAdapterType + { + /// + /// IDE adaptor. + /// + Ide = 0, + + /// + /// SCSI adaptor. + /// + Scsi = 1 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Geometry.cs b/DiscUtils/Core/Geometry.cs new file mode 100644 index 0000000..e23797f --- /dev/null +++ b/DiscUtils/Core/Geometry.cs @@ -0,0 +1,477 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Class whose instances represent disk geometries. + /// + /// Instances of this class are immutable. + public sealed class Geometry + { + /// + /// Initializes a new instance of the Geometry class. The default 512 bytes per sector is assumed. + /// + /// The number of cylinders of the disk. + /// The number of heads (aka platters) of the disk. + /// The number of sectors per track/cylinder of the disk. + public Geometry(int cylinders, int headsPerCylinder, int sectorsPerTrack) + { + Cylinders = cylinders; + HeadsPerCylinder = headsPerCylinder; + SectorsPerTrack = sectorsPerTrack; + BytesPerSector = 512; + } + + /// + /// Initializes a new instance of the Geometry class. + /// + /// The number of cylinders of the disk. + /// The number of heads (aka platters) of the disk. + /// The number of sectors per track/cylinder of the disk. + /// The number of bytes per sector of the disk. + public Geometry(int cylinders, int headsPerCylinder, int sectorsPerTrack, int bytesPerSector) + { + Cylinders = cylinders; + HeadsPerCylinder = headsPerCylinder; + SectorsPerTrack = sectorsPerTrack; + BytesPerSector = bytesPerSector; + } + + /// + /// Initializes a new instance of the Geometry class. + /// + /// The total capacity of the disk. + /// The number of heads (aka platters) of the disk. + /// The number of sectors per track/cylinder of the disk. + /// The number of bytes per sector of the disk. + public Geometry(long capacity, int headsPerCylinder, int sectorsPerTrack, int bytesPerSector) + { + Cylinders = (int)(capacity / (headsPerCylinder * (long)sectorsPerTrack * bytesPerSector)); + HeadsPerCylinder = headsPerCylinder; + SectorsPerTrack = sectorsPerTrack; + BytesPerSector = bytesPerSector; + } + + /// + /// Gets the number of bytes in each sector. + /// + public int BytesPerSector { get; } + + /// + /// Gets the total capacity of the disk (in bytes). + /// + public long Capacity + { + get { return TotalSectorsLong * BytesPerSector; } + } + + /// + /// Gets the number of cylinders. + /// + public int Cylinders { get; } + + /// + /// Gets the number of heads (aka platters). + /// + public int HeadsPerCylinder { get; } + + /// + /// Gets a value indicating whether the Geometry is representable both by the BIOS and by IDE. + /// + public bool IsBiosAndIdeSafe + { + get { return Cylinders <= 1024 && HeadsPerCylinder <= 16 && SectorsPerTrack <= 63; } + } + + /// + /// Gets a value indicating whether the Geometry is consistent with the values a BIOS can support. + /// + public bool IsBiosSafe + { + get { return Cylinders <= 1024 && HeadsPerCylinder <= 255 && SectorsPerTrack <= 63; } + } + + /// + /// Gets a value indicating whether the Geometry is consistent with the values IDE can represent. + /// + public bool IsIdeSafe + { + get { return Cylinders <= 65536 && HeadsPerCylinder <= 16 && SectorsPerTrack <= 255; } + } + + /// + /// Gets the address of the last sector on the disk. + /// + public ChsAddress LastSector + { + get { return new ChsAddress(Cylinders - 1, HeadsPerCylinder - 1, SectorsPerTrack); } + } + + /// + /// Gets a null geometry, which has 512-byte sectors but zero sectors, tracks or cylinders. + /// + public static Geometry Null + { + get { return new Geometry(0, 0, 0, 512); } + } + + /// + /// Gets the number of sectors per track. + /// + public int SectorsPerTrack { get; } + + /// + /// Gets the total size of the disk (in sectors). + /// + [Obsolete("Use TotalSectorsLong instead, to support very large disks.")] + public int TotalSectors + { + get { return Cylinders * HeadsPerCylinder * SectorsPerTrack; } + } + + /// + /// Gets the total size of the disk (in sectors). + /// + public long TotalSectorsLong + { + get { return Cylinders * (long)HeadsPerCylinder * SectorsPerTrack; } + } + + /// + /// Gets the 'Large' BIOS geometry for a disk, given it's physical geometry. + /// + /// The physical (aka IDE) geometry of the disk. + /// The geometry a BIOS using the 'Large' method for calculating disk geometry will indicate for the disk. + public static Geometry LargeBiosGeometry(Geometry ideGeometry) + { + int cylinders = ideGeometry.Cylinders; + int heads = ideGeometry.HeadsPerCylinder; + int sectors = ideGeometry.SectorsPerTrack; + + while (cylinders > 1024 && heads <= 127) + { + cylinders >>= 1; + heads <<= 1; + } + + return new Geometry(cylinders, heads, sectors); + } + + /// + /// Gets the 'LBA Assisted' BIOS geometry for a disk, given it's capacity. + /// + /// The capacity of the disk. + /// The geometry a BIOS using the 'LBA Assisted' method for calculating disk geometry will indicate for the disk. + public static Geometry LbaAssistedBiosGeometry(long capacity) + { + int heads; + if (capacity <= 504 * Sizes.OneMiB) + { + heads = 16; + } + else if (capacity <= 1008 * Sizes.OneMiB) + { + heads = 32; + } + else if (capacity <= 2016 * Sizes.OneMiB) + { + heads = 64; + } + else if (capacity <= 4032 * Sizes.OneMiB) + { + heads = 128; + } + else + { + heads = 255; + } + + int sectors = 63; + int cylinders = (int)Math.Min(1024, capacity / (sectors * (long)heads * Sizes.Sector)); + return new Geometry(cylinders, heads, sectors, Sizes.Sector); + } + + /// + /// Converts a geometry into one that is BIOS-safe, if not already. + /// + /// The geometry to make BIOS-safe. + /// The capacity of the disk. + /// The new geometry. + /// This method returns the LBA-Assisted geometry if the given geometry isn't BIOS-safe. + public static Geometry MakeBiosSafe(Geometry geometry, long capacity) + { + if (geometry == null) + { + return LbaAssistedBiosGeometry(capacity); + } + if (geometry.IsBiosSafe) + { + return geometry; + } + return LbaAssistedBiosGeometry(capacity); + } + + /// + /// Calculates a sensible disk geometry for a disk capacity using the VHD algorithm (errs under). + /// + /// The desired capacity of the disk. + /// The appropriate disk geometry. + /// The geometry returned tends to produce a disk with less capacity + /// than requested (an exact capacity is not always possible). The geometry returned is the IDE + /// (aka Physical) geometry of the disk, not necessarily the geometry used by the BIOS. + public static Geometry FromCapacity(long capacity) + { + return FromCapacity(capacity, Sizes.Sector); + } + + /// + /// Calculates a sensible disk geometry for a disk capacity using the VHD algorithm (errs under). + /// + /// The desired capacity of the disk. + /// The logical sector size of the disk. + /// The appropriate disk geometry. + /// The geometry returned tends to produce a disk with less capacity + /// than requested (an exact capacity is not always possible). The geometry returned is the IDE + /// (aka Physical) geometry of the disk, not necessarily the geometry used by the BIOS. + public static Geometry FromCapacity(long capacity, int sectorSize) + { + int totalSectors; + int cylinders; + int headsPerCylinder; + int sectorsPerTrack; + + // If more than ~128GB truncate at ~128GB + if (capacity > 65535 * (long)16 * 255 * sectorSize) + { + totalSectors = 65535 * 16 * 255; + } + else + { + totalSectors = (int)(capacity / sectorSize); + } + + // If more than ~32GB, break partition table compatibility. + // Partition table has max 63 sectors per track. Otherwise + // we're looking for a geometry that's valid for both BIOS + // and ATA. + if (totalSectors > 65535 * 16 * 63) + { + sectorsPerTrack = 255; + headsPerCylinder = 16; + } + else + { + sectorsPerTrack = 17; + int cylindersTimesHeads = totalSectors / sectorsPerTrack; + headsPerCylinder = (cylindersTimesHeads + 1023) / 1024; + + if (headsPerCylinder < 4) + { + headsPerCylinder = 4; + } + + // If we need more than 1023 cylinders, or 16 heads, try more sectors per track + if (cylindersTimesHeads >= headsPerCylinder * 1024U || headsPerCylinder > 16) + { + sectorsPerTrack = 31; + headsPerCylinder = 16; + cylindersTimesHeads = totalSectors / sectorsPerTrack; + } + + // We need 63 sectors per track to keep the cylinder count down + if (cylindersTimesHeads >= headsPerCylinder * 1024U) + { + sectorsPerTrack = 63; + headsPerCylinder = 16; + } + } + + cylinders = totalSectors / sectorsPerTrack / headsPerCylinder; + + return new Geometry(cylinders, headsPerCylinder, sectorsPerTrack, sectorSize); + } + + /// + /// Converts a CHS (Cylinder,Head,Sector) address to a LBA (Logical Block Address). + /// + /// The CHS address to convert. + /// The Logical Block Address (in sectors). + public long ToLogicalBlockAddress(ChsAddress chsAddress) + { + return ToLogicalBlockAddress(chsAddress.Cylinder, chsAddress.Head, chsAddress.Sector); + } + + /// + /// Converts a CHS (Cylinder,Head,Sector) address to a LBA (Logical Block Address). + /// + /// The cylinder of the address. + /// The head of the address. + /// The sector of the address. + /// The Logical Block Address (in sectors). + public long ToLogicalBlockAddress(int cylinder, int head, int sector) + { + if (cylinder < 0) + { + throw new ArgumentOutOfRangeException(nameof(cylinder), cylinder, "cylinder number is negative"); + } + + if (head >= HeadsPerCylinder) + { + throw new ArgumentOutOfRangeException(nameof(head), head, "head number is larger than disk geometry"); + } + + if (head < 0) + { + throw new ArgumentOutOfRangeException(nameof(head), head, "head number is negative"); + } + + if (sector > SectorsPerTrack) + { + throw new ArgumentOutOfRangeException(nameof(sector), sector, + "sector number is larger than disk geometry"); + } + + if (sector < 1) + { + throw new ArgumentOutOfRangeException(nameof(sector), sector, + "sector number is less than one (sectors are 1-based)"); + } + + return (cylinder * (long)HeadsPerCylinder + head) * SectorsPerTrack + sector - 1; + } + + /// + /// Converts a LBA (Logical Block Address) to a CHS (Cylinder, Head, Sector) address. + /// + /// The logical block address (in sectors). + /// The address in CHS form. + public ChsAddress ToChsAddress(long logicalBlockAddress) + { + if (logicalBlockAddress < 0) + { + throw new ArgumentOutOfRangeException(nameof(logicalBlockAddress), logicalBlockAddress, + "Logical Block Address is negative"); + } + + int cylinder = (int)(logicalBlockAddress / (HeadsPerCylinder * SectorsPerTrack)); + int temp = (int)(logicalBlockAddress % (HeadsPerCylinder * SectorsPerTrack)); + int head = temp / SectorsPerTrack; + int sector = temp % SectorsPerTrack + 1; + + return new ChsAddress(cylinder, head, sector); + } + + /// + /// Translates an IDE (aka Physical) geometry to a BIOS (aka Logical) geometry. + /// + /// The translation to perform. + /// The translated disk geometry. + public Geometry TranslateToBios(GeometryTranslation translation) + { + return TranslateToBios(0, translation); + } + + /// + /// Translates an IDE (aka Physical) geometry to a BIOS (aka Logical) geometry. + /// + /// The capacity of the disk, required if the geometry is an approximation on the actual disk size. + /// The translation to perform. + /// The translated disk geometry. + public Geometry TranslateToBios(long capacity, GeometryTranslation translation) + { + if (capacity <= 0) + { + capacity = TotalSectorsLong * 512L; + } + + switch (translation) + { + case GeometryTranslation.None: + return this; + + case GeometryTranslation.Auto: + if (IsBiosSafe) + { + return this; + } + return LbaAssistedBiosGeometry(capacity); + + case GeometryTranslation.Lba: + return LbaAssistedBiosGeometry(capacity); + + case GeometryTranslation.Large: + return LargeBiosGeometry(this); + + default: + throw new ArgumentException( + string.Format(CultureInfo.InvariantCulture, "Translation mode '{0}' not yet implemented", + translation), nameof(translation)); + } + } + + /// + /// Determines if this object is equivalent to another. + /// + /// The object to test against. + /// true if the is equivalent, else false. + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != GetType()) + { + return false; + } + + Geometry other = (Geometry)obj; + + return Cylinders == other.Cylinders && HeadsPerCylinder == other.HeadsPerCylinder + && SectorsPerTrack == other.SectorsPerTrack && BytesPerSector == other.BytesPerSector; + } + + /// + /// Calculates the hash code for this object. + /// + /// The hash code. + public override int GetHashCode() + { + return Cylinders.GetHashCode() ^ HeadsPerCylinder.GetHashCode() + ^ SectorsPerTrack.GetHashCode() ^ BytesPerSector.GetHashCode(); + } + + /// + /// Gets a string representation of this object, in the form (C/H/S). + /// + /// The string representation. + public override string ToString() + { + if (BytesPerSector == 512) + { + return "(" + Cylinders + "/" + HeadsPerCylinder + "/" + SectorsPerTrack + ")"; + } + return "(" + Cylinders + "/" + HeadsPerCylinder + "/" + SectorsPerTrack + ":" + BytesPerSector + ")"; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/GeometryCalculation.cs b/DiscUtils/Core/GeometryCalculation.cs new file mode 100644 index 0000000..6ce1c0a --- /dev/null +++ b/DiscUtils/Core/GeometryCalculation.cs @@ -0,0 +1,9 @@ +namespace DiscUtils +{ + /// + /// Delegate for calculating a disk geometry from a capacity. + /// + /// The disk capacity to convert. + /// The appropriate geometry for the disk. + public delegate Geometry GeometryCalculation(long capacity); +} \ No newline at end of file diff --git a/DiscUtils/Core/GeometryTranslation.cs b/DiscUtils/Core/GeometryTranslation.cs new file mode 100644 index 0000000..9b79514 --- /dev/null +++ b/DiscUtils/Core/GeometryTranslation.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// Enumeration of standard BIOS disk geometry translation methods. + /// + public enum GeometryTranslation + { + /// + /// Apply no translation. + /// + None = 0, + + /// + /// Automatic, based on the physical geometry select the most appropriate translation. + /// + Auto = 1, + + /// + /// LBA assisted translation, based on just the disk capacity. + /// + Lba = 2, + + /// + /// Bit-shifting translation, based on the physical geometry of the disk. + /// + Large = 3 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/IClusterBasedFileSystem.cs b/DiscUtils/Core/IClusterBasedFileSystem.cs new file mode 100644 index 0000000..71a4427 --- /dev/null +++ b/DiscUtils/Core/IClusterBasedFileSystem.cs @@ -0,0 +1,81 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Base class for all file systems based on a cluster model. + /// + public interface IClusterBasedFileSystem : IFileSystem + { + /// + /// Gets the size (in bytes) of each cluster. + /// + long ClusterSize { get; } + + /// + /// Gets the total number of clusters managed by the file system. + /// + long TotalClusters { get; } + + /// + /// Converts a cluster (index) into an absolute byte position in the underlying stream. + /// + /// The cluster to convert. + /// The corresponding absolute byte position. + long ClusterToOffset(long cluster); + + /// + /// Converts an absolute byte position in the underlying stream to a cluster (index). + /// + /// The byte position to convert. + /// The cluster containing the specified byte. + long OffsetToCluster(long offset); + + /// + /// Converts a file name to the list of clusters occupied by the file's data. + /// + /// The path to inspect. + /// The clusters. + /// Note that in some file systems, small files may not have dedicated + /// clusters. Only dedicated clusters will be returned. + Range[] PathToClusters(string path); + + /// + /// Converts a file name to the extents containing its data. + /// + /// The path to inspect. + /// The file extents, as absolute byte positions in the underlying stream. + /// Use this method with caution - not all file systems will store all bytes + /// directly in extents. Files may be compressed, sparse or encrypted. This method + /// merely indicates where file data is stored, not what's stored. + StreamExtent[] PathToExtents(string path); + + /// + /// Gets an object that can convert between clusters and files. + /// + /// The cluster map. + ClusterMap BuildClusterMap(); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/IDiagnosticTraceable.cs b/DiscUtils/Core/IDiagnosticTraceable.cs new file mode 100644 index 0000000..3911e8e --- /dev/null +++ b/DiscUtils/Core/IDiagnosticTraceable.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils +{ + /// + /// Interface exposed by objects that can provide a structured trace of their content. + /// + public interface IDiagnosticTraceable + { + /// + /// Writes a diagnostic report about the state of the object to a writer. + /// + /// The writer to send the report to. + /// The prefix to place at the start of each line. + void Dump(TextWriter writer, string linePrefix); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/IFileSystem.cs b/DiscUtils/Core/IFileSystem.cs new file mode 100644 index 0000000..2a38fd1 --- /dev/null +++ b/DiscUtils/Core/IFileSystem.cs @@ -0,0 +1,367 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Common interface for all file systems. + /// + public interface IFileSystem + { + /// + /// Gets a value indicating whether the file system is read-only or read-write. + /// + /// true if the file system is read-write. + bool CanWrite { get; } + + /// + /// Gets a value indicating whether the file system is thread-safe. + /// + bool IsThreadSafe { get; } + + /// + /// Gets the root directory of the file system. + /// + DiscDirectoryInfo Root { get; } + + /// + /// Copies an existing file to a new file. + /// + /// The source file. + /// The destination file. + void CopyFile(string sourceFile, string destinationFile); + + /// + /// Copies an existing file to a new file, allowing overwriting of an existing file. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + void CopyFile(string sourceFile, string destinationFile, bool overwrite); + + /// + /// Creates a directory. + /// + /// The path of the new directory. + void CreateDirectory(string path); + + /// + /// Deletes a directory. + /// + /// The path of the directory to delete. + void DeleteDirectory(string path); + + /// + /// Deletes a directory, optionally with all descendants. + /// + /// The path of the directory to delete. + /// Determines if the all descendants should be deleted. + void DeleteDirectory(string path, bool recursive); + + /// + /// Deletes a file. + /// + /// The path of the file to delete. + void DeleteFile(string path); + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + bool DirectoryExists(string path); + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + bool FileExists(string path); + + /// + /// Indicates if a file or directory exists. + /// + /// The path to test. + /// true if the file or directory exists. + bool Exists(string path); + + /// + /// Gets the names of subdirectories in a specified directory. + /// + /// The path to search. + /// Array of directories. + string[] GetDirectories(string path); + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of directories matching the search pattern. + string[] GetDirectories(string path, string searchPattern); + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + string[] GetDirectories(string path, string searchPattern, SearchOption searchOption); + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// Array of files. + string[] GetFiles(string path); + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// The search string to match against. + /// Array of files matching the search pattern. + string[] GetFiles(string path, string searchPattern); + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + string[] GetFiles(string path, string searchPattern, SearchOption searchOption); + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + string[] GetFileSystemEntries(string path); + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + string[] GetFileSystemEntries(string path, string searchPattern); + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName); + + /// + /// Moves a file. + /// + /// The file to move. + /// The target file name. + void MoveFile(string sourceName, string destinationName); + + /// + /// Moves a file, allowing an existing file to be overwritten. + /// + /// The file to move. + /// The target file name. + /// Whether to permit a destination file to be overwritten. + void MoveFile(string sourceName, string destinationName, bool overwrite); + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + SparseStream OpenFile(string path, FileMode mode); + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The new stream. + SparseStream OpenFile(string path, FileMode mode, FileAccess access); + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + FileAttributes GetAttributes(string path); + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + void SetAttributes(string path, FileAttributes newValue); + + /// + /// Gets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + DateTime GetCreationTime(string path); + + /// + /// Sets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetCreationTime(string path, DateTime newTime); + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + DateTime GetCreationTimeUtc(string path); + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetCreationTimeUtc(string path, DateTime newTime); + + /// + /// Gets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + DateTime GetLastAccessTime(string path); + + /// + /// Sets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetLastAccessTime(string path, DateTime newTime); + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + DateTime GetLastAccessTimeUtc(string path); + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetLastAccessTimeUtc(string path, DateTime newTime); + + /// + /// Gets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + DateTime GetLastWriteTime(string path); + + /// + /// Sets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetLastWriteTime(string path, DateTime newTime); + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + DateTime GetLastWriteTimeUtc(string path); + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + void SetLastWriteTimeUtc(string path, DateTime newTime); + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + long GetFileLength(string path); + + /// + /// Gets an object representing a possible file. + /// + /// The file path. + /// The representing object. + /// The file does not need to exist. + DiscFileInfo GetFileInfo(string path); + + /// + /// Gets an object representing a possible directory. + /// + /// The directory path. + /// The representing object. + /// The directory does not need to exist. + DiscDirectoryInfo GetDirectoryInfo(string path); + + /// + /// Gets an object representing a possible file system object (file or directory). + /// + /// The file system path. + /// The representing object. + /// The file system object does not need to exist. + DiscFileSystemInfo GetFileSystemInfo(string path); + + /// + /// Reads the boot code of the file system into a byte array. + /// + /// The boot code, or null if not available. + byte[] ReadBootCode(); + + /// + /// Size of the Filesystem in bytes + /// + long Size { get; } + + /// + /// Used space of the Filesystem in bytes + /// + long UsedSpace { get; } + + /// + /// Available space of the Filesystem in bytes + /// + long AvailableSpace { get; } + } +} diff --git a/DiscUtils/Core/IUnixFileSystem.cs b/DiscUtils/Core/IUnixFileSystem.cs new file mode 100644 index 0000000..9106c57 --- /dev/null +++ b/DiscUtils/Core/IUnixFileSystem.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// Provides the base class for all file systems that support Unix semantics. + /// + public interface IUnixFileSystem : IFileSystem + { + /// + /// Retrieves Unix-specific information about a file or directory. + /// + /// Path to the file or directory. + /// Information about the owner, group, permissions and type of the + /// file or directory. + UnixFileSystemInfo GetUnixFileInfo(string path); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/IWindowsFileSystem.cs b/DiscUtils/Core/IWindowsFileSystem.cs new file mode 100644 index 0000000..e7b92cd --- /dev/null +++ b/DiscUtils/Core/IWindowsFileSystem.cs @@ -0,0 +1,129 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Security.AccessControl; + +namespace DiscUtils +{ + /// + /// Provides the base class for all file systems that support Windows semantics. + /// + public interface IWindowsFileSystem : IFileSystem + { + /// + /// Gets the security descriptor associated with the file or directory. + /// + /// The file or directory to inspect. + /// The security descriptor. + RawSecurityDescriptor GetSecurity(string path); + + /// + /// Sets the security descriptor associated with the file or directory. + /// + /// The file or directory to change. + /// The new security descriptor. + void SetSecurity(string path, RawSecurityDescriptor securityDescriptor); + + /// + /// Gets the reparse point data associated with a file or directory. + /// + /// The file to query. + /// The reparse point information. + ReparsePoint GetReparsePoint(string path); + + /// + /// Sets the reparse point data on a file or directory. + /// + /// The file to set the reparse point on. + /// The new reparse point. + void SetReparsePoint(string path, ReparsePoint reparsePoint); + + /// + /// Removes a reparse point from a file or directory, without deleting the file or directory. + /// + /// The path to the file or directory to remove the reparse point from. + void RemoveReparsePoint(string path); + + /// + /// Gets the short name for a given path. + /// + /// The path to convert. + /// The short name. + /// + /// This method only gets the short name for the final part of the path, to + /// convert a complete path, call this method repeatedly, once for each path + /// segment. If there is no short name for the given path,null is + /// returned. + /// + string GetShortName(string path); + + /// + /// Sets the short name for a given file or directory. + /// + /// The full path to the file or directory to change. + /// The shortName, which should not include a path. + void SetShortName(string path, string shortName); + + /// + /// Gets the standard file information for a file. + /// + /// The full path to the file or directory to query. + /// The standard file information. + WindowsFileInformation GetFileStandardInformation(string path); + + /// + /// Sets the standard file information for a file. + /// + /// The full path to the file or directory to query. + /// The standard file information. + void SetFileStandardInformation(string path, WindowsFileInformation info); + + /// + /// Gets the names of the alternate data streams for a file. + /// + /// The path to the file. + /// + /// The list of alternate data streams (or empty, if none). To access the contents + /// of the alternate streams, use OpenFile(path + ":" + name, ...). + /// + string[] GetAlternateDataStreams(string path); + + /// + /// Gets the file id for a given path. + /// + /// The path to get the id of. + /// The file id, or -1. + /// + /// The returned file id uniquely identifies the file, and is shared by all hard + /// links to the same file. The value -1 indicates no unique identifier is + /// available, and so it can be assumed the file has no hard links. + /// + long GetFileId(string path); + + /// + /// Indicates whether the file is known by other names. + /// + /// The file to inspect. + /// true if the file has other names, else false. + bool HasHardLinks(string path); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/Crc32.cs b/DiscUtils/Core/Internal/Crc32.cs new file mode 100644 index 0000000..9a24617 --- /dev/null +++ b/DiscUtils/Core/Internal/Crc32.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Internal +{ + internal abstract class Crc32 + { + protected readonly uint[] Table; + protected uint _value; + + protected Crc32(uint[] table) + { + Table = table; + _value = 0xFFFFFFFF; + } + + public uint Value + { + get { return _value ^ 0xFFFFFFFF; } + } + + public abstract void Process(byte[] buffer, int offset, int count); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/Crc32Algorithm.cs b/DiscUtils/Core/Internal/Crc32Algorithm.cs new file mode 100644 index 0000000..decc6e0 --- /dev/null +++ b/DiscUtils/Core/Internal/Crc32Algorithm.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Internal +{ + internal enum Crc32Algorithm + { + /// + /// Used in Ethernet, PKZIP, BZIP2, Gzip, PNG, etc. (aka CRC32). + /// + Common = 0, + + /// + /// Used in iSCSI, SCTP, Btrfs, Vhdx. (aka CRC32C). + /// + Castagnoli = 1, + + /// + /// Unknown usage. (aka CRC32K). + /// + Koopman = 2, + + /// + /// Used in AIXM. (aka CRC32Q). + /// + Aeronautical = 3 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/Crc32BigEndian.cs b/DiscUtils/Core/Internal/Crc32BigEndian.cs new file mode 100644 index 0000000..6ffdc00 --- /dev/null +++ b/DiscUtils/Core/Internal/Crc32BigEndian.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Internal +{ + /// + /// Calculates CRC32 of buffers. + /// + internal sealed class Crc32BigEndian : Crc32 + { + private static readonly uint[][] Tables; + + static Crc32BigEndian() + { + Tables = new uint[4][]; + + Tables[(int)Crc32Algorithm.Common] = CalcTable(0x04C11DB7); + Tables[(int)Crc32Algorithm.Castagnoli] = CalcTable(0x1EDC6F41); + Tables[(int)Crc32Algorithm.Koopman] = CalcTable(0x741B8CD7); + Tables[(int)Crc32Algorithm.Aeronautical] = CalcTable(0x814141AB); + } + + public Crc32BigEndian(Crc32Algorithm algorithm) + : base(Tables[(int)algorithm]) {} + + public static uint Compute(Crc32Algorithm algorithm, byte[] buffer, int offset, int count) + { + return Process(Tables[(int)algorithm], 0xFFFFFFFF, buffer, offset, count) ^ 0xFFFFFFFF; + } + + public override void Process(byte[] buffer, int offset, int count) + { + _value = Process(Table, _value, buffer, offset, count); + } + + private static uint[] CalcTable(uint polynomial) + { + uint[] table = new uint[256]; + + for (uint i = 0; i < 256; ++i) + { + uint crc = i << 24; + + for (int j = 8; j > 0; --j) + { + if ((crc & 0x80000000) != 0) + { + crc = (crc << 1) ^ polynomial; + } + else + { + crc <<= 1; + } + } + + table[i] = crc; + } + + return table; + } + + private static uint Process(uint[] table, uint accumulator, byte[] buffer, int offset, int count) + { + uint value = accumulator; + + for (int i = 0; i < count; ++i) + { + byte b = buffer[offset + i]; + value = table[(value >> 24) ^ b] ^ (value << 8); + } + + return value; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/Crc32LittleEndian.cs b/DiscUtils/Core/Internal/Crc32LittleEndian.cs new file mode 100644 index 0000000..b7eb055 --- /dev/null +++ b/DiscUtils/Core/Internal/Crc32LittleEndian.cs @@ -0,0 +1,98 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Internal +{ + /// + /// Calculates CRC32 of buffers. + /// + internal sealed class Crc32LittleEndian : Crc32 + { + private static readonly uint[][] Tables; + + static Crc32LittleEndian() + { + Tables = new uint[4][]; + + Tables[(int)Crc32Algorithm.Common] = CalcTable(0xEDB88320); + Tables[(int)Crc32Algorithm.Castagnoli] = CalcTable(0x82F63B78); + Tables[(int)Crc32Algorithm.Koopman] = CalcTable(0xEB31D82E); + Tables[(int)Crc32Algorithm.Aeronautical] = CalcTable(0xD5828281); + } + + public Crc32LittleEndian(Crc32Algorithm algorithm) + : base(Tables[(int)algorithm]) {} + + public static uint Compute(Crc32Algorithm algorithm, byte[] buffer, int offset, int count) + { + return Process(Tables[(int)algorithm], 0xFFFFFFFF, buffer, offset, count) ^ 0xFFFFFFFF; + } + + public override void Process(byte[] buffer, int offset, int count) + { + _value = Process(Table, _value, buffer, offset, count); + } + + private static uint[] CalcTable(uint polynomial) + { + uint[] table = new uint[256]; + + table[0] = 0; + for (uint i = 0; i <= 255; ++i) + { + uint crc = i; + + for (int j = 8; j > 0; --j) + { + if ((crc & 1) != 0) + { + crc = (crc >> 1) ^ polynomial; + } + else + { + crc >>= 1; + } + } + + table[i] = crc; + } + + return table; + } + + private static uint Process(uint[] table, uint accumulator, byte[] buffer, int offset, int count) + { + uint value = accumulator; + + for (int i = 0; i < count; ++i) + { + byte b = buffer[offset + i]; + + uint temp1 = (value >> 8) & 0x00FFFFFF; + uint temp2 = table[(value ^ b) & 0xFF]; + value = temp1 ^ temp2; + } + + return value; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/LocalFileLocator.cs b/DiscUtils/Core/Internal/LocalFileLocator.cs new file mode 100644 index 0000000..0a03351 --- /dev/null +++ b/DiscUtils/Core/Internal/LocalFileLocator.cs @@ -0,0 +1,108 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Internal +{ + public sealed class LocalFileLocator : FileLocator + { + private readonly string _dir; + + public LocalFileLocator(string dir) + { + _dir = dir; + } + + public override bool Exists(string fileName) + { + return File.Exists(Path.Combine(_dir, fileName)); + } + + protected override Stream OpenFile(string fileName, FileMode mode, FileAccess access, FileShare share) + { + return new FileStream(Path.Combine(_dir, fileName), mode, access, share); + } + + public override FileLocator GetRelativeLocator(string path) + { + return new LocalFileLocator(Path.Combine(_dir, path)); + } + + public override string GetFullPath(string path) + { + string combinedPath = Path.Combine(_dir, path); + if (string.IsNullOrEmpty(combinedPath)) + { +#if NETSTANDARD1_5 + return Directory.GetCurrentDirectory(); +#else + return Environment.CurrentDirectory; +#endif + } + return Path.GetFullPath(combinedPath); + } + + public override string GetDirectoryFromPath(string path) + { + return Path.GetDirectoryName(path); + } + + public override string GetFileFromPath(string path) + { + return Path.GetFileName(path); + } + + public override DateTime GetLastWriteTimeUtc(string path) + { + return File.GetLastWriteTimeUtc(Path.Combine(_dir, path)); + } + + public override bool HasCommonRoot(FileLocator other) + { + LocalFileLocator otherLocal = other as LocalFileLocator; + if (otherLocal == null) + { + return false; + } + + // If the paths have drive specifiers, then common root depends on them having a common + // drive letter. + string otherDir = otherLocal._dir; + if (otherDir.Length >= 2 && _dir.Length >= 2) + { + if (otherDir[1] == ':' && _dir[1] == ':') + { + return char.ToUpperInvariant(otherDir[0]) == char.ToUpperInvariant(_dir[0]); + } + } + + return true; + } + + public override string ResolveRelativePath(string path) + { + return Utilities.ResolveRelativePath(_dir, path); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/LogicalVolumeFactory.cs b/DiscUtils/Core/Internal/LogicalVolumeFactory.cs new file mode 100644 index 0000000..acfdedc --- /dev/null +++ b/DiscUtils/Core/Internal/LogicalVolumeFactory.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; + +namespace DiscUtils.Internal +{ + internal abstract class LogicalVolumeFactory + { + public abstract bool HandlesPhysicalVolume(PhysicalVolumeInfo volume); + + public abstract void MapDisks(IEnumerable disks, Dictionary result); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/LogicalVolumeFactoryAttribute.cs b/DiscUtils/Core/Internal/LogicalVolumeFactoryAttribute.cs new file mode 100644 index 0000000..59fc39c --- /dev/null +++ b/DiscUtils/Core/Internal/LogicalVolumeFactoryAttribute.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Internal +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + internal sealed class LogicalVolumeFactoryAttribute : Attribute {} +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/ObjectCache.cs b/DiscUtils/Core/Internal/ObjectCache.cs new file mode 100644 index 0000000..17e3994 --- /dev/null +++ b/DiscUtils/Core/Internal/ObjectCache.cs @@ -0,0 +1,149 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Internal +{ + /// + /// Caches objects. + /// + /// The type of the object key. + /// The type of the objects to cache. + /// + /// Can be use for two purposes - to ensure there is only one instance of a given object, + /// and to prevent the need to recreate objects that are expensive to create. + /// + internal class ObjectCache + { + private const int MostRecentListSize = 20; + private const int PruneGap = 500; + + private readonly Dictionary _entries; + private int _nextPruneCount; + private readonly List> _recent; + + public ObjectCache() + { + _entries = new Dictionary(); + _recent = new List>(); + } + + public V this[K key] + { + get + { + for (int i = 0; i < _recent.Count; ++i) + { + KeyValuePair recentEntry = _recent[i]; + if (recentEntry.Key.Equals(key)) + { + MakeMostRecent(i); + return recentEntry.Value; + } + } + + WeakReference wRef; + if (_entries.TryGetValue(key, out wRef)) + { + V val = (V)wRef.Target; + if (val != null) + { + MakeMostRecent(key, val); + } + + return val; + } + + return default(V); + } + + set + { + _entries[key] = new WeakReference(value); + MakeMostRecent(key, value); + PruneEntries(); + } + } + + internal void Remove(K key) + { + for (int i = 0; i < _recent.Count; ++i) + { + if (_recent[i].Key.Equals(key)) + { + _recent.RemoveAt(i); + break; + } + } + + _entries.Remove(key); + } + + private void PruneEntries() + { + _nextPruneCount++; + + if (_nextPruneCount > PruneGap) + { + List toPrune = new List(); + foreach (KeyValuePair entry in _entries) + { + if (!entry.Value.IsAlive) + { + toPrune.Add(entry.Key); + } + } + + foreach (K key in toPrune) + { + _entries.Remove(key); + } + + _nextPruneCount = 0; + } + } + + private void MakeMostRecent(int i) + { + if (i == 0) + { + return; + } + + KeyValuePair entry = _recent[i]; + _recent.RemoveAt(i); + _recent.Insert(0, entry); + } + + private void MakeMostRecent(K key, V val) + { + while (_recent.Count >= MostRecentListSize) + { + _recent.RemoveAt(_recent.Count - 1); + } + + _recent.Insert(0, new KeyValuePair(key, val)); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/Utilities.cs b/DiscUtils/Core/Internal/Utilities.cs new file mode 100644 index 0000000..64c1cb3 --- /dev/null +++ b/DiscUtils/Core/Internal/Utilities.cs @@ -0,0 +1,467 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace DiscUtils.Internal +{ + public static class Utilities + { + /// + /// Converts between two arrays. + /// + /// The type of the elements of the source array. + /// The type of the elements of the destination array. + /// The source array. + /// The function to map from source type to destination type. + /// The resultant array. + public static U[] Map(ICollection source, Func func) + { + U[] result = new U[source.Count]; + int i = 0; + + foreach (T sVal in source) + { + result[i++] = func(sVal); + } + + return result; + } + + /// + /// Converts between two arrays. + /// + /// The type of the elements of the source array. + /// The type of the elements of the destination array. + /// The source array. + /// The function to map from source type to destination type. + /// The resultant array. + public static U[] Map(IEnumerable source, Func func) + { + List result = new List(); + + foreach (T sVal in source) + { + result.Add(func(sVal)); + } + + return result.ToArray(); + } + + /// + /// Filters a collection into a new collection. + /// + /// The type of the new collection. + /// The type of the collection entries. + /// The collection to filter. + /// The predicate to select which entries are carried over. + /// The new collection, containing all entries where the predicate returns true. + public static C Filter(ICollection source, Func predicate) where C : ICollection, new() + { + C result = new C(); + foreach (T val in source) + { + if (predicate(val)) + { + result.Add(val); + } + } + + return result; + } + + /// + /// Indicates if two ranges overlap. + /// + /// The type of the ordinals. + /// The lowest ordinal of the first range (inclusive). + /// The highest ordinal of the first range (exclusive). + /// The lowest ordinal of the second range (inclusive). + /// The highest ordinal of the second range (exclusive). + /// true if the ranges overlap, else false. + public static bool RangesOverlap(T xFirst, T xLast, T yFirst, T yLast) where T : IComparable + { + return !((xLast.CompareTo(yFirst) <= 0) || (xFirst.CompareTo(yLast) >= 0)); + } + + #region Bit Twiddling + + public static bool IsAllZeros(byte[] buffer, int offset, int count) + { + int end = offset + count; + for (int i = offset; i < end; ++i) + { + if (buffer[i] != 0) + { + return false; + } + } + + return true; + } + + public static bool IsPowerOfTwo(uint val) + { + if (val == 0) + { + return false; + } + + while ((val & 1) != 1) + { + val >>= 1; + } + + return val == 1; + } + + public static bool IsPowerOfTwo(long val) + { + if (val == 0) + { + return false; + } + + while ((val & 1) != 1) + { + val >>= 1; + } + + return val == 1; + } + + public static bool AreEqual(byte[] a, byte[] b) + { + if (a.Length != b.Length) + { + return false; + } + + for (int i = 0; i < a.Length; ++i) + { + if (a[i] != b[i]) + { + return false; + } + } + + return true; + } + + public static ushort BitSwap(ushort value) + { + return (ushort)(((value & 0x00FF) << 8) | ((value & 0xFF00) >> 8)); + } + + public static uint BitSwap(uint value) + { + return ((value & 0xFF) << 24) | ((value & 0xFF00) << 8) | ((value & 0x00FF0000) >> 8) | + ((value & 0xFF000000) >> 24); + } + + public static ulong BitSwap(ulong value) + { + return ((ulong)BitSwap((uint)(value & 0xFFFFFFFF)) << 32) | BitSwap((uint)(value >> 32)); + } + + public static short BitSwap(short value) + { + return (short)BitSwap((ushort)value); + } + + public static int BitSwap(int value) + { + return (int)BitSwap((uint)value); + } + + public static long BitSwap(long value) + { + return (long)BitSwap((ulong)value); + } + + #endregion + + #region Path Manipulation + + /// + /// Extracts the directory part of a path. + /// + /// The path to process. + /// The directory part. + public static string GetDirectoryFromPath(string path) + { + string trimmed = path.TrimEnd('\\'); + + int index = trimmed.LastIndexOf('\\'); + if (index < 0) + { + return string.Empty; // No directory, just a file name + } + + return trimmed.Substring(0, index); + } + + /// + /// Extracts the file part of a path. + /// + /// The path to process. + /// The file part of the path. + public static string GetFileFromPath(string path) + { + string trimmed = path.Trim('\\'); + + int index = trimmed.LastIndexOf('\\'); + if (index < 0) + { + return trimmed; // No directory, just a file name + } + + return trimmed.Substring(index + 1); + } + + /// + /// Combines two paths. + /// + /// The first part of the path. + /// The second part of the path. + /// The combined path. + public static string CombinePaths(string a, string b) + { + if (string.IsNullOrEmpty(a) || (b.Length > 0 && b[0] == '\\')) + { + return b; + } + if (string.IsNullOrEmpty(b)) + { + return a; + } + return a.TrimEnd('\\') + '\\' + b.TrimStart('\\'); + } + + /// + /// Resolves a relative path into an absolute one. + /// + /// The base path to resolve from. + /// The relative path. + /// The absolute path. If no is specified + /// then relativePath is returned as-is. If + /// contains more '..' characters than the base path contains levels of + /// directory, the resultant string be the root drive followed by the file name. + /// If no the basePath starts with '\' (no drive specified) then the returned + /// path will also start with '\'. + /// For example: (\TEMP\Foo.txt, ..\..\Bar.txt) gives (\Bar.txt). + /// + public static string ResolveRelativePath(string basePath, string relativePath) + { + if (string.IsNullOrEmpty(basePath)) + { + return relativePath; + } + + if (!basePath.EndsWith(@"\")) + basePath = Path.GetDirectoryName(basePath); + + string merged = Path.GetFullPath(Path.Combine(basePath, relativePath)); + + if (basePath.StartsWith(@"\") && merged.Length > 2 && merged[1].Equals(':')) + { + return merged.Substring(2); + } + + return merged; + } + + public static string ResolvePath(string basePath, string path) + { + if (!path.StartsWith("\\", StringComparison.OrdinalIgnoreCase)) + { + return ResolveRelativePath(basePath, path); + } + return path; + } + + public static string MakeRelativePath(string path, string basePath) + { + List pathElements = + new List(path.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)); + List basePathElements = + new List(basePath.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries)); + + if (!basePath.EndsWith("\\", StringComparison.Ordinal) && basePathElements.Count > 0) + { + basePathElements.RemoveAt(basePathElements.Count - 1); + } + + // Find first part of paths that don't match + int i = 0; + while (i < Math.Min(pathElements.Count - 1, basePathElements.Count)) + { + if (pathElements[i].ToUpperInvariant() != basePathElements[i].ToUpperInvariant()) + { + break; + } + + ++i; + } + + // For each remaining part of the base path, insert '..' + StringBuilder result = new StringBuilder(); + if (i == basePathElements.Count) + { + result.Append(@".\"); + } + else if (i < basePathElements.Count) + { + for (int j = 0; j < basePathElements.Count - i; ++j) + { + result.Append(@"..\"); + } + } + + // For each remaining part of the path, add the path element + for (int j = i; j < pathElements.Count - 1; ++j) + { + result.Append(pathElements[j]); + result.Append(@"\"); + } + + result.Append(pathElements[pathElements.Count - 1]); + + // If the target was a directory, put the terminator back + if (path.EndsWith(@"\", StringComparison.Ordinal)) + { + result.Append(@"\"); + } + + return result.ToString(); + } + + #endregion + + #region Filesystem Support + + /// + /// Indicates if a file name matches the 8.3 pattern. + /// + /// The name to test. + /// true if the name is 8.3, otherwise false. + public static bool Is8Dot3(string name) + { + if (name.Length > 12) + { + return false; + } + + string[] split = name.Split('.'); + + if (split.Length > 2 || split.Length < 1) + { + return false; + } + + if (split[0].Length > 8) + { + return false; + } + + foreach (char ch in split[0]) + { + if (!Is8Dot3Char(ch)) + { + return false; + } + } + + if (split.Length > 1) + { + if (split[1].Length > 3) + { + return false; + } + + foreach (char ch in split[1]) + { + if (!Is8Dot3Char(ch)) + { + return false; + } + } + } + + return true; + } + + public static bool Is8Dot3Char(char ch) + { + return (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || "_^$~!#%£-{}()@'`&".IndexOf(ch) != -1; + } + + /// + /// Converts a 'standard' wildcard file/path specification into a regular expression. + /// + /// The wildcard pattern to convert. + /// The resultant regular expression. + /// + /// The wildcard * (star) matches zero or more characters (including '.'), and ? + /// (question mark) matches precisely one character (except '.'). + /// + public static Regex ConvertWildcardsToRegEx(string pattern) + { + if (!pattern.Contains(".")) + { + pattern += "."; + } + + string query = "^" + Regex.Escape(pattern).Replace(@"\*", ".*").Replace(@"\?", "[^.]") + "$"; + return new Regex(query, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + } + + public static FileAttributes FileAttributesFromUnixFileType(UnixFileType fileType) + { + switch (fileType) + { + case UnixFileType.Fifo: + return FileAttributes.Device | FileAttributes.System; + case UnixFileType.Character: + return FileAttributes.Device | FileAttributes.System; + case UnixFileType.Directory: + return FileAttributes.Directory; + case UnixFileType.Block: + return FileAttributes.Device | FileAttributes.System; + case UnixFileType.Regular: + return FileAttributes.Normal; + case UnixFileType.Link: + return FileAttributes.ReparsePoint; + case UnixFileType.Socket: + return FileAttributes.Device | FileAttributes.System; + default: + return 0; + } + } + + #endregion + } +} diff --git a/DiscUtils/Core/Internal/VirtualDiskFactory.cs b/DiscUtils/Core/Internal/VirtualDiskFactory.cs new file mode 100644 index 0000000..814da9e --- /dev/null +++ b/DiscUtils/Core/Internal/VirtualDiskFactory.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; + +namespace DiscUtils.Internal +{ + internal abstract class VirtualDiskFactory + { + public abstract string[] Variants { get; } + + public abstract VirtualDiskTypeInfo GetDiskTypeInformation(string variant); + + public abstract DiskImageBuilder GetImageBuilder(string variant); + + public abstract VirtualDisk CreateDisk(FileLocator locator, string variant, string path, + VirtualDiskParameters diskParameters); + + public abstract VirtualDisk OpenDisk(string path, FileAccess access); + + public abstract VirtualDisk OpenDisk(FileLocator locator, string path, FileAccess access); + + public virtual VirtualDisk OpenDisk(FileLocator locator, string path, string extraInfo, + Dictionary parameters, FileAccess access) + { + return OpenDisk(locator, path, access); + } + + public VirtualDisk OpenDisk(DiscFileSystem fileSystem, string path, FileAccess access) + { + return OpenDisk(new DiscFileLocator(fileSystem, @"\"), path, access); + } + + public abstract VirtualDiskLayer OpenDiskLayer(FileLocator locator, string path, FileAccess access); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/VirtualDiskFactoryAttribute.cs b/DiscUtils/Core/Internal/VirtualDiskFactoryAttribute.cs new file mode 100644 index 0000000..3b56b03 --- /dev/null +++ b/DiscUtils/Core/Internal/VirtualDiskFactoryAttribute.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Internal +{ + [AttributeUsage(AttributeTargets.Class)] + internal sealed class VirtualDiskFactoryAttribute : Attribute + { + public VirtualDiskFactoryAttribute(string type, string fileExtensions) + { + Type = type; + FileExtensions = fileExtensions.Replace(".", string.Empty).Split(','); + } + + public string[] FileExtensions { get; } + + public string Type { get; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/VirtualDiskTransport.cs b/DiscUtils/Core/Internal/VirtualDiskTransport.cs new file mode 100644 index 0000000..2ad1427 --- /dev/null +++ b/DiscUtils/Core/Internal/VirtualDiskTransport.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Internal +{ + internal abstract class VirtualDiskTransport : IDisposable + { + public abstract bool IsRawDisk { get; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public abstract void Connect(Uri uri, string username, string password); + + public abstract VirtualDisk OpenDisk(FileAccess access); + + public abstract FileLocator GetFileLocator(); + + public abstract string GetFileName(); + + public abstract string GetExtraInfo(); + + protected virtual void Dispose(bool disposing) {} + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Internal/VirtualDiskTransportAttribute.cs b/DiscUtils/Core/Internal/VirtualDiskTransportAttribute.cs new file mode 100644 index 0000000..d42737d --- /dev/null +++ b/DiscUtils/Core/Internal/VirtualDiskTransportAttribute.cs @@ -0,0 +1,37 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Internal +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + internal sealed class VirtualDiskTransportAttribute : Attribute + { + public VirtualDiskTransportAttribute(string scheme) + { + Scheme = scheme; + } + + public string Scheme { get; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/InvalidFileSystemException.cs b/DiscUtils/Core/InvalidFileSystemException.cs new file mode 100644 index 0000000..601bb2b --- /dev/null +++ b/DiscUtils/Core/InvalidFileSystemException.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +#if !NETSTANDARD1_5 +using System.Runtime.Serialization; +#endif + +namespace DiscUtils +{ + /// + /// Exception thrown when some invalid file system data is found, indicating probably corruption. + /// +#if !NETSTANDARD1_5 + [Serializable] +#endif + public class InvalidFileSystemException : IOException + { + /// + /// Initializes a new instance of the InvalidFileSystemException class. + /// + public InvalidFileSystemException() {} + + /// + /// Initializes a new instance of the InvalidFileSystemException class. + /// + /// The exception message. + public InvalidFileSystemException(string message) + : base(message) {} + + /// + /// Initializes a new instance of the InvalidFileSystemException class. + /// + /// The exception message. + /// The inner exception. + public InvalidFileSystemException(string message, Exception innerException) + : base(message, innerException) {} + +#if !NETSTANDARD1_5 + /// + /// Initializes a new instance of the InvalidFileSystemException class. + /// + /// The serialization info. + /// The streaming context. + protected InvalidFileSystemException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } +#endif + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/ComponentRecord.cs b/DiscUtils/Core/LogicalDiskManager/ComponentRecord.cs new file mode 100644 index 0000000..99a8e5e --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/ComponentRecord.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal sealed class ComponentRecord : DatabaseRecord + { + public uint LinkId; // Identical on mirrors + public ExtentMergeType MergeType; // (02 Spanned, Simple, Mirrored) (01 on striped) + public ulong NumExtents; // Could be num disks + public string StatusString; + public long StripeSizeSectors; + public long StripeStride; // aka num partitions + public uint Unknown1; // Zero + public uint Unknown2; // Zero + public ulong Unknown3; // 00 .. 00 + public ulong Unknown4; // ?? + public ulong VolumeId; + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + StatusString = ReadVarString(buffer, ref pos); + MergeType = (ExtentMergeType)ReadByte(buffer, ref pos); + Unknown1 = ReadUInt(buffer, ref pos); // Zero + NumExtents = ReadVarULong(buffer, ref pos); + Unknown2 = ReadUInt(buffer, ref pos); + LinkId = ReadUInt(buffer, ref pos); + Unknown3 = ReadULong(buffer, ref pos); // Zero + VolumeId = ReadVarULong(buffer, ref pos); + Unknown4 = ReadVarULong(buffer, ref pos); // Zero + + if ((Flags & 0x1000) != 0) + { + StripeSizeSectors = ReadVarLong(buffer, ref pos); + StripeStride = ReadVarLong(buffer, ref pos); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/Database.cs b/DiscUtils/Core/LogicalDiskManager/Database.cs new file mode 100644 index 0000000..51459c2 --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/Database.cs @@ -0,0 +1,178 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Streams; + +namespace DiscUtils.LogicalDiskManager +{ + internal class Database + { + private readonly Dictionary _records; + private readonly DatabaseHeader _vmdb; + + public Database(Stream stream) + { + long dbStart = stream.Position; + + byte[] buffer = new byte[Sizes.Sector]; + stream.Read(buffer, 0, buffer.Length); + _vmdb = new DatabaseHeader(); + _vmdb.ReadFrom(buffer, 0); + + stream.Position = dbStart + _vmdb.HeaderSize; + + buffer = StreamUtilities.ReadExact(stream, (int)(_vmdb.BlockSize * _vmdb.NumVBlks)); + + _records = new Dictionary(); + for (int i = 0; i < _vmdb.NumVBlks; ++i) + { + DatabaseRecord rec = DatabaseRecord.ReadFrom(buffer, (int)(i * _vmdb.BlockSize)); + if (rec != null) + { + _records.Add(rec.Id, rec); + } + } + } + + internal IEnumerable Disks + { + get + { + foreach (DatabaseRecord record in _records.Values) + { + if (record.RecordType == RecordType.Disk) + { + yield return (DiskRecord)record; + } + } + } + } + + internal IEnumerable Volumes + { + get + { + foreach (DatabaseRecord record in _records.Values) + { + if (record.RecordType == RecordType.Volume) + { + yield return (VolumeRecord)record; + } + } + } + } + + internal DiskGroupRecord GetDiskGroup(Guid guid) + { + foreach (DatabaseRecord record in _records.Values) + { + if (record.RecordType == RecordType.DiskGroup) + { + DiskGroupRecord dgRecord = (DiskGroupRecord)record; + if (new Guid(dgRecord.GroupGuidString) == guid || guid == Guid.Empty) + { + return dgRecord; + } + } + } + + return null; + } + + internal IEnumerable GetVolumeComponents(ulong volumeId) + { + foreach (DatabaseRecord record in _records.Values) + { + if (record.RecordType == RecordType.Component) + { + ComponentRecord cmpntRecord = (ComponentRecord)record; + if (cmpntRecord.VolumeId == volumeId) + { + yield return cmpntRecord; + } + } + } + } + + internal IEnumerable GetComponentExtents(ulong componentId) + { + foreach (DatabaseRecord record in _records.Values) + { + if (record.RecordType == RecordType.Extent) + { + ExtentRecord extentRecord = (ExtentRecord)record; + if (extentRecord.ComponentId == componentId) + { + yield return extentRecord; + } + } + } + } + + internal DiskRecord GetDisk(ulong diskId) + { + return (DiskRecord)_records[diskId]; + } + + internal VolumeRecord GetVolume(ulong volumeId) + { + return (VolumeRecord)_records[volumeId]; + } + + internal VolumeRecord GetVolume(Guid id) + { + return FindRecord(r => r.VolumeGuid == id, RecordType.Volume); + } + + internal IEnumerable GetVolumes() + { + foreach (DatabaseRecord record in _records.Values) + { + if (record.RecordType == RecordType.Volume) + { + yield return (VolumeRecord)record; + } + } + } + + internal T FindRecord(Predicate pred, RecordType typeId) + where T : DatabaseRecord + { + foreach (DatabaseRecord record in _records.Values) + { + if (record.RecordType == typeId) + { + T t = (T)record; + if (pred(t)) + { + return t; + } + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/DatabaseHeader.cs b/DiscUtils/Core/LogicalDiskManager/DatabaseHeader.cs new file mode 100644 index 0000000..b978273 --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/DatabaseHeader.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.LogicalDiskManager +{ + internal class DatabaseHeader + { + public uint BlockSize; // 00 00 00 80 + public long CommittedSequence; // 0xA + public string DiskGroupId; + public string GroupName; + public uint HeaderSize; // 00 00 02 00 + public uint NumVBlks; // 00 00 17 24 + public long PendingSequence; // 0xA + public string Signature; // VMDB + public DateTime Timestamp; + public ushort Unknown1; // 00 01 + public uint Unknown2; // 1 + public uint Unknown3; // 1 + public uint Unknown4; // 3 + public uint Unknown5; // 3 + public long Unknown6; // 0 + public long Unknown7; // 1 + public uint Unknown8; // 1 + public uint Unknown9; // 3 + public uint UnknownA; // 3 + public long UnknownB; // 0 + public uint UnknownC; // 0 + public ushort VersionDenom; // 00 0a + public ushort VersionNum; // 00 04 + + public void ReadFrom(byte[] buffer, int offset) + { + Signature = EndianUtilities.BytesToString(buffer, offset + 0x00, 4); + NumVBlks = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x04); + BlockSize = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x08); + HeaderSize = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x0C); + Unknown1 = EndianUtilities.ToUInt16BigEndian(buffer, offset + 0x10); + VersionNum = EndianUtilities.ToUInt16BigEndian(buffer, offset + 0x12); + VersionDenom = EndianUtilities.ToUInt16BigEndian(buffer, offset + 0x14); + GroupName = EndianUtilities.BytesToString(buffer, offset + 0x16, 31).Trim('\0'); + DiskGroupId = EndianUtilities.BytesToString(buffer, offset + 0x35, 0x40).Trim('\0'); + + // May be wrong way round... + CommittedSequence = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x75); + PendingSequence = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x7D); + + Unknown2 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x85); + Unknown3 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x89); + Unknown4 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x8D); + Unknown5 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x91); + Unknown6 = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x95); + Unknown7 = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x9D); + Unknown8 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0xA5); + Unknown9 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0xA9); + UnknownA = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0xAD); + + UnknownB = EndianUtilities.ToInt64BigEndian(buffer, offset + 0xB1); + UnknownC = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0xB9); + + Timestamp = DateTime.FromFileTimeUtc(EndianUtilities.ToInt64BigEndian(buffer, offset + 0xBD)); + } + + ////} + //// throw new NotImplementedException(); + //// // Add all byte values for ?? bytes + //// // Zero checksum bytes (0x08, 4) + ////{ + + ////private static int CalcChecksum() + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/DatabaseRecord.cs b/DiscUtils/Core/LogicalDiskManager/DatabaseRecord.cs new file mode 100644 index 0000000..017947d --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/DatabaseRecord.cs @@ -0,0 +1,154 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.LogicalDiskManager +{ + internal abstract class DatabaseRecord + { + public uint Counter; + public uint DataLength; + public uint Flags; + + public ulong Id; + public uint Label; + public string Name; + public RecordType RecordType; + public string Signature; // VBLK + public uint Valid; + + public static DatabaseRecord ReadFrom(byte[] buffer, int offset) + { + DatabaseRecord result = null; + + if (EndianUtilities.ToInt32BigEndian(buffer, offset + 0xC) != 0) + { + switch ((RecordType)(buffer[offset + 0x13] & 0xF)) + { + case RecordType.Volume: + result = new VolumeRecord(); + break; + + case RecordType.Component: + result = new ComponentRecord(); + break; + + case RecordType.Extent: + result = new ExtentRecord(); + break; + + case RecordType.Disk: + result = new DiskRecord(); + break; + + case RecordType.DiskGroup: + result = new DiskGroupRecord(); + break; + + default: + throw new NotImplementedException("Unrecognized record type: " + buffer[offset + 0x13]); + } + + result.DoReadFrom(buffer, offset); + } + + return result; + } + + protected static ulong ReadVarULong(byte[] buffer, ref int offset) + { + int length = buffer[offset]; + + ulong result = 0; + for (int i = 0; i < length; ++i) + { + result = (result << 8) | buffer[offset + i + 1]; + } + + offset += length + 1; + + return result; + } + + protected static long ReadVarLong(byte[] buffer, ref int offset) + { + return (long)ReadVarULong(buffer, ref offset); + } + + protected static string ReadVarString(byte[] buffer, ref int offset) + { + int length = buffer[offset]; + + string result = EndianUtilities.BytesToString(buffer, offset + 1, length); + offset += length + 1; + return result; + } + + protected static byte ReadByte(byte[] buffer, ref int offset) + { + return buffer[offset++]; + } + + protected static uint ReadUInt(byte[] buffer, ref int offset) + { + offset += 4; + return EndianUtilities.ToUInt32BigEndian(buffer, offset - 4); + } + + protected static long ReadLong(byte[] buffer, ref int offset) + { + offset += 8; + return EndianUtilities.ToInt64BigEndian(buffer, offset - 8); + } + + protected static ulong ReadULong(byte[] buffer, ref int offset) + { + offset += 8; + return EndianUtilities.ToUInt64BigEndian(buffer, offset - 8); + } + + protected static string ReadString(byte[] buffer, int len, ref int offset) + { + offset += len; + return EndianUtilities.BytesToString(buffer, offset - len, len); + } + + protected static Guid ReadBinaryGuid(byte[] buffer, ref int offset) + { + offset += 16; + return EndianUtilities.ToGuidBigEndian(buffer, offset - 16); + } + + protected virtual void DoReadFrom(byte[] buffer, int offset) + { + Signature = EndianUtilities.BytesToString(buffer, offset + 0x00, 4); + Label = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x04); + Counter = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x08); + Valid = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x0C); + Flags = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x10); + RecordType = (RecordType)(Flags & 0xF); + DataLength = EndianUtilities.ToUInt32BigEndian(buffer, 0x14); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/DiskGroupRecord.cs b/DiscUtils/Core/LogicalDiskManager/DiskGroupRecord.cs new file mode 100644 index 0000000..6742ea2 --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/DiskGroupRecord.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal sealed class DiskGroupRecord : DatabaseRecord + { + public string GroupGuidString; + public uint Unknown1; + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + if ((Flags & 0xF0) == 0x40) + { + GroupGuidString = ReadBinaryGuid(buffer, ref pos).ToString(); + } + else + { + GroupGuidString = ReadVarString(buffer, ref pos); + } + Unknown1 = ReadUInt(buffer, ref pos); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/DiskRecord.cs b/DiscUtils/Core/LogicalDiskManager/DiskRecord.cs new file mode 100644 index 0000000..b3674a2 --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/DiskRecord.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal sealed class DiskRecord : DatabaseRecord + { + public string DiskGuidString; + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + if ((Flags & 0xF0) == 0x40) + { + DiskGuidString = ReadBinaryGuid(buffer, ref pos).ToString(); + } + else + { + DiskGuidString = ReadVarString(buffer, ref pos); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicDisk.cs b/DiscUtils/Core/LogicalDiskManager/DynamicDisk.cs new file mode 100644 index 0000000..a6f453a --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/DynamicDisk.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Partitions; +using DiscUtils.Streams; + +namespace DiscUtils.LogicalDiskManager +{ + internal class DynamicDisk : IDiagnosticTraceable + { + private readonly VirtualDisk _disk; + private readonly PrivateHeader _header; + + internal DynamicDisk(VirtualDisk disk) + { + _disk = disk; + _header = GetPrivateHeader(_disk); + + TocBlock toc = GetTableOfContents(); + + long dbStart = _header.ConfigurationStartLba * 512 + toc.Item1Start * 512; + _disk.Content.Position = dbStart; + Database = new Database(_disk.Content); + } + + public SparseStream Content + { + get { return _disk.Content; } + } + + public Database Database { get; } + + public long DataOffset + { + get { return _header.DataStartLba; } + } + + public Guid GroupId + { + get { return string.IsNullOrEmpty(_header.DiskGroupId) ? Guid.Empty : new Guid(_header.DiskGroupId); } + } + + public Guid Id + { + get { return new Guid(_header.DiskId); } + } + + public void Dump(TextWriter writer, string linePrefix) + { + writer.WriteLine(linePrefix + "DISK (" + _header.DiskId + ")"); + writer.WriteLine(linePrefix + " Metadata Version: " + ((_header.Version >> 16) & 0xFFFF) + "." + + (_header.Version & 0xFFFF)); + writer.WriteLine(linePrefix + " Timestamp: " + _header.Timestamp); + writer.WriteLine(linePrefix + " Disk Id: " + _header.DiskId); + writer.WriteLine(linePrefix + " Host Id: " + _header.HostId); + writer.WriteLine(linePrefix + " Disk Group Id: " + _header.DiskGroupId); + writer.WriteLine(linePrefix + " Disk Group Name: " + _header.DiskGroupName); + writer.WriteLine(linePrefix + " Data Start: " + _header.DataStartLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Data Size: " + _header.DataSizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Configuration Start: " + _header.ConfigurationStartLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Configuration Size: " + _header.ConfigurationSizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " TOC Size: " + _header.TocSizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Next TOC: " + _header.NextTocLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Number of Configs: " + _header.NumberOfConfigs); + writer.WriteLine(linePrefix + " Config Size: " + _header.ConfigurationSizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Number of Logs: " + _header.NumberOfLogs); + writer.WriteLine(linePrefix + " Log Size: " + _header.LogSizeLba + " (Sectors)"); + } + + internal static PrivateHeader GetPrivateHeader(VirtualDisk disk) + { + if (disk.IsPartitioned) + { + long headerPos = 0; + PartitionTable pt = disk.Partitions; + if (pt is BiosPartitionTable) + { + headerPos = 0xc00; + } + else + { + foreach (PartitionInfo part in pt.Partitions) + { + if (part.GuidType == GuidPartitionTypes.WindowsLdmMetadata) + { + headerPos = part.LastSector * Sizes.Sector; + } + } + } + + if (headerPos != 0) + { + disk.Content.Position = headerPos; + byte[] buffer = new byte[Sizes.Sector]; + disk.Content.Read(buffer, 0, buffer.Length); + + PrivateHeader hdr = new PrivateHeader(); + hdr.ReadFrom(buffer, 0); + return hdr; + } + } + + return null; + } + + private TocBlock GetTableOfContents() + { + byte[] buffer = new byte[_header.TocSizeLba * 512]; + _disk.Content.Position = _header.ConfigurationStartLba * 512 + 1 * _header.TocSizeLba * 512; + + _disk.Content.Read(buffer, 0, buffer.Length); + TocBlock tocBlock = new TocBlock(); + tocBlock.ReadFrom(buffer, 0); + + if (tocBlock.Signature == "TOCBLOCK") + { + return tocBlock; + } + + return null; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicDiskGroup.cs b/DiscUtils/Core/LogicalDiskManager/DynamicDiskGroup.cs new file mode 100644 index 0000000..725714b --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/DynamicDiskGroup.cs @@ -0,0 +1,322 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using DiscUtils.Partitions; +using DiscUtils.Streams; + +namespace DiscUtils.LogicalDiskManager +{ + internal class DynamicDiskGroup : IDiagnosticTraceable + { + private readonly Database _database; + private readonly Dictionary _disks; + private readonly DiskGroupRecord _record; + + internal DynamicDiskGroup(VirtualDisk disk) + { + _disks = new Dictionary(); + + DynamicDisk dynDisk = new DynamicDisk(disk); + _database = dynDisk.Database; + _disks.Add(dynDisk.Id, dynDisk); + _record = dynDisk.Database.GetDiskGroup(dynDisk.GroupId); + } + + #region IDiagnosticTraceable Members + + public void Dump(TextWriter writer, string linePrefix) + { + writer.WriteLine(linePrefix + "DISK GROUP (" + _record.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + _record.Name); + writer.WriteLine(linePrefix + " Flags: 0x" + + (_record.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + _record.Id); + writer.WriteLine(linePrefix + " Guid: " + _record.GroupGuidString); + writer.WriteLine(); + + writer.WriteLine(linePrefix + " DISKS"); + foreach (DiskRecord disk in _database.Disks) + { + writer.WriteLine(linePrefix + " DISK (" + disk.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + disk.Name); + writer.WriteLine(linePrefix + " Flags: 0x" + + (disk.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + disk.Id); + writer.WriteLine(linePrefix + " Guid: " + disk.DiskGuidString); + + DynamicDisk dynDisk; + if (_disks.TryGetValue(new Guid(disk.DiskGuidString), out dynDisk)) + { + writer.WriteLine(linePrefix + " PRIVATE HEADER"); + dynDisk.Dump(writer, linePrefix + " "); + } + } + + writer.WriteLine(linePrefix + " VOLUMES"); + foreach (VolumeRecord vol in _database.Volumes) + { + writer.WriteLine(linePrefix + " VOLUME (" + vol.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + vol.Name); + writer.WriteLine(linePrefix + " BIOS Type: " + + vol.BiosType.ToString("X2", CultureInfo.InvariantCulture) + " [" + + BiosPartitionTypes.ToString(vol.BiosType) + "]"); + writer.WriteLine(linePrefix + " Flags: 0x" + + (vol.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + vol.Id); + writer.WriteLine(linePrefix + " Guid: " + vol.VolumeGuid); + writer.WriteLine(linePrefix + " State: " + vol.ActiveString); + writer.WriteLine(linePrefix + " Drive Hint: " + vol.MountHint); + writer.WriteLine(linePrefix + " Num Components: " + vol.ComponentCount); + writer.WriteLine(linePrefix + " Link Id: " + vol.PartitionComponentLink); + + writer.WriteLine(linePrefix + " COMPONENTS"); + foreach (ComponentRecord cmpnt in _database.GetVolumeComponents(vol.Id)) + { + writer.WriteLine(linePrefix + " COMPONENT (" + cmpnt.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + cmpnt.Name); + writer.WriteLine(linePrefix + " Flags: 0x" + + (cmpnt.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + cmpnt.Id); + writer.WriteLine(linePrefix + " State: " + cmpnt.StatusString); + writer.WriteLine(linePrefix + " Mode: " + cmpnt.MergeType); + writer.WriteLine(linePrefix + " Num Extents: " + cmpnt.NumExtents); + writer.WriteLine(linePrefix + " Link Id: " + cmpnt.LinkId); + writer.WriteLine(linePrefix + " Stripe Size: " + cmpnt.StripeSizeSectors + " (Sectors)"); + writer.WriteLine(linePrefix + " Stripe Stride: " + cmpnt.StripeStride); + + writer.WriteLine(linePrefix + " EXTENTS"); + foreach (ExtentRecord extent in _database.GetComponentExtents(cmpnt.Id)) + { + writer.WriteLine(linePrefix + " EXTENT (" + extent.Name + ")"); + writer.WriteLine(linePrefix + " Name: " + extent.Name); + writer.WriteLine(linePrefix + " Flags: 0x" + + (extent.Flags & 0xFFF0).ToString("X4", CultureInfo.InvariantCulture)); + writer.WriteLine(linePrefix + " Database Id: " + extent.Id); + writer.WriteLine(linePrefix + " Disk Offset: " + extent.DiskOffsetLba + + " (Sectors)"); + writer.WriteLine(linePrefix + " Volume Offset: " + extent.OffsetInVolumeLba + + " (Sectors)"); + writer.WriteLine(linePrefix + " Size: " + extent.SizeLba + " (Sectors)"); + writer.WriteLine(linePrefix + " Component Id: " + extent.ComponentId); + writer.WriteLine(linePrefix + " Disk Id: " + extent.DiskId); + writer.WriteLine(linePrefix + " Link Id: " + extent.PartitionComponentLink); + writer.WriteLine(linePrefix + " Interleave Order: " + extent.InterleaveOrder); + } + } + } + } + + #endregion + + public void Add(VirtualDisk disk) + { + DynamicDisk dynDisk = new DynamicDisk(disk); + _disks.Add(dynDisk.Id, dynDisk); + } + + internal DynamicVolume[] GetVolumes() + { + List vols = new List(); + foreach (VolumeRecord record in _database.GetVolumes()) + { + vols.Add(new DynamicVolume(this, record.VolumeGuid)); + } + + return vols.ToArray(); + } + + internal VolumeRecord GetVolume(Guid volume) + { + return _database.GetVolume(volume); + } + + internal LogicalVolumeStatus GetVolumeStatus(ulong volumeId) + { + return GetVolumeStatus(_database.GetVolume(volumeId)); + } + + internal SparseStream OpenVolume(ulong volumeId) + { + return OpenVolume(_database.GetVolume(volumeId)); + } + + private static int CompareExtentOffsets(ExtentRecord x, ExtentRecord y) + { + if (x.OffsetInVolumeLba > y.OffsetInVolumeLba) + { + return 1; + } + if (x.OffsetInVolumeLba < y.OffsetInVolumeLba) + { + return -1; + } + + return 0; + } + + private static int CompareExtentInterleaveOrder(ExtentRecord x, ExtentRecord y) + { + if (x.InterleaveOrder > y.InterleaveOrder) + { + return 1; + } + if (x.InterleaveOrder < y.InterleaveOrder) + { + return -1; + } + + return 0; + } + + private static LogicalVolumeStatus WorstOf(LogicalVolumeStatus x, LogicalVolumeStatus y) + { + return (LogicalVolumeStatus)Math.Max((int)x, (int)y); + } + + private LogicalVolumeStatus GetVolumeStatus(VolumeRecord volume) + { + int numFailed = 0; + ulong numOK = 0; + LogicalVolumeStatus worst = LogicalVolumeStatus.Healthy; + foreach (ComponentRecord cmpnt in _database.GetVolumeComponents(volume.Id)) + { + LogicalVolumeStatus cmpntStatus = GetComponentStatus(cmpnt); + worst = WorstOf(worst, cmpntStatus); + if (cmpntStatus == LogicalVolumeStatus.Failed) + { + numFailed++; + } + else + { + numOK++; + } + } + + if (numOK < 1) + { + return LogicalVolumeStatus.Failed; + } + if (numOK == volume.ComponentCount) + { + return worst; + } + return LogicalVolumeStatus.FailedRedundancy; + } + + private LogicalVolumeStatus GetComponentStatus(ComponentRecord cmpnt) + { + // NOTE: no support for RAID, so either valid or failed... + LogicalVolumeStatus status = LogicalVolumeStatus.Healthy; + + foreach (ExtentRecord extent in _database.GetComponentExtents(cmpnt.Id)) + { + DiskRecord disk = _database.GetDisk(extent.DiskId); + if (!_disks.ContainsKey(new Guid(disk.DiskGuidString))) + { + status = LogicalVolumeStatus.Failed; + break; + } + } + + return status; + } + + private SparseStream OpenExtent(ExtentRecord extent) + { + DiskRecord disk = _database.GetDisk(extent.DiskId); + + DynamicDisk diskObj = _disks[new Guid(disk.DiskGuidString)]; + + return new SubStream(diskObj.Content, Ownership.None, + (diskObj.DataOffset + extent.DiskOffsetLba) * Sizes.Sector, extent.SizeLba * Sizes.Sector); + } + + private SparseStream OpenComponent(ComponentRecord component) + { + if (component.MergeType == ExtentMergeType.Concatenated) + { + List extents = new List(_database.GetComponentExtents(component.Id)); + extents.Sort(CompareExtentOffsets); + + // Sanity Check... + long pos = 0; + foreach (ExtentRecord extent in extents) + { + if (extent.OffsetInVolumeLba != pos) + { + throw new IOException("Volume extents are non-contiguous"); + } + + pos += extent.SizeLba; + } + + List streams = new List(); + foreach (ExtentRecord extent in extents) + { + streams.Add(OpenExtent(extent)); + } + + return new ConcatStream(Ownership.Dispose, streams.ToArray()); + } + if (component.MergeType == ExtentMergeType.Interleaved) + { + List extents = new List(_database.GetComponentExtents(component.Id)); + extents.Sort(CompareExtentInterleaveOrder); + + List streams = new List(); + foreach (ExtentRecord extent in extents) + { + streams.Add(OpenExtent(extent)); + } + + return new StripedStream(component.StripeSizeSectors * Sizes.Sector, Ownership.Dispose, streams.ToArray()); + } + throw new NotImplementedException("Unknown component mode: " + component.MergeType); + } + + private SparseStream OpenVolume(VolumeRecord volume) + { + List cmpntStreams = new List(); + foreach (ComponentRecord component in _database.GetVolumeComponents(volume.Id)) + { + if (GetComponentStatus(component) == LogicalVolumeStatus.Healthy) + { + cmpntStreams.Add(OpenComponent(component)); + } + } + + if (cmpntStreams.Count < 1) + { + throw new IOException("Volume with no associated or healthy components"); + } + if (cmpntStreams.Count == 1) + { + return cmpntStreams[0]; + } + return new MirrorStream(Ownership.Dispose, cmpntStreams.ToArray()); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicDiskManager.cs b/DiscUtils/Core/LogicalDiskManager/DynamicDiskManager.cs new file mode 100644 index 0000000..d2828eb --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/DynamicDiskManager.cs @@ -0,0 +1,153 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; +using DiscUtils.Partitions; + +namespace DiscUtils.LogicalDiskManager +{ + /// + /// A class that understands Windows LDM structures, mapping physical volumes to logical volumes. + /// + public class DynamicDiskManager : IDiagnosticTraceable + { + private readonly Dictionary _groups; + + /// + /// Initializes a new instance of the DynamicDiskManager class. + /// + /// The initial set of disks to manage. + public DynamicDiskManager(params VirtualDisk[] disks) + { + _groups = new Dictionary(); + + foreach (VirtualDisk disk in disks) + { + Add(disk); + } + } + + /// + /// Writes a diagnostic report about the state of the disk manager. + /// + /// The writer to send the report to. + /// The prefix to place at the start of each line. + public void Dump(TextWriter writer, string linePrefix) + { + writer.WriteLine(linePrefix + "DISK GROUPS"); + foreach (DynamicDiskGroup group in _groups.Values) + { + group.Dump(writer, linePrefix + " "); + } + } + + /// + /// Determines if a physical volume contains LDM data. + /// + /// The volume to inspect. + /// true if the physical volume contains LDM data, else false. + public static bool HandlesPhysicalVolume(PhysicalVolumeInfo volumeInfo) + { + PartitionInfo pi = volumeInfo.Partition; + if (pi != null) + { + return IsLdmPartition(pi); + } + + return false; + } + + /// + /// Determines if a disk is 'dynamic' (i.e. contains LDM volumes). + /// + /// The disk to inspect. + /// true if the disk contains LDM volumes, else false. + public static bool IsDynamicDisk(VirtualDisk disk) + { + if (disk.IsPartitioned) + { + foreach (PartitionInfo partition in disk.Partitions.Partitions) + { + if (IsLdmPartition(partition)) + { + return true; + } + } + } + + return false; + } + + /// + /// Adds a new disk to be managed. + /// + /// The disk to manage. + public void Add(VirtualDisk disk) + { + PrivateHeader header = DynamicDisk.GetPrivateHeader(disk); + + DynamicDiskGroup group; + if (_groups.TryGetValue(header.DiskGroupId, out group)) + { + group.Add(disk); + } + else + { + group = new DynamicDiskGroup(disk); + _groups.Add(header.DiskGroupId, group); + } + } + + /// + /// Gets the logical volumes held across the set of managed disks. + /// + /// An array of logical volumes. + public LogicalVolumeInfo[] GetLogicalVolumes() + { + List result = new List(); + foreach (DynamicDiskGroup group in _groups.Values) + { + foreach (DynamicVolume volume in group.GetVolumes()) + { + LogicalVolumeInfo lvi = new LogicalVolumeInfo( + volume.Identity, + null, + volume.Open, + volume.Length, + volume.BiosType, + volume.Status); + result.Add(lvi); + } + } + + return result.ToArray(); + } + + private static bool IsLdmPartition(PartitionInfo partition) + { + return partition.BiosType == BiosPartitionTypes.WindowsDynamicVolume + || partition.GuidType == GuidPartitionTypes.WindowsLdmMetadata + || partition.GuidType == GuidPartitionTypes.WindowsLdmData; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicDiskManagerFactory.cs b/DiscUtils/Core/LogicalDiskManager/DynamicDiskManagerFactory.cs new file mode 100644 index 0000000..ed9afa2 --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/DynamicDiskManagerFactory.cs @@ -0,0 +1,54 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using DiscUtils.Internal; + +namespace DiscUtils.LogicalDiskManager +{ + [LogicalVolumeFactory] + internal class DynamicDiskManagerFactory : LogicalVolumeFactory + { + public override bool HandlesPhysicalVolume(PhysicalVolumeInfo volume) + { + return DynamicDiskManager.HandlesPhysicalVolume(volume); + } + + public override void MapDisks(IEnumerable disks, Dictionary result) + { + DynamicDiskManager mgr = new DynamicDiskManager(); + + foreach (VirtualDisk disk in disks) + { + if (DynamicDiskManager.IsDynamicDisk(disk)) + { + mgr.Add(disk); + } + } + + foreach (LogicalVolumeInfo vol in mgr.GetLogicalVolumes()) + { + result.Add(vol.Identity, vol); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicVolume.cs b/DiscUtils/Core/LogicalDiskManager/DynamicVolume.cs new file mode 100644 index 0000000..146c67f --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/DynamicVolume.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.LogicalDiskManager +{ + internal class DynamicVolume + { + private readonly DynamicDiskGroup _group; + + internal DynamicVolume(DynamicDiskGroup group, Guid volumeId) + { + _group = group; + Identity = volumeId; + } + + public byte BiosType + { + get { return Record.BiosType; } + } + + public Guid Identity { get; } + + public long Length + { + get { return Record.Size * Sizes.Sector; } + } + + private VolumeRecord Record + { + get { return _group.GetVolume(Identity); } + } + + public LogicalVolumeStatus Status + { + get { return _group.GetVolumeStatus(Record.Id); } + } + + public SparseStream Open() + { + if (Status == LogicalVolumeStatus.Failed) + { + throw new IOException("Attempt to open 'failed' volume"); + } + return _group.OpenVolume(Record.Id); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/ExtentMergeType.cs b/DiscUtils/Core/LogicalDiskManager/ExtentMergeType.cs new file mode 100644 index 0000000..f10d983 --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/ExtentMergeType.cs @@ -0,0 +1,31 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal enum ExtentMergeType : byte + { + None = 0, + Interleaved = 1, + Concatenated = 2 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/ExtentRecord.cs b/DiscUtils/Core/LogicalDiskManager/ExtentRecord.cs new file mode 100644 index 0000000..b612c3a --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/ExtentRecord.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal sealed class ExtentRecord : DatabaseRecord + { + public ulong ComponentId; + public ulong DiskId; + public long DiskOffsetLba; + public ulong InterleaveOrder; + public long OffsetInVolumeLba; + public uint PartitionComponentLink; + public long SizeLba; + public uint Unknown1; + public uint Unknown2; + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + Unknown1 = ReadUInt(buffer, ref pos); + Unknown2 = ReadUInt(buffer, ref pos); + PartitionComponentLink = ReadUInt(buffer, ref pos); + DiskOffsetLba = ReadLong(buffer, ref pos); + OffsetInVolumeLba = ReadLong(buffer, ref pos); + SizeLba = ReadVarLong(buffer, ref pos); + ComponentId = ReadVarULong(buffer, ref pos); + DiskId = ReadVarULong(buffer, ref pos); + + if ((Flags & 0x0800) != 0) + { + InterleaveOrder = ReadVarULong(buffer, ref pos); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/PrivateHeader.cs b/DiscUtils/Core/LogicalDiskManager/PrivateHeader.cs new file mode 100644 index 0000000..fa6f375 --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/PrivateHeader.cs @@ -0,0 +1,90 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.LogicalDiskManager +{ + internal class PrivateHeader + { + public uint Checksum; // 00 00 2f 96 + public long ConfigSizeLba; + public long ConfigurationSizeLba; // 08 00 + public long ConfigurationStartLba; // 03 FF F8 00 + public long DataSizeLba; // 03 FF F7 C1 + public long DataStartLba; // 3F + public string DiskGroupId; // GUID string + public string DiskGroupName; // MAX_COMPUTER_NAME_LENGTH? + public string DiskId; // GUID string + public string HostId; // GUID string + public long LogSizeLba; + public long NextTocLba; + public long NumberOfConfigs; + public long NumberOfLogs; + public string Signature; // PRIVHEAD + public DateTime Timestamp; + public long TocSizeLba; + public long Unknown2; // Active TOC? 00 .. 00 01 + public long Unknown3; // 00 .. 07 ff // 1 sector less than 2MB + public long Unknown4; // 00 .. 07 40 + public uint Unknown5; // Sector Size? + public uint Version; // 2.12 + + public void ReadFrom(byte[] buffer, int offset) + { + Signature = EndianUtilities.BytesToString(buffer, offset + 0x00, 8); + Checksum = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x08); + Version = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x0C); + Timestamp = DateTime.FromFileTimeUtc(EndianUtilities.ToInt64BigEndian(buffer, offset + 0x10)); + Unknown2 = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x18); + Unknown3 = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x20); + Unknown4 = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x28); + DiskId = EndianUtilities.BytesToString(buffer, offset + 0x30, 0x40).Trim('\0'); + HostId = EndianUtilities.BytesToString(buffer, offset + 0x70, 0x40).Trim('\0'); + DiskGroupId = EndianUtilities.BytesToString(buffer, offset + 0xB0, 0x40).Trim('\0'); + DiskGroupName = EndianUtilities.BytesToString(buffer, offset + 0xF0, 31).Trim('\0'); + Unknown5 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x10F); + DataStartLba = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x11B); + DataSizeLba = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x123); + ConfigurationStartLba = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x12B); + ConfigurationSizeLba = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x133); + TocSizeLba = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x13B); + NextTocLba = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x143); + + // These two may be reversed + NumberOfConfigs = EndianUtilities.ToInt32BigEndian(buffer, offset + 0x14B); + NumberOfLogs = EndianUtilities.ToInt32BigEndian(buffer, offset + 0x14F); + + ConfigSizeLba = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x153); + LogSizeLba = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x15B); + } + + ////} + //// throw new NotImplementedException(); + //// // Add all byte values for 512 bytes + //// // Zero checksum bytes (0x08, 4) + ////{ + + ////private static int CalcChecksum() + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/RecordType.cs b/DiscUtils/Core/LogicalDiskManager/RecordType.cs new file mode 100644 index 0000000..b8d66cf --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/RecordType.cs @@ -0,0 +1,34 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.LogicalDiskManager +{ + internal enum RecordType : byte + { + None = 0, + Volume = 1, + Component = 2, + Extent = 3, + Disk = 4, + DiskGroup = 5 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/TocBlock.cs b/DiscUtils/Core/LogicalDiskManager/TocBlock.cs new file mode 100644 index 0000000..669b078 --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/TocBlock.cs @@ -0,0 +1,72 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.LogicalDiskManager +{ + internal class TocBlock + { + public uint Checksum; // 00 00 08 B6 + public long Item1Size; // Unit? + public long Item1Start; // Sector Offset from ConfigurationStart + public string Item1Str; // 'config', length 10 + public long Item2Size; // Unit? + public long Item2Start; // Sector Offset from ConfigurationStart + public string Item2Str; // 'log', length 10 + public long SequenceNumber; // 00 .. 01 + public string Signature; // TOCBLOCK + public long Unknown1; // 0 + public long Unknown2; // 00 + public uint Unknown3; // 00 06 00 01 (may be two values?) + public uint Unknown4; // 00 00 00 00 + public uint Unknown5; // 00 06 00 01 (may be two values?) + public uint Unknown6; // 00 00 00 00 + + public void ReadFrom(byte[] buffer, int offset) + { + Signature = EndianUtilities.BytesToString(buffer, offset + 0x00, 8); + Checksum = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x08); + SequenceNumber = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x0C); + Unknown1 = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x14); + Unknown2 = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x1C); + Item1Str = EndianUtilities.BytesToString(buffer, offset + 0x24, 10).Trim('\0'); + Item1Start = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x2E); + Item1Size = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x36); + Unknown3 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x3E); + Unknown4 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x42); + Item2Str = EndianUtilities.BytesToString(buffer, offset + 0x46, 10).Trim('\0'); + Item2Start = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x50); + Item2Size = EndianUtilities.ToInt64BigEndian(buffer, offset + 0x58); + Unknown5 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x60); + Unknown6 = EndianUtilities.ToUInt32BigEndian(buffer, offset + 0x64); + } + + ////} + //// throw new NotImplementedException(); + //// // Add all byte values for ?? bytes + //// // Zero checksum bytes (0x08, 4) + ////{ + + ////private static int CalcChecksum() + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/VolumeRecord.cs b/DiscUtils/Core/LogicalDiskManager/VolumeRecord.cs new file mode 100644 index 0000000..1838522 --- /dev/null +++ b/DiscUtils/Core/LogicalDiskManager/VolumeRecord.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.LogicalDiskManager +{ + internal sealed class VolumeRecord : DatabaseRecord + { + public string ActiveString; + public byte BiosType; + public ulong ComponentCount; + public ulong DupCount; // ??Seen once after adding 'foreign disk', from broken mirror (identical links(P/V/C)) + public string GenString; + public string MountHint; + public string NumberString; // 8000000000000000 sometimes... + public uint PartitionComponentLink; + public long Size; + public ulong Unknown1; // Zero + public uint Unknown2; // Zero + public ulong UnknownA; // Zero + public ulong UnknownB; // 00 .. 03 + public uint UnknownC; // 00 00 00 11 + public uint UnknownD; // Zero + public Guid VolumeGuid; + + protected override void DoReadFrom(byte[] buffer, int offset) + { + base.DoReadFrom(buffer, offset); + + int pos = offset + 0x18; + + Id = ReadVarULong(buffer, ref pos); + Name = ReadVarString(buffer, ref pos); + GenString = ReadVarString(buffer, ref pos); + NumberString = ReadVarString(buffer, ref pos); + ActiveString = ReadString(buffer, 6, ref pos); + UnknownA = ReadVarULong(buffer, ref pos); + UnknownB = ReadULong(buffer, ref pos); + DupCount = ReadVarULong(buffer, ref pos); + UnknownC = ReadUInt(buffer, ref pos); + ComponentCount = ReadVarULong(buffer, ref pos); + UnknownD = ReadUInt(buffer, ref pos); + PartitionComponentLink = ReadUInt(buffer, ref pos); + Unknown1 = ReadULong(buffer, ref pos); + Size = ReadVarLong(buffer, ref pos); + Unknown2 = ReadUInt(buffer, ref pos); + BiosType = ReadByte(buffer, ref pos); + VolumeGuid = EndianUtilities.ToGuidBigEndian(buffer, pos); + pos += 16; + + if ((Flags & 0x0200) != 0) + { + MountHint = ReadVarString(buffer, ref pos); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalVolumeInfo.cs b/DiscUtils/Core/LogicalVolumeInfo.cs new file mode 100644 index 0000000..697a716 --- /dev/null +++ b/DiscUtils/Core/LogicalVolumeInfo.cs @@ -0,0 +1,120 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Information about a logical disk volume, which may be backed by one or more physical volumes. + /// + public sealed class LogicalVolumeInfo : VolumeInfo + { + private Guid _guid; + private readonly SparseStreamOpenDelegate _opener; + private readonly PhysicalVolumeInfo _physicalVol; + + internal LogicalVolumeInfo(Guid guid, PhysicalVolumeInfo physicalVolume, SparseStreamOpenDelegate opener, + long length, byte biosType, LogicalVolumeStatus status) + { + _guid = guid; + _physicalVol = physicalVolume; + _opener = opener; + Length = length; + BiosType = biosType; + Status = status; + } + + /// + /// Gets the disk geometry of the underlying storage medium (as used in BIOS calls), may be null. + /// + public override Geometry BiosGeometry + { + get { return _physicalVol == null ? Geometry.Null : _physicalVol.BiosGeometry; } + } + + /// + /// Gets the one-byte BIOS type for this volume, which indicates the content. + /// + public override byte BiosType { get; } + + /// + /// The stable identity for this logical volume. + /// + /// The stability of the identity depends the disk structure. + /// In some cases the identity may include a simple index, when no other information + /// is available. Best practice is to add disks to the Volume Manager in a stable + /// order, if the stability of this identity is paramount. + public override string Identity + { + get + { + if (_guid != Guid.Empty) + { + return "VLG" + _guid.ToString("B"); + } + return "VLP:" + _physicalVol.Identity; + } + } + + /// + /// Gets the length of the volume (in bytes). + /// + public override long Length { get; } + + /// + /// Gets the disk geometry of the underlying storage medium, if any (may be Geometry.Null). + /// + public override Geometry PhysicalGeometry + { + get { return _physicalVol == null ? Geometry.Null : _physicalVol.PhysicalGeometry; } + } + + /// + /// Gets the offset of this volume in the underlying storage medium, if any (may be Zero). + /// + public override long PhysicalStartSector + { + get { return _physicalVol == null ? 0 : _physicalVol.PhysicalStartSector; } + } + + /// + /// Gets the status of the logical volume, indicating volume health. + /// + public LogicalVolumeStatus Status { get; } + + /// + /// Gets the underlying physical volume info + /// + public PhysicalVolumeInfo PhysicalVolume { get { return _physicalVol; } } + + /// + /// Opens a stream with access to the content of the logical volume. + /// + /// The volume's content as a stream. + public override SparseStream Open() + { + return _opener(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalVolumeStatus.cs b/DiscUtils/Core/LogicalVolumeStatus.cs new file mode 100644 index 0000000..368c759 --- /dev/null +++ b/DiscUtils/Core/LogicalVolumeStatus.cs @@ -0,0 +1,23 @@ +namespace DiscUtils +{ + /// + /// Enumeration of the health status of a logical volume. + /// + public enum LogicalVolumeStatus + { + /// + /// The volume is healthy and fully functional. + /// + Healthy = 0, + + /// + /// The volume is completely accessible, but at degraded redundancy. + /// + FailedRedundancy = 1, + + /// + /// The volume is wholey, or partly, inaccessible. + /// + Failed = 2 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/NativeFileSystem.cs b/DiscUtils/Core/NativeFileSystem.cs new file mode 100644 index 0000000..4c9f9c9 --- /dev/null +++ b/DiscUtils/Core/NativeFileSystem.cs @@ -0,0 +1,804 @@ +// +// DiscUtils Copyright (c) 2008-2011, Kenneth Bell +// +// Original NativeFileSystem contributed by bsobel: +// http://discutils.codeplex.com/workitem/5190 +// +// 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 DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Provides an implementation for OS-mounted file systems. + /// + public class NativeFileSystem : DiscFileSystem + { + private readonly bool _readOnly; + + /// + /// Initializes a new instance of the NativeFileSystem class. + /// + /// The 'root' directory of the new instance. + /// Only permit 'read' activities. + public NativeFileSystem(string basePath, bool readOnly) + { + BasePath = basePath; + if (!BasePath.EndsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + BasePath += @"\"; + } + + _readOnly = readOnly; + } + + /// + /// Gets the base path used to create the file system. + /// + public string BasePath { get; } + + /// + /// Indicates whether the file system is read-only or read-write. + /// + /// true if the file system is read-write. + public override bool CanWrite + { + get { return !_readOnly; } + } + + /// + /// Provides a friendly description of the file system type. + /// + public override string FriendlyName + { + get { return "Native"; } + } + + /// + /// Gets a value indicating whether the file system is thread-safe. + /// + /// The Native File System is thread safe. + public override bool IsThreadSafe + { + get { return true; } + } + + /// + /// Gets the root directory of the file system. + /// + public override DiscDirectoryInfo Root + { + get { return new DiscDirectoryInfo(this, string.Empty); } + } + + /// + /// Gets the volume label. + /// + public override string VolumeLabel + { + get { return string.Empty; } + } + + /// + /// Copies an existing file to a new file. + /// + /// The source file. + /// The destination file. + public override void CopyFile(string sourceFile, string destinationFile) + { + CopyFile(sourceFile, destinationFile, true); + } + + /// + /// Copies an existing file to a new file, allowing overwriting of an existing file. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (sourceFile.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + sourceFile = sourceFile.Substring(1); + } + + if (destinationFile.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + destinationFile = destinationFile.Substring(1); + } + + File.Copy(Path.Combine(BasePath, sourceFile), Path.Combine(BasePath, destinationFile), true); + } + + /// + /// Creates a directory. + /// + /// The path of the new directory. + public override void CreateDirectory(string path) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + Directory.CreateDirectory(Path.Combine(BasePath, path)); + } + + /// + /// Deletes a directory. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + Directory.Delete(Path.Combine(BasePath, path)); + } + + /// + /// Deletes a directory, optionally with all descendants. + /// + /// The path of the directory to delete. + /// Determines if the all descendants should be deleted. + public override void DeleteDirectory(string path, bool recursive) + { + if (recursive) + { + foreach (string dir in GetDirectories(path)) + { + DeleteDirectory(dir, true); + } + + foreach (string file in GetFiles(path)) + { + DeleteFile(file); + } + } + + DeleteDirectory(path); + } + + /// + /// Deletes a file. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + File.Delete(Path.Combine(BasePath, path)); + } + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + public override bool DirectoryExists(string path) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + return Directory.Exists(Path.Combine(BasePath, path)); + } + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + public override bool FileExists(string path) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + return File.Exists(Path.Combine(BasePath, path)); + } + + /// + /// Indicates if a file or directory exists. + /// + /// The path to test. + /// true if the file or directory exists. + public override bool Exists(string path) + { + return FileExists(path) || DirectoryExists(path); + } + + /// + /// Gets the names of subdirectories in a specified directory. + /// + /// The path to search. + /// Array of directories. + public override string[] GetDirectories(string path) + { + return GetDirectories(path, "*.*", SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern) + { + return GetDirectories(path, searchPattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern, SearchOption searchOption) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + try + { + return CleanItems(Directory.GetDirectories(Path.Combine(BasePath, path), searchPattern, searchOption)); + } + catch (IOException) + { + return new string[0]; + } + catch (UnauthorizedAccessException) + { + return new string[0]; + } + } + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// Array of files. + public override string[] GetFiles(string path) + { + return GetFiles(path, "*.*", SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// The search string to match against. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern) + { + return GetFiles(path, searchPattern, SearchOption.TopDirectoryOnly); + } + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern, SearchOption searchOption) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + try + { + return CleanItems(Directory.GetFiles(Path.Combine(BasePath, path), searchPattern, searchOption)); + } + catch (IOException) + { + return new string[0]; + } + catch (UnauthorizedAccessException) + { + return new string[0]; + } + } + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path) + { + return GetFileSystemEntries(path, "*.*"); + } + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path, string searchPattern) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + try + { + return CleanItems(Directory.GetFileSystemEntries(Path.Combine(BasePath, path), searchPattern)); + } + catch (IOException) + { + return new string[0]; + } + catch (UnauthorizedAccessException) + { + return new string[0]; + } + } + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (sourceDirectoryName.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + sourceDirectoryName = sourceDirectoryName.Substring(1); + } + + if (destinationDirectoryName.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + destinationDirectoryName = destinationDirectoryName.Substring(1); + } + + Directory.Move(Path.Combine(BasePath, sourceDirectoryName), + Path.Combine(BasePath, destinationDirectoryName)); + } + + /// + /// Moves a file. + /// + /// The file to move. + /// The target file name. + public override void MoveFile(string sourceName, string destinationName) + { + MoveFile(sourceName, destinationName, false); + } + + /// + /// Moves a file, allowing an existing file to be overwritten. + /// + /// The file to move. + /// The target file name. + /// Whether to permit a destination file to be overwritten. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (destinationName.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + destinationName = destinationName.Substring(1); + } + + if (FileExists(Path.Combine(BasePath, destinationName))) + { + if (overwrite) + { + DeleteFile(Path.Combine(BasePath, destinationName)); + } + else + { + throw new IOException("File already exists"); + } + } + + if (sourceName.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + sourceName = sourceName.Substring(1); + } + + File.Move(Path.Combine(BasePath, sourceName), Path.Combine(BasePath, destinationName)); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode) + { + return OpenFile(path, mode, FileAccess.ReadWrite); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode, FileAccess access) + { + if (_readOnly && access != FileAccess.Read) + { + throw new UnauthorizedAccessException(); + } + + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + FileShare fileShare = FileShare.None; + if (access == FileAccess.Read) + { + fileShare = FileShare.Read; + } + + var locator = new LocalFileLocator(BasePath); + return SparseStream.FromStream(locator.Open(path, mode, access, fileShare), + Ownership.Dispose); + } + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + public override FileAttributes GetAttributes(string path) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + return File.GetAttributes(Path.Combine(BasePath, path)); + } + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + File.SetAttributes(Path.Combine(BasePath, path), newValue); + } + + /// + /// Gets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTime(string path) + { + return GetCreationTimeUtc(path).ToLocalTime(); + } + + /// + /// Sets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTime(string path, DateTime newTime) + { + SetCreationTimeUtc(path, newTime.ToUniversalTime()); + } + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTimeUtc(string path) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + return File.GetCreationTimeUtc(Path.Combine(BasePath, path)); + } + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + File.SetCreationTimeUtc(Path.Combine(BasePath, path), newTime); + } + + /// + /// Gets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public override DateTime GetLastAccessTime(string path) + { + return GetLastAccessTimeUtc(path).ToLocalTime(); + } + + /// + /// Sets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTime(string path, DateTime newTime) + { + SetLastAccessTimeUtc(path, newTime.ToUniversalTime()); + } + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public override DateTime GetLastAccessTimeUtc(string path) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + return File.GetLastAccessTimeUtc(Path.Combine(BasePath, path)); + } + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + File.SetLastAccessTimeUtc(Path.Combine(BasePath, path), newTime); + } + + /// + /// Gets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public override DateTime GetLastWriteTime(string path) + { + return GetLastWriteTimeUtc(path).ToLocalTime(); + } + + /// + /// Sets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTime(string path, DateTime newTime) + { + SetLastWriteTimeUtc(path, newTime.ToUniversalTime()); + } + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public override DateTime GetLastWriteTimeUtc(string path) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + return File.GetLastWriteTimeUtc(Path.Combine(BasePath, path)); + } + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + if (_readOnly) + { + throw new UnauthorizedAccessException(); + } + + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + File.SetLastWriteTimeUtc(Path.Combine(BasePath, path), newTime); + } + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + public override long GetFileLength(string path) + { + if (path.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(1); + } + + return new FileInfo(Path.Combine(BasePath, path)).Length; + } + + /// + /// Gets an object representing a possible file. + /// + /// The file path. + /// The representing object. + /// The file does not need to exist. + public override DiscFileInfo GetFileInfo(string path) + { + return new DiscFileInfo(this, path); + } + + /// + /// Gets an object representing a possible directory. + /// + /// The directory path. + /// The representing object. + /// The directory does not need to exist. + public override DiscDirectoryInfo GetDirectoryInfo(string path) + { + return new DiscDirectoryInfo(this, path); + } + + /// + /// Gets an object representing a possible file system object (file or directory). + /// + /// The file system path. + /// The representing object. + /// The file system object does not need to exist. + public override DiscFileSystemInfo GetFileSystemInfo(string path) + { + return new DiscFileSystemInfo(this, path); + } + + /// + /// Size of the Filesystem in bytes + /// + public override long Size + { + get + { + DriveInfo info = new DriveInfo(BasePath); + return info.TotalSize; + } + } + + /// + /// Used space of the Filesystem in bytes + /// + public override long UsedSpace + { + get { return Size - AvailableSpace; } + } + + /// + /// Available space of the Filesystem in bytes + /// + public override long AvailableSpace + { + get + { + DriveInfo info = new DriveInfo(BasePath); + return info.AvailableFreeSpace; + } + } + + private string[] CleanItems(string[] dirtyItems) + { + string[] cleanList = new string[dirtyItems.Length]; + for (int x = 0; x < dirtyItems.Length; x++) + { + cleanList[x] = dirtyItems[x].Substring(BasePath.Length - 1); + } + + return cleanList; + } + } +} diff --git a/DiscUtils/Core/Partitions/BiosExtendedPartitionTable.cs b/DiscUtils/Core/Partitions/BiosExtendedPartitionTable.cs new file mode 100644 index 0000000..d08ca87 --- /dev/null +++ b/DiscUtils/Core/Partitions/BiosExtendedPartitionTable.cs @@ -0,0 +1,119 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; +using DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + internal class BiosExtendedPartitionTable + { + private readonly Stream _disk; + private readonly uint _firstSector; + + public BiosExtendedPartitionTable(Stream disk, uint firstSector) + { + _disk = disk; + _firstSector = firstSector; + } + + public BiosPartitionRecord[] GetPartitions() + { + List result = new List(); + + uint partPos = _firstSector; + while (partPos != 0) + { + _disk.Position = (long)partPos * Sizes.Sector; + byte[] sector = StreamUtilities.ReadExact(_disk, Sizes.Sector); + if (sector[510] != 0x55 || sector[511] != 0xAA) + { + throw new IOException("Invalid extended partition sector"); + } + + uint nextPartPos = 0; + for (int offset = 0x1BE; offset <= 0x1EE; offset += 0x10) + { + BiosPartitionRecord thisPart = new BiosPartitionRecord(sector, offset, partPos, -1); + + if (thisPart.StartCylinder != 0 || thisPart.StartHead != 0 || thisPart.StartSector != 0 || + (thisPart.LBAStart != 0 && thisPart.LBALength != 0)) + { + if (thisPart.PartitionType != 0x05 && thisPart.PartitionType != 0x0F) + { + result.Add(thisPart); + } + else + { + nextPartPos = _firstSector + thisPart.LBAStart; + } + } + } + + partPos = nextPartPos; + } + + return result.ToArray(); + } + + /// + /// Gets all of the disk ranges containing partition table data. + /// + /// Set of stream extents, indicated as byte offset from the start of the disk. + public IEnumerable GetMetadataDiskExtents() + { + List extents = new List(); + + uint partPos = _firstSector; + while (partPos != 0) + { + extents.Add(new StreamExtent((long)partPos * Sizes.Sector, Sizes.Sector)); + + _disk.Position = (long)partPos * Sizes.Sector; + byte[] sector = StreamUtilities.ReadExact(_disk, Sizes.Sector); + if (sector[510] != 0x55 || sector[511] != 0xAA) + { + throw new IOException("Invalid extended partition sector"); + } + + uint nextPartPos = 0; + for (int offset = 0x1BE; offset <= 0x1EE; offset += 0x10) + { + BiosPartitionRecord thisPart = new BiosPartitionRecord(sector, offset, partPos, -1); + + if (thisPart.StartCylinder != 0 || thisPart.StartHead != 0 || thisPart.StartSector != 0) + { + if (thisPart.PartitionType == 0x05 || thisPart.PartitionType == 0x0F) + { + nextPartPos = _firstSector + thisPart.LBAStart; + } + } + } + + partPos = nextPartPos; + } + + return extents; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/BiosPartitionInfo.cs b/DiscUtils/Core/Partitions/BiosPartitionInfo.cs new file mode 100644 index 0000000..ea7a4ef --- /dev/null +++ b/DiscUtils/Core/Partitions/BiosPartitionInfo.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + /// + /// Provides access to partition records in a BIOS (MBR) partition table. + /// + public sealed class BiosPartitionInfo : PartitionInfo + { + private readonly BiosPartitionRecord _record; + private readonly BiosPartitionTable _table; + + internal BiosPartitionInfo(BiosPartitionTable table, BiosPartitionRecord record) + { + _table = table; + _record = record; + } + + /// + /// Gets the type of the partition. + /// + public override byte BiosType + { + get { return _record.PartitionType; } + } + + /// + /// Gets the end (inclusive) of the partition as a CHS address. + /// + public ChsAddress End + { + get { return new ChsAddress(_record.EndCylinder, _record.EndHead, _record.EndSector); } + } + + /// + /// Gets the first sector of the partion (relative to start of disk) as a Logical Block Address. + /// + public override long FirstSector + { + get { return _record.LBAStartAbsolute; } + } + + /// + /// Always returns .Empty. + /// + public override Guid GuidType + { + get { return Guid.Empty; } + } + + /// + /// Gets a value indicating whether this partition is active (bootable). + /// + public bool IsActive + { + get { return _record.Status != 0; } + } + + /// + /// Gets a value indicating whether the partition is a primary (rather than extended) partition. + /// + public bool IsPrimary + { + get { return PrimaryIndex >= 0; } + } + + /// + /// Gets the last sector of the partion (relative to start of disk) as a Logical Block Address (inclusive). + /// + public override long LastSector + { + get { return _record.LBAStartAbsolute + _record.LBALength - 1; } + } + + /// + /// Gets the index of the partition in the primary partition table, or -1 if not a primary partition. + /// + public int PrimaryIndex + { + get { return _record.Index; } + } + + /// + /// Gets the start of the partition as a CHS address. + /// + public ChsAddress Start + { + get { return new ChsAddress(_record.StartCylinder, _record.StartHead, _record.StartSector); } + } + + /// + /// Gets the type of the partition as a string. + /// + public override string TypeAsString + { + get { return _record.FriendlyPartitionType; } + } + + internal override PhysicalVolumeType VolumeType + { + get { return PhysicalVolumeType.BiosPartition; } + } + + /// + /// Opens a stream to access the content of the partition. + /// + /// The new stream. + public override SparseStream Open() + { + return _table.Open(_record); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/BiosPartitionRecord.cs b/DiscUtils/Core/Partitions/BiosPartitionRecord.cs new file mode 100644 index 0000000..a76fafb --- /dev/null +++ b/DiscUtils/Core/Partitions/BiosPartitionRecord.cs @@ -0,0 +1,107 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + internal class BiosPartitionRecord : IComparable + { + private readonly uint _lbaOffset; + + public BiosPartitionRecord() {} + + public BiosPartitionRecord(byte[] data, int offset, uint lbaOffset, int index) + { + _lbaOffset = lbaOffset; + + Status = data[offset]; + StartHead = data[offset + 1]; + StartSector = (byte)(data[offset + 2] & 0x3F); + StartCylinder = (ushort)(data[offset + 3] | ((data[offset + 2] & 0xC0) << 2)); + PartitionType = data[offset + 4]; + EndHead = data[offset + 5]; + EndSector = (byte)(data[offset + 6] & 0x3F); + EndCylinder = (ushort)(data[offset + 7] | ((data[offset + 6] & 0xC0) << 2)); + LBAStart = EndianUtilities.ToUInt32LittleEndian(data, offset + 8); + LBALength = EndianUtilities.ToUInt32LittleEndian(data, offset + 12); + Index = index; + } + + public ushort EndCylinder { get; set; } + + public byte EndHead { get; set; } + + public byte EndSector { get; set; } + + public string FriendlyPartitionType + { + get { return BiosPartitionTypes.ToString(PartitionType); } + } + + public int Index { get; } + + public bool IsValid + { + get { return EndHead != 0 || EndSector != 0 || EndCylinder != 0 || LBALength != 0; } + } + + public uint LBALength { get; set; } + + public uint LBAStart { get; set; } + + public uint LBAStartAbsolute + { + get { return LBAStart + _lbaOffset; } + } + + public byte PartitionType { get; set; } + + public ushort StartCylinder { get; set; } + + public byte StartHead { get; set; } + + public byte StartSector { get; set; } + + public byte Status { get; set; } + + public int CompareTo(BiosPartitionRecord other) + { + return LBAStartAbsolute.CompareTo(other.LBAStartAbsolute); + } + + internal void WriteTo(byte[] buffer, int offset) + { + buffer[offset] = Status; + buffer[offset + 1] = StartHead; + buffer[offset + 2] = (byte)((StartSector & 0x3F) | ((StartCylinder >> 2) & 0xC0)); + buffer[offset + 3] = (byte)StartCylinder; + buffer[offset + 4] = PartitionType; + buffer[offset + 5] = EndHead; + buffer[offset + 6] = (byte)((EndSector & 0x3F) | ((EndCylinder >> 2) & 0xC0)); + buffer[offset + 7] = (byte)EndCylinder; + EndianUtilities.WriteBytesLittleEndian(LBAStart, buffer, offset + 8); + EndianUtilities.WriteBytesLittleEndian(LBALength, buffer, offset + 12); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/BiosPartitionTable.cs b/DiscUtils/Core/Partitions/BiosPartitionTable.cs new file mode 100644 index 0000000..48682d5 --- /dev/null +++ b/DiscUtils/Core/Partitions/BiosPartitionTable.cs @@ -0,0 +1,734 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + /// + /// Represents a BIOS (MBR) Partition Table. + /// + public sealed class BiosPartitionTable : PartitionTable + { + private Stream _diskData; + private Geometry _diskGeometry; + + /// + /// Initializes a new instance of the BiosPartitionTable class. + /// + /// The disk containing the partition table. + public BiosPartitionTable(VirtualDisk disk) + { + Init(disk.Content, disk.BiosGeometry); + } + + /// + /// Initializes a new instance of the BiosPartitionTable class. + /// + /// The stream containing the disk data. + /// The geometry of the disk. + public BiosPartitionTable(Stream disk, Geometry diskGeometry) + { + Init(disk, diskGeometry); + } + + /// + /// Gets a collection of the partitions for storing Operating System file-systems. + /// + public ReadOnlyCollection BiosUserPartitions + { + get + { + List result = new List(); + foreach (BiosPartitionRecord r in GetAllRecords()) + { + if (r.IsValid) + { + result.Add(new BiosPartitionInfo(this, r)); + } + } + + return new ReadOnlyCollection(result); + } + } + + /// + /// Gets the GUID that uniquely identifies this disk, if supported (else returns null). + /// + public override Guid DiskGuid + { + get { return Guid.Empty; } + } + + /// + /// Gets a collection of the partitions for storing Operating System file-systems. + /// + public override ReadOnlyCollection Partitions + { + get + { + List result = new List(); + foreach (BiosPartitionRecord r in GetAllRecords()) + { + if (r.IsValid) + { + result.Add(new BiosPartitionInfo(this, r)); + } + } + + return new ReadOnlyCollection(result); + } + } + + /// + /// Makes a best guess at the geometry of a disk. + /// + /// String containing the disk image to detect the geometry from. + /// The detected geometry. + public static Geometry DetectGeometry(Stream disk) + { + if (disk.Length >= Sizes.Sector) + { + disk.Position = 0; + byte[] bootSector = StreamUtilities.ReadExact(disk, Sizes.Sector); + if (bootSector[510] == 0x55 && bootSector[511] == 0xAA) + { + byte maxHead = 0; + byte maxSector = 0; + foreach (BiosPartitionRecord record in ReadPrimaryRecords(bootSector)) + { + maxHead = Math.Max(maxHead, record.EndHead); + maxSector = Math.Max(maxSector, record.EndSector); + } + + if (maxHead > 0 && maxSector > 0) + { + int cylSize = (maxHead + 1) * maxSector * 512; + return new Geometry((int)MathUtilities.Ceil(disk.Length, cylSize), maxHead + 1, maxSector); + } + } + } + + return Geometry.FromCapacity(disk.Length); + } + + /// + /// Indicates if a stream contains a valid partition table. + /// + /// The stream to inspect. + /// true if the partition table is valid, else false. + public static bool IsValid(Stream disk) + { + if (disk.Length < Sizes.Sector) + { + return false; + } + + disk.Position = 0; + byte[] bootSector = StreamUtilities.ReadExact(disk, Sizes.Sector); + + // Check for the 'bootable sector' marker + if (bootSector[510] != 0x55 || bootSector[511] != 0xAA) + { + return false; + } + + List knownPartitions = new List(); + foreach (BiosPartitionRecord record in ReadPrimaryRecords(bootSector)) + { + // If the partition extends beyond the end of the disk, this is probably an invalid partition table + if (record.LBALength != 0xFFFFFFFF && + (record.LBAStart + (long)record.LBALength) * Sizes.Sector > disk.Length) + { + return false; + } + + if (record.LBALength > 0) + { + StreamExtent[] thisPartitionExtents = { new StreamExtent(record.LBAStart, record.LBALength) }; + + // If the partition intersects another partition, this is probably an invalid partition table + foreach (StreamExtent overlap in StreamExtent.Intersect(knownPartitions, thisPartitionExtents)) + { + return false; + } + + knownPartitions = new List(StreamExtent.Union(knownPartitions, thisPartitionExtents)); + } + } + + return true; + } + + /// + /// Creates a new partition table on a disk. + /// + /// The disk to initialize. + /// An object to access the newly created partition table. + public static BiosPartitionTable Initialize(VirtualDisk disk) + { + return Initialize(disk.Content, disk.BiosGeometry); + } + + /// + /// Creates a new partition table on a disk containing a single partition. + /// + /// The disk to initialize. + /// The partition type for the single partition. + /// An object to access the newly created partition table. + public static BiosPartitionTable Initialize(VirtualDisk disk, WellKnownPartitionType type) + { + BiosPartitionTable table = Initialize(disk.Content, disk.BiosGeometry); + table.Create(type, true); + return table; + } + + /// + /// Creates a new partition table on a disk. + /// + /// The stream containing the disk data. + /// The geometry of the disk. + /// An object to access the newly created partition table. + public static BiosPartitionTable Initialize(Stream disk, Geometry diskGeometry) + { + Stream data = disk; + + byte[] bootSector; + if (data.Length >= Sizes.Sector) + { + data.Position = 0; + bootSector = StreamUtilities.ReadExact(data, Sizes.Sector); + } + else + { + bootSector = new byte[Sizes.Sector]; + } + + // Wipe all four 16-byte partition table entries + Array.Clear(bootSector, 0x01BE, 16 * 4); + + // Marker bytes + bootSector[510] = 0x55; + bootSector[511] = 0xAA; + + data.Position = 0; + data.Write(bootSector, 0, bootSector.Length); + + return new BiosPartitionTable(disk, diskGeometry); + } + + /// + /// Creates a new partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + public override int Create(WellKnownPartitionType type, bool active) + { + Geometry allocationGeometry = new Geometry(_diskData.Length, _diskGeometry.HeadsPerCylinder, + _diskGeometry.SectorsPerTrack, _diskGeometry.BytesPerSector); + + ChsAddress start = new ChsAddress(0, 1, 1); + ChsAddress last = allocationGeometry.LastSector; + + long startLba = allocationGeometry.ToLogicalBlockAddress(start); + long lastLba = allocationGeometry.ToLogicalBlockAddress(last); + + return CreatePrimaryByCylinder(0, allocationGeometry.Cylinders - 1, + ConvertType(type, (lastLba - startLba) * Sizes.Sector), active); + } + + /// + /// Creates a new primary partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the new partition. + public override int Create(long size, WellKnownPartitionType type, bool active) + { + int cylinderCapacity = _diskGeometry.SectorsPerTrack * _diskGeometry.HeadsPerCylinder * + _diskGeometry.BytesPerSector; + int numCylinders = (int)(size / cylinderCapacity); + + int startCylinder = FindCylinderGap(numCylinders); + + return CreatePrimaryByCylinder(startCylinder, startCylinder + numCylinders - 1, ConvertType(type, size), + active); + } + + /// + /// Creates a new aligned partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in bytes). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is acheived by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(WellKnownPartitionType type, bool active, int alignment) + { + Geometry allocationGeometry = new Geometry(_diskData.Length, _diskGeometry.HeadsPerCylinder, + _diskGeometry.SectorsPerTrack, _diskGeometry.BytesPerSector); + + ChsAddress start = new ChsAddress(0, 1, 1); + + long startLba = MathUtilities.RoundUp(allocationGeometry.ToLogicalBlockAddress(start), + alignment / _diskGeometry.BytesPerSector); + long lastLba = MathUtilities.RoundDown(_diskData.Length / _diskGeometry.BytesPerSector, + alignment / _diskGeometry.BytesPerSector); + + return CreatePrimaryBySector(startLba, lastLba - 1, + ConvertType(type, (lastLba - startLba) * _diskGeometry.BytesPerSector), active); + } + + /// + /// Creates a new aligned partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in bytes). + /// The index of the new partition. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is achieved by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(long size, WellKnownPartitionType type, bool active, int alignment) + { + if (size < _diskGeometry.BytesPerSector) + { + throw new ArgumentOutOfRangeException(nameof(size), size, "size must be at least one sector"); + } + + if (alignment % _diskGeometry.BytesPerSector != 0) + { + throw new ArgumentException("Alignment is not a multiple of the sector size"); + } + + if (size % alignment != 0) + { + throw new ArgumentException("Size is not a multiple of the alignment"); + } + + long sectorLength = size / _diskGeometry.BytesPerSector; + long start = FindGap(size / _diskGeometry.BytesPerSector, alignment / _diskGeometry.BytesPerSector); + + return CreatePrimaryBySector(start, start + sectorLength - 1, + ConvertType(type, sectorLength * Sizes.Sector), active); + } + + /// + /// Deletes a partition at a given index. + /// + /// The index of the partition. + public override void Delete(int index) + { + WriteRecord(index, new BiosPartitionRecord()); + } + + /// + /// Creates a new Primary Partition that occupies whole cylinders, for best compatibility. + /// + /// The first cylinder to include in the partition (inclusive). + /// The last cylinder to include in the partition (inclusive). + /// The BIOS (MBR) type of the new partition. + /// Whether to mark the partition active (bootable). + /// The index of the new partition. + /// If the cylinder 0 is given, the first track will not be used, to reserve space + /// for the meta-data at the start of the disk. + public int CreatePrimaryByCylinder(int first, int last, byte type, bool markActive) + { + if (first < 0) + { + throw new ArgumentOutOfRangeException(nameof(first), first, "First cylinder must be Zero or greater"); + } + + if (last <= first) + { + throw new ArgumentException("Last cylinder must be greater than first"); + } + + long lbaStart = first == 0 + ? _diskGeometry.ToLogicalBlockAddress(0, 1, 1) + : _diskGeometry.ToLogicalBlockAddress(first, 0, 1); + long lbaLast = _diskGeometry.ToLogicalBlockAddress(last, _diskGeometry.HeadsPerCylinder - 1, + _diskGeometry.SectorsPerTrack); + + return CreatePrimaryBySector(lbaStart, lbaLast, type, markActive); + } + + /// + /// Creates a new Primary Partition, specified by Logical Block Addresses. + /// + /// The LBA address of the first sector (inclusive). + /// The LBA address of the last sector (inclusive). + /// The BIOS (MBR) type of the new partition. + /// Whether to mark the partition active (bootable). + /// The index of the new partition. + public int CreatePrimaryBySector(long first, long last, byte type, bool markActive) + { + if (first >= last) + { + throw new ArgumentException("The first sector in a partition must be before the last"); + } + + if ((last + 1) * _diskGeometry.BytesPerSector > _diskData.Length) + { + throw new ArgumentOutOfRangeException(nameof(last), last, + "The last sector extends beyond the end of the disk"); + } + + BiosPartitionRecord[] existing = GetPrimaryRecords(); + + BiosPartitionRecord newRecord = new BiosPartitionRecord(); + ChsAddress startAddr = _diskGeometry.ToChsAddress(first); + ChsAddress endAddr = _diskGeometry.ToChsAddress(last); + + // Because C/H/S addresses can max out at lower values than the LBA values, + // the special tuple (1023, 254, 63) is used. + if (startAddr.Cylinder > 1023) + { + startAddr = new ChsAddress(1023, 254, 63); + } + + if (endAddr.Cylinder > 1023) + { + endAddr = new ChsAddress(1023, 254, 63); + } + + newRecord.StartCylinder = (ushort)startAddr.Cylinder; + newRecord.StartHead = (byte)startAddr.Head; + newRecord.StartSector = (byte)startAddr.Sector; + newRecord.EndCylinder = (ushort)endAddr.Cylinder; + newRecord.EndHead = (byte)endAddr.Head; + newRecord.EndSector = (byte)endAddr.Sector; + newRecord.LBAStart = (uint)first; + newRecord.LBALength = (uint)(last - first + 1); + newRecord.PartitionType = type; + newRecord.Status = (byte)(markActive ? 0x80 : 0x00); + + // First check for overlap with existing partition... + foreach (BiosPartitionRecord r in existing) + { + if (Utilities.RangesOverlap((uint)first, (uint)last + 1, r.LBAStartAbsolute, + r.LBAStartAbsolute + r.LBALength)) + { + throw new IOException("New partition overlaps with existing partition"); + } + } + + // Now look for empty partition + for (int i = 0; i < 4; ++i) + { + if (!existing[i].IsValid) + { + WriteRecord(i, newRecord); + return i; + } + } + + throw new IOException("No primary partition slots available"); + } + + /// + /// Sets the active partition. + /// + /// The index of the primary partition to mark bootable, or -1 for none. + /// The supplied index is the index within the primary partition, see PrimaryIndex on BiosPartitionInfo. + public void SetActivePartition(int index) + { + List records = new List(GetPrimaryRecords()); + + for (int i = 0; i < records.Count; ++i) + { + records[i].Status = i == index ? (byte)0x80 : (byte)0x00; + WriteRecord(i, records[i]); + } + } + + /// + /// Gets all of the disk ranges containing partition table metadata. + /// + /// Set of stream extents, indicated as byte offset from the start of the disk. + public IEnumerable GetMetadataDiskExtents() + { + List extents = new List(); + + extents.Add(new StreamExtent(0, Sizes.Sector)); + + foreach (BiosPartitionRecord primaryRecord in GetPrimaryRecords()) + { + if (primaryRecord.IsValid) + { + if (IsExtendedPartition(primaryRecord)) + { + extents.AddRange( + new BiosExtendedPartitionTable(_diskData, primaryRecord.LBAStart).GetMetadataDiskExtents()); + } + } + } + + return extents; + } + + /// + /// Updates the CHS fields in partition records to reflect a new BIOS geometry. + /// + /// The disk's new BIOS geometry. + /// The partitions are not relocated to a cylinder boundary, just the CHS fields are updated on the + /// assumption the LBA fields are definitive. + public void UpdateBiosGeometry(Geometry geometry) + { + _diskData.Position = 0; + byte[] bootSector = StreamUtilities.ReadExact(_diskData, Sizes.Sector); + + BiosPartitionRecord[] records = ReadPrimaryRecords(bootSector); + for (int i = 0; i < records.Length; ++i) + { + BiosPartitionRecord record = records[i]; + if (record.IsValid) + { + ChsAddress newStartAddress = geometry.ToChsAddress(record.LBAStartAbsolute); + if (newStartAddress.Cylinder > 1023) + { + newStartAddress = new ChsAddress(1023, geometry.HeadsPerCylinder - 1, geometry.SectorsPerTrack); + } + + ChsAddress newEndAddress = geometry.ToChsAddress(record.LBAStartAbsolute + record.LBALength - 1); + if (newEndAddress.Cylinder > 1023) + { + newEndAddress = new ChsAddress(1023, geometry.HeadsPerCylinder - 1, geometry.SectorsPerTrack); + } + + record.StartCylinder = (ushort)newStartAddress.Cylinder; + record.StartHead = (byte)newStartAddress.Head; + record.StartSector = (byte)newStartAddress.Sector; + record.EndCylinder = (ushort)newEndAddress.Cylinder; + record.EndHead = (byte)newEndAddress.Head; + record.EndSector = (byte)newEndAddress.Sector; + + WriteRecord(i, record); + } + } + + _diskGeometry = geometry; + } + + internal SparseStream Open(BiosPartitionRecord record) + { + return new SubStream(_diskData, Ownership.None, + record.LBAStartAbsolute * _diskGeometry.BytesPerSector, + record.LBALength * _diskGeometry.BytesPerSector); + } + + private static BiosPartitionRecord[] ReadPrimaryRecords(byte[] bootSector) + { + BiosPartitionRecord[] records = new BiosPartitionRecord[4]; + for (int i = 0; i < 4; ++i) + { + records[i] = new BiosPartitionRecord(bootSector, 0x01BE + i * 0x10, 0, i); + } + + return records; + } + + private static bool IsExtendedPartition(BiosPartitionRecord r) + { + return r.PartitionType == BiosPartitionTypes.Extended || r.PartitionType == BiosPartitionTypes.ExtendedLba; + } + + private static byte ConvertType(WellKnownPartitionType type, long size) + { + switch (type) + { + case WellKnownPartitionType.WindowsFat: + if (size < 512 * Sizes.OneMiB) + { + return BiosPartitionTypes.Fat16; + } + if (size < 1023 * (long)254 * 63 * 512) + { + // Max BIOS size + return BiosPartitionTypes.Fat32; + } + return BiosPartitionTypes.Fat32Lba; + + case WellKnownPartitionType.WindowsNtfs: + return BiosPartitionTypes.Ntfs; + case WellKnownPartitionType.Linux: + return BiosPartitionTypes.LinuxNative; + case WellKnownPartitionType.LinuxSwap: + return BiosPartitionTypes.LinuxSwap; + case WellKnownPartitionType.LinuxLvm: + return BiosPartitionTypes.LinuxLvm; + default: + throw new ArgumentException( + string.Format(CultureInfo.InvariantCulture, "Unrecognized partition type: '{0}'", type), + nameof(type)); + } + } + + private BiosPartitionRecord[] GetAllRecords() + { + List newList = new List(); + + foreach (BiosPartitionRecord primaryRecord in GetPrimaryRecords()) + { + if (primaryRecord.IsValid) + { + if (IsExtendedPartition(primaryRecord)) + { + newList.AddRange(GetExtendedRecords(primaryRecord)); + } + else + { + newList.Add(primaryRecord); + } + } + } + + return newList.ToArray(); + } + + private BiosPartitionRecord[] GetPrimaryRecords() + { + _diskData.Position = 0; + byte[] bootSector = StreamUtilities.ReadExact(_diskData, Sizes.Sector); + + return ReadPrimaryRecords(bootSector); + } + + private BiosPartitionRecord[] GetExtendedRecords(BiosPartitionRecord r) + { + return new BiosExtendedPartitionTable(_diskData, r.LBAStart).GetPartitions(); + } + + private void WriteRecord(int i, BiosPartitionRecord newRecord) + { + _diskData.Position = 0; + byte[] bootSector = StreamUtilities.ReadExact(_diskData, Sizes.Sector); + newRecord.WriteTo(bootSector, 0x01BE + i * 16); + _diskData.Position = 0; + _diskData.Write(bootSector, 0, bootSector.Length); + } + + private int FindCylinderGap(int numCylinders) + { + List list = Utilities.Filter, BiosPartitionRecord>(GetPrimaryRecords(), + r => r.IsValid); + list.Sort(); + + int startCylinder = 0; + foreach (BiosPartitionRecord r in list) + { + int existingStart = r.StartCylinder; + int existingEnd = r.EndCylinder; + + // LBA can represent bigger disk locations than CHS, so assume the LBA to be definitive in the case where it + // appears the CHS address has been truncated. + if (r.LBAStart > _diskGeometry.ToLogicalBlockAddress(r.StartCylinder, r.StartHead, r.StartSector)) + { + existingStart = _diskGeometry.ToChsAddress((int)r.LBAStart).Cylinder; + } + + if (r.LBAStart + r.LBALength > + _diskGeometry.ToLogicalBlockAddress(r.EndCylinder, r.EndHead, r.EndSector)) + { + existingEnd = _diskGeometry.ToChsAddress((int)(r.LBAStart + r.LBALength)).Cylinder; + } + + if ( + !Utilities.RangesOverlap(startCylinder, startCylinder + numCylinders - 1, existingStart, existingEnd)) + { + break; + } + startCylinder = existingEnd + 1; + } + + return startCylinder; + } + + private long FindGap(long numSectors, long alignmentSectors) + { + List list = Utilities.Filter, BiosPartitionRecord>(GetPrimaryRecords(), + r => r.IsValid); + list.Sort(); + + long startSector = MathUtilities.RoundUp(_diskGeometry.ToLogicalBlockAddress(0, 1, 1), alignmentSectors); + + int idx = 0; + while (idx < list.Count) + { + BiosPartitionRecord entry = list[idx]; + while (idx < list.Count && startSector >= entry.LBAStartAbsolute + entry.LBALength) + { + idx++; + entry = list[idx]; + } + + if (Utilities.RangesOverlap(startSector, startSector + numSectors, entry.LBAStartAbsolute, + entry.LBAStartAbsolute + entry.LBALength)) + { + startSector = MathUtilities.RoundUp(entry.LBAStartAbsolute + entry.LBALength, alignmentSectors); + } + + idx++; + } + + if (_diskGeometry.TotalSectorsLong - startSector < numSectors) + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, + "Unable to find free space of {0} sectors", numSectors)); + } + + return startSector; + } + + private void Init(Stream disk, Geometry diskGeometry) + { + _diskData = disk; + _diskGeometry = diskGeometry; + + _diskData.Position = 0; + byte[] bootSector = StreamUtilities.ReadExact(_diskData, Sizes.Sector); + if (bootSector[510] != 0x55 || bootSector[511] != 0xAA) + { + throw new IOException("Invalid boot sector - no magic number 0xAA55"); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/BiosPartitionTypes.cs b/DiscUtils/Core/Partitions/BiosPartitionTypes.cs new file mode 100644 index 0000000..114447b --- /dev/null +++ b/DiscUtils/Core/Partitions/BiosPartitionTypes.cs @@ -0,0 +1,197 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Partitions +{ + /// + /// Convenient access to well-known BIOS (MBR) Partition Types. + /// + public static class BiosPartitionTypes + { + /// + /// Microsoft FAT12 (fewer than 32,680 sectors in the volume). + /// + public const byte Fat12 = 0x01; + + /// + /// Microsoft FAT16 (32,680–65,535 sectors or 16 MB–33 MB). + /// + public const byte Fat16Small = 0x04; + + /// + /// Extended Partition (contains other partitions). + /// + public const byte Extended = 0x05; + + /// + /// Microsoft BIGDOS FAT16 (33 MB–4 GB). + /// + public const byte Fat16 = 0x06; + + /// + /// Installable File System (NTFS). + /// + public const byte Ntfs = 0x07; + + /// + /// Microsoft FAT32. + /// + public const byte Fat32 = 0x0B; + + /// + /// Microsoft FAT32, accessed using Int13h BIOS LBA extensions. + /// + public const byte Fat32Lba = 0x0C; + + /// + /// Microsoft BIGDOS FAT16, accessed using Int13h BIOS LBA extensions. + /// + public const byte Fat16Lba = 0x0E; + + /// + /// Extended Partition (contains other partitions), accessed using Int13h BIOS LBA extensions. + /// + public const byte ExtendedLba = 0x0F; + + /// + /// Windows Logical Disk Manager dynamic volume. + /// + public const byte WindowsDynamicVolume = 0x42; + + /// + /// Linux Swap. + /// + public const byte LinuxSwap = 0x82; + + /// + /// Linux Native (ext2 and friends). + /// + public const byte LinuxNative = 0x83; + + /// + /// Linux Logical Volume Manager (LVM). + /// + public const byte LinuxLvm = 0x8E; + + /// + /// GUID Partition Table (GPT) protective partition, fills entire disk. + /// + public const byte GptProtective = 0xEE; + + /// + /// EFI System partition on an MBR disk. + /// + public const byte EfiSystem = 0xEF; + + /// + /// Provides a string representation of some known BIOS partition types. + /// + /// The partition type to represent as a string. + /// The string representation. + public static string ToString(byte type) + { + switch (type) + { + case 0x00: + return "Unused"; + case 0x01: + return "FAT12"; + case 0x02: + return "XENIX root"; + case 0x03: + return "XENIX /usr"; + case 0x04: + return "FAT16 (<32M)"; + case 0x05: + return "Extended (non-LBA)"; + case 0x06: + return "FAT16 (>32M)"; + case 0x07: + return "IFS (NTFS or HPFS)"; + case 0x0B: + return "FAT32 (non-LBA)"; + case 0x0C: + return "FAT32 (LBA)"; + case 0x0E: + return "FAT16 (LBA)"; + case 0x0F: + return "Extended (LBA)"; + case 0x11: + return "Hidden FAT12"; + case 0x12: + return "Vendor Config/Recovery/Diagnostics"; + case 0x14: + return "Hidden FAT16 (<32M)"; + case 0x16: + return "Hidden FAT16 (>32M)"; + case 0x17: + return "Hidden IFS (NTFS or HPFS)"; + case 0x1B: + return "Hidden FAT32 (non-LBA)"; + case 0x1C: + return "Hidden FAT32 (LBA)"; + case 0x1E: + return "Hidden FAT16 (LBA)"; + case 0x27: + return "Windows Recovery Environment"; + case 0x42: + return "Windows Dynamic Volume"; + case 0x80: + return "Minix v1.1 - v1.4a"; + case 0x81: + return "Minix / Early Linux"; + case 0x82: + return "Linux Swap"; + case 0x83: + return "Linux Native"; + case 0x84: + return "Hibernation"; + case 0x8E: + return "Linux LVM"; + case 0xA0: + return "Laptop Hibernation"; + case 0xA8: + return "Mac OS-X"; + case 0xAB: + return "Mac OS-X Boot"; + case 0xAF: + return "Mac OS-X HFS"; + case 0xC0: + return "NTFT"; + case 0xDE: + return "Dell OEM"; + case 0xEE: + return "GPT Protective"; + case 0xEF: + return "EFI"; + case 0xFB: + return "VMware File System"; + case 0xFC: + return "VMware Swap"; + case 0xFE: + return "IBM OEM"; + default: + return "Unknown"; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/BiosPartitionedDiskBuilder.cs b/DiscUtils/Core/Partitions/BiosPartitionedDiskBuilder.cs new file mode 100644 index 0000000..23eecbe --- /dev/null +++ b/DiscUtils/Core/Partitions/BiosPartitionedDiskBuilder.cs @@ -0,0 +1,166 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + /// + /// Builds a stream with the contents of a BIOS partitioned disk. + /// + /// + /// This class assembles a disk image dynamically in memory. The + /// constructed stream will read data from the partition content + /// streams only when a client of this class tries to read from + /// that partition. + /// + public class BiosPartitionedDiskBuilder : StreamBuilder + { + private Geometry _biosGeometry; + private readonly SparseMemoryStream _bootSectors; + private readonly long _capacity; + + private readonly Dictionary _partitionContents; + + /// + /// Initializes a new instance of the BiosPartitionedDiskBuilder class. + /// + /// The capacity of the disk (in bytes). + /// The BIOS geometry of the disk. + public BiosPartitionedDiskBuilder(long capacity, Geometry biosGeometry) + { + _capacity = capacity; + _biosGeometry = biosGeometry; + + _bootSectors = new SparseMemoryStream(); + _bootSectors.SetLength(capacity); + PartitionTable = BiosPartitionTable.Initialize(_bootSectors, _biosGeometry); + + _partitionContents = new Dictionary(); + } + + /// + /// Initializes a new instance of the BiosPartitionedDiskBuilder class. + /// + /// The capacity of the disk (in bytes). + /// The boot sector(s) of the disk. + /// The BIOS geometry of the disk. + [Obsolete("Use the variant that takes VirtualDisk, this method breaks for disks with extended partitions", false + )] + public BiosPartitionedDiskBuilder(long capacity, byte[] bootSectors, Geometry biosGeometry) + { + if (bootSectors == null) + { + throw new ArgumentNullException(nameof(bootSectors)); + } + + _capacity = capacity; + _biosGeometry = biosGeometry; + + _bootSectors = new SparseMemoryStream(); + _bootSectors.SetLength(capacity); + _bootSectors.Write(bootSectors, 0, bootSectors.Length); + PartitionTable = new BiosPartitionTable(_bootSectors, biosGeometry); + + _partitionContents = new Dictionary(); + } + + /// + /// Initializes a new instance of the BiosPartitionedDiskBuilder class by + /// cloning the partition structure of a source disk. + /// + /// The disk to clone. + public BiosPartitionedDiskBuilder(VirtualDisk sourceDisk) + { + if (sourceDisk == null) + { + throw new ArgumentNullException(nameof(sourceDisk)); + } + + _capacity = sourceDisk.Capacity; + _biosGeometry = sourceDisk.BiosGeometry; + + _bootSectors = new SparseMemoryStream(); + _bootSectors.SetLength(_capacity); + + foreach (StreamExtent extent in new BiosPartitionTable(sourceDisk).GetMetadataDiskExtents()) + { + sourceDisk.Content.Position = extent.Start; + byte[] buffer = StreamUtilities.ReadExact(sourceDisk.Content, (int)extent.Length); + _bootSectors.Position = extent.Start; + _bootSectors.Write(buffer, 0, buffer.Length); + } + + PartitionTable = new BiosPartitionTable(_bootSectors, _biosGeometry); + + _partitionContents = new Dictionary(); + } + + /// + /// Gets the partition table in the disk. + /// + public BiosPartitionTable PartitionTable { get; } + + /// + /// Sets a stream representing the content of a partition in the partition table. + /// + /// The index of the partition. + /// The stream with the contents of the partition. + public void SetPartitionContent(int index, SparseStream stream) + { + _partitionContents[index] = new BuilderSparseStreamExtent(PartitionTable[index].FirstSector * Sizes.Sector, + stream); + } + + /// + /// Updates the CHS fields in partition records to reflect a new BIOS geometry. + /// + /// The disk's new BIOS geometry. + /// The partitions are not relocated to a cylinder boundary, just the CHS fields are updated on the + /// assumption the LBA fields are definitive. + public void UpdateBiosGeometry(Geometry geometry) + { + PartitionTable.UpdateBiosGeometry(geometry); + _biosGeometry = geometry; + } + + protected override List FixExtents(out long totalLength) + { + totalLength = _capacity; + + List extents = new List(); + + foreach (StreamExtent extent in PartitionTable.GetMetadataDiskExtents()) + { + _bootSectors.Position = extent.Start; + byte[] buffer = StreamUtilities.ReadExact(_bootSectors, (int)extent.Length); + + extents.Add(new BuilderBufferExtent(extent.Start, buffer)); + } + + extents.AddRange(_partitionContents.Values); + return extents; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/DefaultPartitionTableFactory.cs b/DiscUtils/Core/Partitions/DefaultPartitionTableFactory.cs new file mode 100644 index 0000000..ae8fb0f --- /dev/null +++ b/DiscUtils/Core/Partitions/DefaultPartitionTableFactory.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Partitions +{ + [PartitionTableFactory] + internal sealed class DefaultPartitionTableFactory : PartitionTableFactory + { + public override bool DetectIsPartitioned(Stream s) + { + return BiosPartitionTable.IsValid(s); + } + + public override PartitionTable DetectPartitionTable(VirtualDisk disk) + { + if (BiosPartitionTable.IsValid(disk.Content)) + { + BiosPartitionTable table = new BiosPartitionTable(disk); + if (table.Count == 1 && table[0].BiosType == BiosPartitionTypes.GptProtective) + { + return new GuidPartitionTable(disk); + } + return table; + } + return null; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/GptEntry.cs b/DiscUtils/Core/Partitions/GptEntry.cs new file mode 100644 index 0000000..918ebb5 --- /dev/null +++ b/DiscUtils/Core/Partitions/GptEntry.cs @@ -0,0 +1,146 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + internal class GptEntry : IComparable + { + public ulong Attributes; + public long FirstUsedLogicalBlock; + public Guid Identity; + public long LastUsedLogicalBlock; + public string Name; + public Guid PartitionType; + + public GptEntry() + { + PartitionType = Guid.Empty; + Identity = Guid.Empty; + Name = string.Empty; + } + + public string FriendlyPartitionType + { + get + { + switch (PartitionType.ToString().ToUpperInvariant()) + { + case "00000000-0000-0000-0000-000000000000": + return "Unused"; + case "024DEE41-33E7-11D3-9D69-0008C781F39F": + return "MBR Partition Scheme"; + case "C12A7328-F81F-11D2-BA4B-00A0C93EC93B": + return "EFI System"; + case "21686148-6449-6E6F-744E-656564454649": + return "BIOS Boot"; + case "E3C9E316-0B5C-4DB8-817D-F92DF00215AE": + return "Microsoft Reserved"; + case "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7": + return "Windows Basic Data"; + case "5808C8AA-7E8F-42E0-85D2-E1E90434CFB3": + return "Windows Logical Disk Manager Metadata"; + case "AF9B60A0-1431-4F62-BC68-3311714A69AD": + return "Windows Logical Disk Manager Data"; + case "75894C1E-3AEB-11D3-B7C1-7B03A0000000": + return "HP-UX Data"; + case "E2A1E728-32E3-11D6-A682-7B03A0000000": + return "HP-UX Service"; + case "A19D880F-05FC-4D3B-A006-743F0F84911E": + return "Linux RAID"; + case "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F": + return "Linux Swap"; + case "E6D6D379-F507-44C2-A23C-238F2A3DF928": + return "Linux Logical Volume Manager"; + case "83BD6B9D-7F41-11DC-BE0B-001560B84F0F": + return "FreeBSD Boot"; + case "516E7CB4-6ECF-11D6-8FF8-00022D09712B": + return "FreeBSD Data"; + case "516E7CB5-6ECF-11D6-8FF8-00022D09712B": + return "FreeBSD Swap"; + case "516E7CB6-6ECF-11D6-8FF8-00022D09712B": + return "FreeBSD Unix File System"; + case "516E7CB8-6ECF-11D6-8FF8-00022D09712B": + return "FreeBSD Vinum volume manager"; + case "516E7CBA-6ECF-11D6-8FF8-00022D09712B": + return "FreeBSD ZFS"; + case "48465300-0000-11AA-AA11-00306543ECAC": + return "Mac OS X HFS+"; + case "55465300-0000-11AA-AA11-00306543ECAC": + return "Mac OS X UFS"; + case "6A898CC3-1DD2-11B2-99A6-080020736631": + return "Mac OS X ZFS"; + case "52414944-0000-11AA-AA11-00306543ECAC": + return "Mac OS X RAID"; + case "52414944-5F4F-11AA-AA11-00306543ECAC": + return "Mac OS X RAID, Offline"; + case "426F6F74-0000-11AA-AA11-00306543ECAC": + return "Mac OS X Boot"; + case "4C616265-6C00-11AA-AA11-00306543ECAC": + return "Mac OS X Label"; + case "49F48D32-B10E-11DC-B99B-0019D1879648": + return "NetBSD Swap"; + case "49F48D5A-B10E-11DC-B99B-0019D1879648": + return "NetBSD Fast File System"; + case "49F48D82-B10E-11DC-B99B-0019D1879648": + return "NetBSD Log-Structed File System"; + case "49F48DAA-B10E-11DC-B99B-0019D1879648": + return "NetBSD RAID"; + case "2DB519C4-B10F-11DC-B99B-0019D1879648": + return "NetBSD Concatenated"; + case "2DB519EC-B10F-11DC-B99B-0019D1879648": + return "NetBSD Encrypted"; + default: + return "Unknown"; + } + } + } + + public int CompareTo(GptEntry other) + { + return FirstUsedLogicalBlock.CompareTo(other.FirstUsedLogicalBlock); + } + + public void ReadFrom(byte[] buffer, int offset) + { + PartitionType = EndianUtilities.ToGuidLittleEndian(buffer, offset + 0); + Identity = EndianUtilities.ToGuidLittleEndian(buffer, offset + 16); + FirstUsedLogicalBlock = EndianUtilities.ToInt64LittleEndian(buffer, offset + 32); + LastUsedLogicalBlock = EndianUtilities.ToInt64LittleEndian(buffer, offset + 40); + Attributes = EndianUtilities.ToUInt64LittleEndian(buffer, offset + 48); + Name = Encoding.Unicode.GetString(buffer, offset + 56, 72).TrimEnd('\0'); + } + + public void WriteTo(byte[] buffer, int offset) + { + EndianUtilities.WriteBytesLittleEndian(PartitionType, buffer, offset + 0); + EndianUtilities.WriteBytesLittleEndian(Identity, buffer, offset + 16); + EndianUtilities.WriteBytesLittleEndian(FirstUsedLogicalBlock, buffer, offset + 32); + EndianUtilities.WriteBytesLittleEndian(LastUsedLogicalBlock, buffer, offset + 40); + EndianUtilities.WriteBytesLittleEndian(Attributes, buffer, offset + 48); + Encoding.Unicode.GetBytes(Name + new string('\0', 36), 0, 36, buffer, offset + 56); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/GptHeader.cs b/DiscUtils/Core/Partitions/GptHeader.cs new file mode 100644 index 0000000..2153ba1 --- /dev/null +++ b/DiscUtils/Core/Partitions/GptHeader.cs @@ -0,0 +1,146 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + internal class GptHeader + { + public const string GptSignature = "EFI PART"; + public long AlternateHeaderLba; + + public byte[] Buffer; + public uint Crc; + public Guid DiskGuid; + public uint EntriesCrc; + public long FirstUsable; + public long HeaderLba; + public int HeaderSize; + public long LastUsable; + public long PartitionEntriesLba; + public uint PartitionEntryCount; + public int PartitionEntrySize; + + public string Signature; + public uint Version; + + public GptHeader(int sectorSize) + { + Signature = GptSignature; + Version = 0x00010000; + HeaderSize = 92; + Buffer = new byte[sectorSize]; + } + + public GptHeader(GptHeader toCopy) + { + Signature = toCopy.Signature; + Version = toCopy.Version; + HeaderSize = toCopy.HeaderSize; + Crc = toCopy.Crc; + HeaderLba = toCopy.HeaderLba; + AlternateHeaderLba = toCopy.AlternateHeaderLba; + FirstUsable = toCopy.FirstUsable; + LastUsable = toCopy.LastUsable; + DiskGuid = toCopy.DiskGuid; + PartitionEntriesLba = toCopy.PartitionEntriesLba; + PartitionEntryCount = toCopy.PartitionEntryCount; + PartitionEntrySize = toCopy.PartitionEntrySize; + EntriesCrc = toCopy.EntriesCrc; + + Buffer = new byte[toCopy.Buffer.Length]; + Array.Copy(toCopy.Buffer, Buffer, Buffer.Length); + } + + public bool ReadFrom(byte[] buffer, int offset) + { + Signature = EndianUtilities.BytesToString(buffer, offset + 0, 8); + Version = EndianUtilities.ToUInt32LittleEndian(buffer, offset + 8); + HeaderSize = EndianUtilities.ToInt32LittleEndian(buffer, offset + 12); + Crc = EndianUtilities.ToUInt32LittleEndian(buffer, offset + 16); + HeaderLba = EndianUtilities.ToInt64LittleEndian(buffer, offset + 24); + AlternateHeaderLba = EndianUtilities.ToInt64LittleEndian(buffer, offset + 32); + FirstUsable = EndianUtilities.ToInt64LittleEndian(buffer, offset + 40); + LastUsable = EndianUtilities.ToInt64LittleEndian(buffer, offset + 48); + DiskGuid = EndianUtilities.ToGuidLittleEndian(buffer, offset + 56); + PartitionEntriesLba = EndianUtilities.ToInt64LittleEndian(buffer, offset + 72); + PartitionEntryCount = EndianUtilities.ToUInt32LittleEndian(buffer, offset + 80); + PartitionEntrySize = EndianUtilities.ToInt32LittleEndian(buffer, offset + 84); + EntriesCrc = EndianUtilities.ToUInt32LittleEndian(buffer, offset + 88); + + // In case the header has new fields unknown to us, store the entire header + // as a byte array + Buffer = new byte[HeaderSize]; + Array.Copy(buffer, offset, Buffer, 0, HeaderSize); + + // Reject obviously invalid data + if (Signature != GptSignature || HeaderSize == 0) + { + return false; + } + + return Crc == CalcCrc(buffer, offset, HeaderSize); + } + + public void WriteTo(byte[] buffer, int offset) + { + // First, copy the cached header to allow for unknown fields + Array.Copy(Buffer, 0, buffer, offset, Buffer.Length); + + // Next, write the fields + EndianUtilities.StringToBytes(Signature, buffer, offset + 0, 8); + EndianUtilities.WriteBytesLittleEndian(Version, buffer, offset + 8); + EndianUtilities.WriteBytesLittleEndian(HeaderSize, buffer, offset + 12); + EndianUtilities.WriteBytesLittleEndian((uint)0, buffer, offset + 16); + EndianUtilities.WriteBytesLittleEndian(HeaderLba, buffer, offset + 24); + EndianUtilities.WriteBytesLittleEndian(AlternateHeaderLba, buffer, offset + 32); + EndianUtilities.WriteBytesLittleEndian(FirstUsable, buffer, offset + 40); + EndianUtilities.WriteBytesLittleEndian(LastUsable, buffer, offset + 48); + EndianUtilities.WriteBytesLittleEndian(DiskGuid, buffer, offset + 56); + EndianUtilities.WriteBytesLittleEndian(PartitionEntriesLba, buffer, offset + 72); + EndianUtilities.WriteBytesLittleEndian(PartitionEntryCount, buffer, offset + 80); + EndianUtilities.WriteBytesLittleEndian(PartitionEntrySize, buffer, offset + 84); + EndianUtilities.WriteBytesLittleEndian(EntriesCrc, buffer, offset + 88); + + // Calculate & write the CRC + EndianUtilities.WriteBytesLittleEndian(CalcCrc(buffer, offset, HeaderSize), buffer, offset + 16); + + // Update the cached copy - re-allocate the buffer to allow for HeaderSize potentially having changed + Buffer = new byte[HeaderSize]; + Array.Copy(buffer, offset, Buffer, 0, HeaderSize); + } + + internal static uint CalcCrc(byte[] buffer, int offset, int count) + { + byte[] temp = new byte[count]; + Array.Copy(buffer, offset, temp, 0, count); + + // Reset CRC field + EndianUtilities.WriteBytesLittleEndian((uint)0, temp, 16); + + return Crc32LittleEndian.Compute(Crc32Algorithm.Common, temp, 0, count); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/GuidPartitionInfo.cs b/DiscUtils/Core/Partitions/GuidPartitionInfo.cs new file mode 100644 index 0000000..6669fe6 --- /dev/null +++ b/DiscUtils/Core/Partitions/GuidPartitionInfo.cs @@ -0,0 +1,120 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + /// + /// Provides access to partition records in a GUID partition table. + /// + public sealed class GuidPartitionInfo : PartitionInfo + { + private readonly GptEntry _entry; + private readonly GuidPartitionTable _table; + + internal GuidPartitionInfo(GuidPartitionTable table, GptEntry entry) + { + _table = table; + _entry = entry; + } + + /// + /// Gets the attributes of the partition. + /// + public long Attributes + { + get { return (long)_entry.Attributes; } + } + + /// + /// Always returns Zero. + /// + public override byte BiosType + { + get { return 0; } + } + + /// + /// Gets the first sector of the partion (relative to start of disk) as a Logical Block Address. + /// + public override long FirstSector + { + get { return _entry.FirstUsedLogicalBlock; } + } + + /// + /// Gets the type of the partition, as a GUID. + /// + public override Guid GuidType + { + get { return _entry.PartitionType; } + } + + /// + /// Gets the unique identity of this specific partition. + /// + public Guid Identity + { + get { return _entry.Identity; } + } + + /// + /// Gets the last sector of the partion (relative to start of disk) as a Logical Block Address (inclusive). + /// + public override long LastSector + { + get { return _entry.LastUsedLogicalBlock; } + } + + /// + /// Gets the name of the partition. + /// + public string Name + { + get { return _entry.Name; } + } + + /// + /// Gets the type of the partition as a string. + /// + public override string TypeAsString + { + get { return _entry.FriendlyPartitionType; } + } + + internal override PhysicalVolumeType VolumeType + { + get { return PhysicalVolumeType.GptPartition; } + } + + /// + /// Opens a stream to access the content of the partition. + /// + /// The new stream. + public override SparseStream Open() + { + return _table.Open(_entry); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/GuidPartitionTable.cs b/DiscUtils/Core/Partitions/GuidPartitionTable.cs new file mode 100644 index 0000000..e8abfb5 --- /dev/null +++ b/DiscUtils/Core/Partitions/GuidPartitionTable.cs @@ -0,0 +1,657 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + /// + /// Represents a GUID Partition Table. + /// + public sealed class GuidPartitionTable : PartitionTable + { + private Stream _diskData; + private Geometry _diskGeometry; + private byte[] _entryBuffer; + private GptHeader _primaryHeader; + private GptHeader _secondaryHeader; + + /// + /// Initializes a new instance of the GuidPartitionTable class. + /// + /// The disk containing the partition table. + public GuidPartitionTable(VirtualDisk disk) + { + Init(disk.Content, disk.Geometry); + } + + /// + /// Initializes a new instance of the GuidPartitionTable class. + /// + /// The stream containing the disk data. + /// The geometry of the disk. + public GuidPartitionTable(Stream disk, Geometry diskGeometry) + { + Init(disk, diskGeometry); + } + + /// + /// Gets the unique GPT identifier for this disk. + /// + public override Guid DiskGuid + { + get { return _primaryHeader.DiskGuid; } + } + + /// + /// Gets the first sector of the disk available to hold partitions. + /// + public long FirstUsableSector + { + get { return _primaryHeader.FirstUsable; } + } + + /// + /// Gets the last sector of the disk available to hold partitions. + /// + public long LastUsableSector + { + get { return _primaryHeader.LastUsable; } + } + + /// + /// Gets a collection of the partitions for storing Operating System file-systems. + /// + public override ReadOnlyCollection Partitions + { + get + { + return + new ReadOnlyCollection(Utilities.Map(GetAllEntries(), + e => new GuidPartitionInfo(this, e))); + } + } + + /// + /// Creates a new partition table on a disk. + /// + /// The disk to initialize. + /// An object to access the newly created partition table. + public static GuidPartitionTable Initialize(VirtualDisk disk) + { + return Initialize(disk.Content, disk.Geometry); + } + + /// + /// Creates a new partition table on a disk. + /// + /// The stream containing the disk data. + /// The geometry of the disk. + /// An object to access the newly created partition table. + public static GuidPartitionTable Initialize(Stream disk, Geometry diskGeometry) + { + // Create the protective MBR partition record. + BiosPartitionTable pt = BiosPartitionTable.Initialize(disk, diskGeometry); + pt.CreatePrimaryByCylinder(0, diskGeometry.Cylinders - 1, BiosPartitionTypes.GptProtective, false); + + // Create the GPT headers, and blank-out the entry areas + const int EntryCount = 128; + const int EntrySize = 128; + + int entrySectors = (EntryCount * EntrySize + diskGeometry.BytesPerSector - 1) / diskGeometry.BytesPerSector; + + byte[] entriesBuffer = new byte[EntryCount * EntrySize]; + + // Prepare primary header + GptHeader header = new GptHeader(diskGeometry.BytesPerSector); + header.HeaderLba = 1; + header.AlternateHeaderLba = disk.Length / diskGeometry.BytesPerSector - 1; + header.FirstUsable = header.HeaderLba + entrySectors + 1; + header.LastUsable = header.AlternateHeaderLba - entrySectors - 1; + header.DiskGuid = Guid.NewGuid(); + header.PartitionEntriesLba = 2; + header.PartitionEntryCount = EntryCount; + header.PartitionEntrySize = EntrySize; + header.EntriesCrc = CalcEntriesCrc(entriesBuffer); + + // Write the primary header + byte[] headerBuffer = new byte[diskGeometry.BytesPerSector]; + header.WriteTo(headerBuffer, 0); + disk.Position = header.HeaderLba * diskGeometry.BytesPerSector; + disk.Write(headerBuffer, 0, headerBuffer.Length); + + // Calc alternate header + header.HeaderLba = header.AlternateHeaderLba; + header.AlternateHeaderLba = 1; + header.PartitionEntriesLba = header.HeaderLba - entrySectors; + + // Write the alternate header + header.WriteTo(headerBuffer, 0); + disk.Position = header.HeaderLba * diskGeometry.BytesPerSector; + disk.Write(headerBuffer, 0, headerBuffer.Length); + + return new GuidPartitionTable(disk, diskGeometry); + } + + /// + /// Creates a new partition table on a disk containing a single partition. + /// + /// The disk to initialize. + /// The partition type for the single partition. + /// An object to access the newly created partition table. + public static GuidPartitionTable Initialize(VirtualDisk disk, WellKnownPartitionType type) + { + GuidPartitionTable pt = Initialize(disk); + pt.Create(type, true); + return pt; + } + + /// + /// Creates a new partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + public override int Create(WellKnownPartitionType type, bool active) + { + List allEntries = new List(GetAllEntries()); + + EstablishReservedPartition(allEntries); + + // Fill the rest of the disk with the requested partition + long start = FirstAvailableSector(allEntries); + long end = FindLastFreeSector(start, allEntries); + + return Create(start, end, GuidPartitionTypes.Convert(type), 0, "Data Partition"); + } + + /// + /// Creates a new primary partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the new partition. + public override int Create(long size, WellKnownPartitionType type, bool active) + { + if (size < _diskGeometry.BytesPerSector) + { + throw new ArgumentOutOfRangeException(nameof(size), size, "size must be at least one sector"); + } + + long sectorLength = size / _diskGeometry.BytesPerSector; + long start = FindGap(size / _diskGeometry.BytesPerSector, 1); + + return Create(start, start + sectorLength - 1, GuidPartitionTypes.Convert(type), 0, "Data Partition"); + } + + /// + /// Creates a new aligned partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in bytes). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is acheived by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(WellKnownPartitionType type, bool active, int alignment) + { + if (alignment % _diskGeometry.BytesPerSector != 0) + { + throw new ArgumentException("Alignment is not a multiple of the sector size"); + } + + List allEntries = new List(GetAllEntries()); + + EstablishReservedPartition(allEntries); + + // Fill the rest of the disk with the requested partition + long start = MathUtilities.RoundUp(FirstAvailableSector(allEntries), alignment / _diskGeometry.BytesPerSector); + long end = MathUtilities.RoundDown(FindLastFreeSector(start, allEntries) + 1, + alignment / _diskGeometry.BytesPerSector); + + if (end <= start) + { + throw new IOException("No available space"); + } + + return Create(start, end - 1, GuidPartitionTypes.Convert(type), 0, "Data Partition"); + } + + /// + /// Creates a new aligned partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in bytes). + /// The index of the new partition. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is achieved by aligning partitions on + /// large values that are a power of two. + /// + public override int CreateAligned(long size, WellKnownPartitionType type, bool active, int alignment) + { + if (size < _diskGeometry.BytesPerSector) + { + throw new ArgumentOutOfRangeException(nameof(size), size, "size must be at least one sector"); + } + + if (alignment % _diskGeometry.BytesPerSector != 0) + { + throw new ArgumentException("Alignment is not a multiple of the sector size"); + } + + if (size % alignment != 0) + { + throw new ArgumentException("Size is not a multiple of the alignment"); + } + + long sectorLength = size / _diskGeometry.BytesPerSector; + long start = FindGap(size / _diskGeometry.BytesPerSector, alignment / _diskGeometry.BytesPerSector); + + return Create(start, start + sectorLength - 1, GuidPartitionTypes.Convert(type), 0, "Data Partition"); + } + + /// + /// Creates a new GUID partition on the disk. + /// + /// The first sector of the partition. + /// The last sector of the partition. + /// The partition type. + /// The partition attributes. + /// The name of the partition. + /// The index of the new partition. + /// No checking is performed on the parameters, the caller is + /// responsible for ensuring that the partition does not overlap other partitions. + public int Create(long startSector, long endSector, Guid type, long attributes, string name) + { + GptEntry newEntry = CreateEntry(startSector, endSector, type, attributes, name); + return GetEntryIndex(newEntry.Identity); + } + + /// + /// Deletes a partition at a given index. + /// + /// The index of the partition. + public override void Delete(int index) + { + int offset = GetPartitionOffset(index); + Array.Clear(_entryBuffer, offset, _primaryHeader.PartitionEntrySize); + Write(); + } + + internal SparseStream Open(GptEntry entry) + { + long start = entry.FirstUsedLogicalBlock * _diskGeometry.BytesPerSector; + long end = (entry.LastUsedLogicalBlock + 1) * _diskGeometry.BytesPerSector; + return new SubStream(_diskData, start, end - start); + } + + private static uint CalcEntriesCrc(byte[] buffer) + { + return Crc32LittleEndian.Compute(Crc32Algorithm.Common, buffer, 0, buffer.Length); + } + + private static int CountEntries(ICollection values, Func pred) + { + int count = 0; + + foreach (T val in values) + { + if (pred(val)) + { + ++count; + } + } + + return count; + } + + private void Init(Stream disk, Geometry diskGeometry) + { + BiosPartitionTable bpt; + try + { + bpt = new BiosPartitionTable(disk, diskGeometry); + } + catch (IOException ioe) + { + throw new IOException("Invalid GPT disk, protective MBR table not present or invalid", ioe); + } + + if (bpt.Count != 1 || bpt[0].BiosType != BiosPartitionTypes.GptProtective) + { + throw new IOException("Invalid GPT disk, protective MBR table is not valid"); + } + + _diskData = disk; + _diskGeometry = diskGeometry; + + disk.Position = diskGeometry.BytesPerSector; + byte[] sector = StreamUtilities.ReadExact(disk, diskGeometry.BytesPerSector); + + _primaryHeader = new GptHeader(diskGeometry.BytesPerSector); + if (!_primaryHeader.ReadFrom(sector, 0) || !ReadEntries(_primaryHeader)) + { + disk.Position = disk.Length - diskGeometry.BytesPerSector; + disk.Read(sector, 0, sector.Length); + _secondaryHeader = new GptHeader(diskGeometry.BytesPerSector); + if (!_secondaryHeader.ReadFrom(sector, 0) || !ReadEntries(_secondaryHeader)) + { + throw new IOException("No valid GUID Partition Table found"); + } + + // Generate from the primary table from the secondary one + _primaryHeader = new GptHeader(_secondaryHeader); + _primaryHeader.HeaderLba = _secondaryHeader.AlternateHeaderLba; + _primaryHeader.AlternateHeaderLba = _secondaryHeader.HeaderLba; + _primaryHeader.PartitionEntriesLba = 2; + + // If the disk is writeable, fix up the primary partition table based on the + // (valid) secondary table. + if (disk.CanWrite) + { + WritePrimaryHeader(); + } + } + + if (_secondaryHeader == null) + { + _secondaryHeader = new GptHeader(diskGeometry.BytesPerSector); + disk.Position = disk.Length - diskGeometry.BytesPerSector; + disk.Read(sector, 0, sector.Length); + if (!_secondaryHeader.ReadFrom(sector, 0) || !ReadEntries(_secondaryHeader)) + { + // Generate from the secondary table from the primary one + _secondaryHeader = new GptHeader(_primaryHeader); + _secondaryHeader.HeaderLba = _secondaryHeader.AlternateHeaderLba; + _secondaryHeader.AlternateHeaderLba = _secondaryHeader.HeaderLba; + _secondaryHeader.PartitionEntriesLba = _secondaryHeader.HeaderLba - + MathUtilities.RoundUp( + _secondaryHeader.PartitionEntryCount * + _secondaryHeader.PartitionEntrySize, + diskGeometry.BytesPerSector); + + // If the disk is writeable, fix up the secondary partition table based on the + // (valid) primary table. + if (disk.CanWrite) + { + WriteSecondaryHeader(); + } + } + } + } + + private void EstablishReservedPartition(List allEntries) + { + // If no MicrosoftReserved partition, and no Microsoft Data partitions, and the disk + // has a 'reasonable' size free, create a Microsoft Reserved partition. + if (CountEntries(allEntries, e => e.PartitionType == GuidPartitionTypes.MicrosoftReserved) == 0 + && CountEntries(allEntries, e => e.PartitionType == GuidPartitionTypes.WindowsBasicData) == 0 + && _diskGeometry.Capacity > 512 * 1024 * 1024) + { + long reservedStart = FirstAvailableSector(allEntries); + long reservedEnd = FindLastFreeSector(reservedStart, allEntries); + + if ((reservedEnd - reservedStart + 1) * _diskGeometry.BytesPerSector > 512 * 1024 * 1024) + { + long size = (_diskGeometry.Capacity < 16 * 1024L * 1024 * 1024 ? 32 : 128) * 1024 * 1024; + reservedEnd = reservedStart + size / _diskGeometry.BytesPerSector - 1; + + int reservedOffset = GetFreeEntryOffset(); + GptEntry newReservedEntry = new GptEntry(); + newReservedEntry.PartitionType = GuidPartitionTypes.MicrosoftReserved; + newReservedEntry.Identity = Guid.NewGuid(); + newReservedEntry.FirstUsedLogicalBlock = reservedStart; + newReservedEntry.LastUsedLogicalBlock = reservedEnd; + newReservedEntry.Attributes = 0; + newReservedEntry.Name = "Microsoft reserved partition"; + newReservedEntry.WriteTo(_entryBuffer, reservedOffset); + allEntries.Add(newReservedEntry); + } + } + } + + private GptEntry CreateEntry(long startSector, long endSector, Guid type, long attributes, string name) + { + if (endSector < startSector) + { + throw new ArgumentException("The end sector is before the start sector"); + } + + int offset = GetFreeEntryOffset(); + GptEntry newEntry = new GptEntry(); + newEntry.PartitionType = type; + newEntry.Identity = Guid.NewGuid(); + newEntry.FirstUsedLogicalBlock = startSector; + newEntry.LastUsedLogicalBlock = endSector; + newEntry.Attributes = (ulong)attributes; + newEntry.Name = name; + newEntry.WriteTo(_entryBuffer, offset); + + // Commit changes to disk + Write(); + + return newEntry; + } + + private long FindGap(long numSectors, long alignmentSectors) + { + List list = new List(GetAllEntries()); + list.Sort(); + + long startSector = MathUtilities.RoundUp(_primaryHeader.FirstUsable, alignmentSectors); + foreach (GptEntry entry in list) + { + if ( + !Utilities.RangesOverlap(startSector, startSector + numSectors - 1, entry.FirstUsedLogicalBlock, + entry.LastUsedLogicalBlock)) + { + break; + } + startSector = MathUtilities.RoundUp(entry.LastUsedLogicalBlock + 1, alignmentSectors); + } + + if (_diskGeometry.TotalSectorsLong - startSector < numSectors) + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, + "Unable to find free space of {0} sectors", numSectors)); + } + + return startSector; + } + + private long FirstAvailableSector(List allEntries) + { + long start = _primaryHeader.FirstUsable; + + foreach (GptEntry entry in allEntries) + { + if (entry.LastUsedLogicalBlock >= start) + { + start = entry.LastUsedLogicalBlock + 1; + } + } + + return start; + } + + private long FindLastFreeSector(long start, List allEntries) + { + long end = _primaryHeader.LastUsable; + + foreach (GptEntry entry in allEntries) + { + if (entry.LastUsedLogicalBlock > start && entry.FirstUsedLogicalBlock <= end) + { + end = entry.FirstUsedLogicalBlock - 1; + } + } + + return end; + } + + private void Write() + { + WritePrimaryHeader(); + WriteSecondaryHeader(); + } + + private void WritePrimaryHeader() + { + byte[] buffer = new byte[_diskGeometry.BytesPerSector]; + _primaryHeader.EntriesCrc = CalcEntriesCrc(); + _primaryHeader.WriteTo(buffer, 0); + _diskData.Position = _diskGeometry.BytesPerSector; + _diskData.Write(buffer, 0, buffer.Length); + + _diskData.Position = 2 * _diskGeometry.BytesPerSector; + _diskData.Write(_entryBuffer, 0, _entryBuffer.Length); + } + + private void WriteSecondaryHeader() + { + byte[] buffer = new byte[_diskGeometry.BytesPerSector]; + _secondaryHeader.EntriesCrc = CalcEntriesCrc(); + _secondaryHeader.WriteTo(buffer, 0); + _diskData.Position = _diskData.Length - _diskGeometry.BytesPerSector; + _diskData.Write(buffer, 0, buffer.Length); + + _diskData.Position = _secondaryHeader.PartitionEntriesLba * _diskGeometry.BytesPerSector; + _diskData.Write(_entryBuffer, 0, _entryBuffer.Length); + } + + private bool ReadEntries(GptHeader header) + { + _diskData.Position = header.PartitionEntriesLba * _diskGeometry.BytesPerSector; + _entryBuffer = StreamUtilities.ReadExact(_diskData, (int)(header.PartitionEntrySize * header.PartitionEntryCount)); + if (header.EntriesCrc != CalcEntriesCrc()) + { + return false; + } + + return true; + } + + private uint CalcEntriesCrc() + { + return Crc32LittleEndian.Compute(Crc32Algorithm.Common, _entryBuffer, 0, _entryBuffer.Length); + } + + private IEnumerable GetAllEntries() + { + for (int i = 0; i < _primaryHeader.PartitionEntryCount; ++i) + { + GptEntry entry = new GptEntry(); + entry.ReadFrom(_entryBuffer, i * _primaryHeader.PartitionEntrySize); + if (entry.PartitionType != Guid.Empty) + { + yield return entry; + } + } + } + + private int GetPartitionOffset(int index) + { + bool found = false; + int entriesSoFar = 0; + int position = 0; + + while (!found && position < _primaryHeader.PartitionEntryCount) + { + GptEntry entry = new GptEntry(); + entry.ReadFrom(_entryBuffer, position * _primaryHeader.PartitionEntrySize); + if (entry.PartitionType != Guid.Empty) + { + if (index == entriesSoFar) + { + found = true; + break; + } + + entriesSoFar++; + } + + position++; + } + + if (found) + { + return position * _primaryHeader.PartitionEntrySize; + } + throw new IOException(string.Format(CultureInfo.InvariantCulture, "No such partition: {0}", index)); + } + + private int GetEntryIndex(Guid identity) + { + int index = 0; + + for (int i = 0; i < _primaryHeader.PartitionEntryCount; ++i) + { + GptEntry entry = new GptEntry(); + entry.ReadFrom(_entryBuffer, i * _primaryHeader.PartitionEntrySize); + + if (entry.Identity == identity) + { + return index; + } + if (entry.PartitionType != Guid.Empty) + { + index++; + } + } + + throw new IOException("No such partition"); + } + + private int GetFreeEntryOffset() + { + for (int i = 0; i < _primaryHeader.PartitionEntryCount; ++i) + { + GptEntry entry = new GptEntry(); + entry.ReadFrom(_entryBuffer, i * _primaryHeader.PartitionEntrySize); + + if (entry.PartitionType == Guid.Empty) + { + return i * _primaryHeader.PartitionEntrySize; + } + } + + throw new IOException("No free partition entries available"); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/GuidPartitionTypes.cs b/DiscUtils/Core/Partitions/GuidPartitionTypes.cs new file mode 100644 index 0000000..d368c99 --- /dev/null +++ b/DiscUtils/Core/Partitions/GuidPartitionTypes.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Partitions +{ + /// + /// Convenient access to well known GPT partition types. + /// + public static class GuidPartitionTypes + { + /// + /// EFI system partition. + /// + public static readonly Guid EfiSystem = new Guid("C12A7328-F81F-11D2-BA4B-00A0C93EC93B"); + + /// + /// BIOS boot partition. + /// + public static readonly Guid BiosBoot = new Guid("21686148-6449-6E6F-744E-656564454649"); + + /// + /// Microsoft reserved partition. + /// + public static readonly Guid MicrosoftReserved = new Guid("E3C9E316-0B5C-4DB8-817D-F92DF00215AE"); + + /// + /// Windows basic data partition. + /// + public static readonly Guid WindowsBasicData = new Guid("EBD0A0A2-B9E5-4433-87C0-68B6B72699C7"); + + /// + /// Linux LVM partition. + /// + public static readonly Guid LinuxLvm = new Guid("E6D6D379-F507-44C2-A23C-238F2A3DF928"); + + /// + /// Linux swap partition. + /// + public static readonly Guid LinuxSwap = new Guid("0657FD6D-A4AB-43C4-84E5-0933C84B4F4F"); + + /// + /// Windows Logical Disk Manager metadata. + /// + public static readonly Guid WindowsLdmMetadata = new Guid("5808C8AA-7E8F-42E0-85D2-E1E90434CFB3"); + + /// + /// Windows Logical Disk Manager data. + /// + public static readonly Guid WindowsLdmData = new Guid("AF9B60A0-1431-4F62-BC68-3311714A69AD"); + + /// + /// Converts a well known partition type to a Guid. + /// + /// The value to convert. + /// The GUID value. + internal static Guid Convert(WellKnownPartitionType wellKnown) + { + switch (wellKnown) + { + case WellKnownPartitionType.Linux: + case WellKnownPartitionType.WindowsFat: + case WellKnownPartitionType.WindowsNtfs: + return WindowsBasicData; + case WellKnownPartitionType.LinuxLvm: + return LinuxLvm; + case WellKnownPartitionType.LinuxSwap: + return LinuxSwap; + default: + throw new ArgumentException("Unknown partition type"); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/PartitionInfo.cs b/DiscUtils/Core/Partitions/PartitionInfo.cs new file mode 100644 index 0000000..856d1aa --- /dev/null +++ b/DiscUtils/Core/Partitions/PartitionInfo.cs @@ -0,0 +1,93 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + /// + /// Base class representing a disk partition. + /// + /// The purpose of this class is to provide a minimal view of a partition, + /// such that callers can access existing partitions without specific knowledge of + /// the partitioning system. + public abstract class PartitionInfo + { + /// + /// Gets the type of the partition, in legacy BIOS form, when available. + /// + /// Zero for GUID-style partitions. + public abstract byte BiosType { get; } + + /// + /// Gets the first sector of the partion (relative to start of disk) as a Logical Block Address. + /// + public abstract long FirstSector { get; } + + /// + /// Gets the type of the partition, as a GUID, when available. + /// + /// .Empty for MBR-style partitions. + public abstract Guid GuidType { get; } + + /// + /// Gets the last sector of the partion (relative to start of disk) as a Logical Block Address (inclusive). + /// + public abstract long LastSector { get; } + + /// + /// Gets the length of the partition in sectors. + /// + public virtual long SectorCount + { + get { return 1 + LastSector - FirstSector; } + } + + /// + /// Gets the partition type as a 'friendly' string. + /// + public abstract string TypeAsString { get; } + + /// + /// Gets the physical volume type for this type of partition. + /// + internal abstract PhysicalVolumeType VolumeType { get; } + + /// + /// Opens a stream that accesses the partition's contents. + /// + /// The new stream. + public abstract SparseStream Open(); + + /// + /// Gets a summary of the partition information as 'first - last (type)'. + /// + /// A string representation of the partition information. + public override string ToString() + { + return string.Format(CultureInfo.InvariantCulture, "0x{0:X} - 0x{1:X} ({2})", FirstSector, LastSector, + TypeAsString); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/PartitionTable.cs b/DiscUtils/Core/Partitions/PartitionTable.cs new file mode 100644 index 0000000..a9b035a --- /dev/null +++ b/DiscUtils/Core/Partitions/PartitionTable.cs @@ -0,0 +1,211 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.ObjectModel; +using System.IO; +using DiscUtils.CoreCompat; +using DiscUtils.Raw; +using DiscUtils.Streams; + +namespace DiscUtils.Partitions +{ + /// + /// Base class for classes which represent a disk partitioning scheme. + /// + /// After modifying the table, by creating or deleting a partition assume that any + /// previously stored partition indexes of higher value are no longer valid. Re-enumerate + /// the partitions to discover the next index-to-partition mapping. + public abstract class PartitionTable + { + private static List _factories; + + /// + /// Gets the number of User partitions on the disk. + /// + public int Count + { + get { return Partitions.Count; } + } + + /// + /// Gets the GUID that uniquely identifies this disk, if supported (else returns null). + /// + public abstract Guid DiskGuid { get; } + + private static List Factories + { + get + { + if (_factories == null) + { + List factories = new List(); + + foreach (Type type in ReflectionHelper.GetAssembly(typeof(VolumeManager)).GetTypes()) + { + foreach (PartitionTableFactoryAttribute attr in ReflectionHelper.GetCustomAttributes(type, typeof(PartitionTableFactoryAttribute), false)) + { + factories.Add((PartitionTableFactory)Activator.CreateInstance(type)); + } + } + + _factories = factories; + } + + return _factories; + } + } + + /// + /// Gets information about a particular User partition. + /// + /// The index of the partition. + /// Information about the partition. + public PartitionInfo this[int index] + { + get { return Partitions[index]; } + } + + /// + /// Gets the list of partitions that contain user data (i.e. non-system / empty). + /// + public abstract ReadOnlyCollection Partitions { get; } + + /// + /// Determines if a disk is partitioned with a known partitioning scheme. + /// + /// The content of the disk to check. + /// true if the disk is partitioned, else false. + public static bool IsPartitioned(Stream content) + { + foreach (PartitionTableFactory partTableFactory in Factories) + { + if (partTableFactory.DetectIsPartitioned(content)) + { + return true; + } + } + + return false; + } + + /// + /// Determines if a disk is partitioned with a known partitioning scheme. + /// + /// The disk to check. + /// true if the disk is partitioned, else false. + public static bool IsPartitioned(VirtualDisk disk) + { + return IsPartitioned(disk.Content); + } + + /// + /// Gets all of the partition tables found on a disk. + /// + /// The disk to inspect. + /// It is rare for a disk to have multiple partition tables, but theoretically + /// possible. + public static IList GetPartitionTables(VirtualDisk disk) + { + List tables = new List(); + + foreach (PartitionTableFactory factory in Factories) + { + PartitionTable table = factory.DetectPartitionTable(disk); + if (table != null) + { + tables.Add(table); + } + } + + return tables; + } + + /// + /// Gets all of the partition tables found on a disk. + /// + /// The content of the disk to inspect. + /// It is rare for a disk to have multiple partition tables, but theoretically + /// possible. + public static IList GetPartitionTables(Stream contentStream) + { + return GetPartitionTables(new Disk(contentStream, Ownership.None)); + } + + /// + /// Creates a new partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + public abstract int Create(WellKnownPartitionType type, bool active); + + /// + /// Creates a new partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The index of the new partition. + public abstract int Create(long size, WellKnownPartitionType type, bool active); + + /// + /// Creates a new aligned partition that encompasses the entire disk. + /// + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in byte). + /// The index of the partition. + /// The partition table must be empty before this method is called, + /// otherwise IOException is thrown. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is acheived by aligning partitions on + /// large values that are a power of two. + /// + public abstract int CreateAligned(WellKnownPartitionType type, bool active, int alignment); + + /// + /// Creates a new aligned partition with a target size. + /// + /// The target size (in bytes). + /// The partition type. + /// Whether the partition is active (bootable). + /// The alignment (in byte). + /// The index of the new partition. + /// + /// Traditionally partitions were aligned to the physical structure of the underlying disk, + /// however with modern storage greater efficiency is achieved by aligning partitions on + /// large values that are a power of two. + /// + public abstract int CreateAligned(long size, WellKnownPartitionType type, bool active, int alignment); + + /// + /// Deletes a partition at a given index. + /// + /// The index of the partition. + public abstract void Delete(int index); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/PartitionTableFactory.cs b/DiscUtils/Core/Partitions/PartitionTableFactory.cs new file mode 100644 index 0000000..164b6a3 --- /dev/null +++ b/DiscUtils/Core/Partitions/PartitionTableFactory.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Partitions +{ + internal abstract class PartitionTableFactory + { + public abstract bool DetectIsPartitioned(Stream s); + + public abstract PartitionTable DetectPartitionTable(VirtualDisk disk); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/PartitionTableFactoryAttribute.cs b/DiscUtils/Core/Partitions/PartitionTableFactoryAttribute.cs new file mode 100644 index 0000000..e199c04 --- /dev/null +++ b/DiscUtils/Core/Partitions/PartitionTableFactoryAttribute.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Partitions +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + internal sealed class PartitionTableFactoryAttribute : Attribute {} +} \ No newline at end of file diff --git a/DiscUtils/Core/Partitions/WellKnownPartitionType.cs b/DiscUtils/Core/Partitions/WellKnownPartitionType.cs new file mode 100644 index 0000000..45a424e --- /dev/null +++ b/DiscUtils/Core/Partitions/WellKnownPartitionType.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Partitions +{ + /// + /// Enumeration of partition-table technology neutral partition types. + /// + public enum WellKnownPartitionType + { + /// + /// Windows FAT-based partition. + /// + WindowsFat = 0, + + /// + /// Windows NTFS-based partition. + /// + WindowsNtfs = 1, + + /// + /// Linux native file system. + /// + Linux = 2, + + /// + /// Linux swap. + /// + LinuxSwap = 3, + + /// + /// Linux Logical Volume Manager (LVM). + /// + LinuxLvm = 4 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/PhysicalVolumeInfo.cs b/DiscUtils/Core/PhysicalVolumeInfo.cs new file mode 100644 index 0000000..2b78fb0 --- /dev/null +++ b/DiscUtils/Core/PhysicalVolumeInfo.cs @@ -0,0 +1,202 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using DiscUtils.Partitions; +using DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Information about a physical disk volume, which may be a partition or an entire disk. + /// + public sealed class PhysicalVolumeInfo : VolumeInfo + { + private readonly VirtualDisk _disk; + private readonly string _diskId; + private readonly SparseStreamOpenDelegate _streamOpener; + + /// + /// Initializes a new instance of the PhysicalVolumeInfo class. + /// + /// The containing disk's identity. + /// The disk containing the partition. + /// Information about the partition. + /// Use this constructor to represent a (BIOS or GPT) partition. + internal PhysicalVolumeInfo( + string diskId, + VirtualDisk disk, + PartitionInfo partitionInfo) + { + _diskId = diskId; + _disk = disk; + _streamOpener = partitionInfo.Open; + VolumeType = partitionInfo.VolumeType; + Partition = partitionInfo; + } + + /// + /// Initializes a new instance of the PhysicalVolumeInfo class. + /// + /// The identity of the disk. + /// The disk itself. + /// Use this constructor to represent an entire disk as a single volume. + internal PhysicalVolumeInfo( + string diskId, + VirtualDisk disk) + { + _diskId = diskId; + _disk = disk; + _streamOpener = delegate { return new SubStream(disk.Content, Ownership.None, 0, disk.Capacity); }; + VolumeType = PhysicalVolumeType.EntireDisk; + } + + /// + /// Gets the disk geometry of the underlying storage medium (as used in BIOS calls), may be null. + /// + public override Geometry BiosGeometry + { + get { return _disk.BiosGeometry; } + } + + /// + /// Gets the one-byte BIOS type for this volume, which indicates the content. + /// + public override byte BiosType + { + get { return Partition == null ? (byte)0 : Partition.BiosType; } + } + + /// + /// Gets the unique identity of the disk containing the volume, if known. + /// + public Guid DiskIdentity + { + get { return VolumeType != PhysicalVolumeType.EntireDisk ? _disk.Partitions.DiskGuid : Guid.Empty; } + } + + /// + /// Gets the signature of the disk containing the volume (only valid for partition-type volumes). + /// + public int DiskSignature + { + get { return VolumeType != PhysicalVolumeType.EntireDisk ? _disk.Signature : 0; } + } + + /// + /// Gets the stable identity for this physical volume. + /// + /// The stability of the identity depends the disk structure. + /// In some cases the identity may include a simple index, when no other information + /// is available. Best practice is to add disks to the Volume Manager in a stable + /// order, if the stability of this identity is paramount. + public override string Identity + { + get + { + if (VolumeType == PhysicalVolumeType.GptPartition) + { + return "VPG" + PartitionIdentity.ToString("B"); + } + string partId; + switch (VolumeType) + { + case PhysicalVolumeType.EntireDisk: + partId = "PD"; + break; + case PhysicalVolumeType.BiosPartition: + case PhysicalVolumeType.ApplePartition: + partId = "PO" + + (Partition.FirstSector * _disk.SectorSize).ToString("X", + CultureInfo.InvariantCulture); + break; + default: + partId = "P*"; + break; + } + + return "VPD:" + _diskId + ":" + partId; + } + } + + /// + /// Gets the size of the volume, in bytes. + /// + public override long Length + { + get { return Partition == null ? _disk.Capacity : Partition.SectorCount * _disk.SectorSize; } + } + + /// + /// Gets the underlying partition (if any). + /// + public PartitionInfo Partition { get; } + + /// + /// Gets the unique identity of the physical partition, if known. + /// + public Guid PartitionIdentity + { + get + { + GuidPartitionInfo gpi = Partition as GuidPartitionInfo; + if (gpi != null) + { + return gpi.Identity; + } + + return Guid.Empty; + } + } + + /// + /// Gets the disk geometry of the underlying storage medium, if any (may be null). + /// + public override Geometry PhysicalGeometry + { + get { return _disk.Geometry; } + } + + /// + /// Gets the offset of this volume in the underlying storage medium, if any (may be Zero). + /// + public override long PhysicalStartSector + { + get { return VolumeType == PhysicalVolumeType.EntireDisk ? 0 : Partition.FirstSector; } + } + + /// + /// Gets the type of the volume. + /// + public PhysicalVolumeType VolumeType { get; } + + /// + /// Opens the volume, providing access to its contents. + /// + /// A stream that can be used to access the volume. + public override SparseStream Open() + { + return _streamOpener(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/PhysicalVolumeType.cs b/DiscUtils/Core/PhysicalVolumeType.cs new file mode 100644 index 0000000..791c9ce --- /dev/null +++ b/DiscUtils/Core/PhysicalVolumeType.cs @@ -0,0 +1,33 @@ +namespace DiscUtils +{ + /// + /// Enumeration of possible types of physical volume. + /// + public enum PhysicalVolumeType + { + /// + /// Unknown type. + /// + None, + + /// + /// Physical volume encompasses the entire disk. + /// + EntireDisk, + + /// + /// Physical volume is defined by a BIOS-style partition table. + /// + BiosPartition, + + /// + /// Physical volume is defined by a GUID partition table. + /// + GptPartition, + + /// + /// Physical volume is defined by an Apple partition map. + /// + ApplePartition + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Plist.cs b/DiscUtils/Core/Plist.cs new file mode 100644 index 0000000..11d9254 --- /dev/null +++ b/DiscUtils/Core/Plist.cs @@ -0,0 +1,208 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using System.Text; +using System.Xml; + +namespace DiscUtils +{ + internal static class Plist + { + internal static Dictionary Parse(Stream stream) + { + XmlDocument xmlDoc = new XmlDocument(); +#if !NETSTANDARD1_5 + xmlDoc.XmlResolver = null; +#endif + + XmlReaderSettings settings = new XmlReaderSettings(); +#if !NET20 + // DTD processing is disabled on anything but .NET 2.0, so this must be set to + // Ignore. + // See https://msdn.microsoft.com/en-us/magazine/ee335713.aspx for additional information. + settings.DtdProcessing = DtdProcessing.Ignore; +#endif + + using (XmlReader reader = XmlReader.Create(stream, settings)) + { + xmlDoc.Load(reader); + } + + XmlElement root = xmlDoc.DocumentElement; + if (root.Name != "plist") + { + throw new InvalidDataException("XML document is not a plist"); + } + + return ParseDictionary(root.FirstChild); + } + + internal static void Write(Stream stream, Dictionary plist) + { + XmlDocument xmlDoc = new XmlDocument(); +#if !NETSTANDARD1_5 + xmlDoc.XmlResolver = null; +#endif + + XmlDeclaration xmlDecl = xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", null); + xmlDoc.AppendChild(xmlDecl); + +#if !NETSTANDARD1_5 + XmlDocumentType xmlDocType = xmlDoc.CreateDocumentType("plist", "-//Apple//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd", null); + xmlDoc.AppendChild(xmlDocType); +#endif + + XmlElement rootElement = xmlDoc.CreateElement("plist"); + rootElement.SetAttribute("Version", "1.0"); + xmlDoc.AppendChild(rootElement); + + xmlDoc.DocumentElement.SetAttribute("Version", "1.0"); + + rootElement.AppendChild(CreateNode(xmlDoc, plist)); + + XmlWriterSettings settings = new XmlWriterSettings(); + settings.Indent = true; + settings.Encoding = Encoding.UTF8; + + using (XmlWriter xw = XmlWriter.Create(stream, settings)) + { + xmlDoc.Save(xw); + } + } + + private static object ParseNode(XmlNode xmlNode) + { + switch (xmlNode.Name) + { + case "dict": + return ParseDictionary(xmlNode); + case "array": + return ParseArray(xmlNode); + case "string": + return ParseString(xmlNode); + case "data": + return ParseData(xmlNode); + case "integer": + return ParseInteger(xmlNode); + case "true": + return true; + case "false": + return false; + default: + throw new NotImplementedException(); + } + } + + private static XmlNode CreateNode(XmlDocument xmlDoc, object obj) + { + if (obj is Dictionary) + { + return CreateDictionary(xmlDoc, (Dictionary)obj); + } + if (obj is string) + { + XmlText text = xmlDoc.CreateTextNode((string)obj); + XmlElement node = xmlDoc.CreateElement("string"); + node.AppendChild(text); + return node; + } + throw new NotImplementedException(); + } + + private static XmlNode CreateDictionary(XmlDocument xmlDoc, Dictionary dict) + { + XmlElement dictNode = xmlDoc.CreateElement("dict"); + + foreach (KeyValuePair entry in dict) + { + XmlText text = xmlDoc.CreateTextNode(entry.Key); + XmlElement keyNode = xmlDoc.CreateElement("key"); + keyNode.AppendChild(text); + + dictNode.AppendChild(keyNode); + + XmlNode valueNode = CreateNode(xmlDoc, entry.Value); + dictNode.AppendChild(valueNode); + } + + return dictNode; + } + + private static Dictionary ParseDictionary(XmlNode xmlNode) + { + Dictionary result = new Dictionary(); + + XmlNode focusNode = xmlNode.FirstChild; + while (focusNode != null) + { + if (focusNode.Name != "key") + { + throw new InvalidDataException("Invalid plist, expected dictionary key"); + } + + string key = focusNode.InnerText; + + focusNode = focusNode.NextSibling; + + result.Add(key, ParseNode(focusNode)); + + focusNode = focusNode.NextSibling; + } + + return result; + } + + private static object ParseArray(XmlNode xmlNode) + { + List result = new List(); + + XmlNode focusNode = xmlNode.FirstChild; + while (focusNode != null) + { + result.Add(ParseNode(focusNode)); + focusNode = focusNode.NextSibling; + } + + return result; + } + + private static object ParseString(XmlNode xmlNode) + { + return xmlNode.InnerText; + } + + private static object ParseData(XmlNode xmlNode) + { + string base64 = xmlNode.InnerText; + return Convert.FromBase64String(base64); + } + + private static object ParseInteger(XmlNode xmlNode) + { + return int.Parse(xmlNode.InnerText, CultureInfo.InvariantCulture); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Raw/Disk.cs b/DiscUtils/Core/Raw/Disk.cs new file mode 100644 index 0000000..beb6d60 --- /dev/null +++ b/DiscUtils/Core/Raw/Disk.cs @@ -0,0 +1,222 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils.Raw +{ + /// + /// Represents a raw disk image. + /// + /// This disk format is simply an uncompressed capture of all blocks on a disk. + public sealed class Disk : VirtualDisk + { + private DiskImageFile _file; + + /// + /// Initializes a new instance of the Disk class. + /// + /// The stream to read. + /// Indicates if the new instance should control the lifetime of the stream. + public Disk(Stream stream, Ownership ownsStream) + : this(stream, ownsStream, null) {} + + /// + /// Initializes a new instance of the Disk class. + /// + /// The stream to read. + /// Indicates if the new instance should control the lifetime of the stream. + /// The emulated geometry of the disk. + public Disk(Stream stream, Ownership ownsStream, Geometry geometry) + { + _file = new DiskImageFile(stream, ownsStream, geometry); + } + + /// + /// Initializes a new instance of the Disk class. + /// + /// The path to the disk image. + public Disk(string path) + :this(path, FileAccess.ReadWrite) {} + + /// + /// Initializes a new instance of the Disk class. + /// + /// The path to the disk image. + /// The access requested to the disk. + public Disk(string path, FileAccess access) + { + FileShare share = access == FileAccess.Read ? FileShare.Read : FileShare.None; + var locator = new LocalFileLocator(string.Empty); + _file = new DiskImageFile(locator.Open(path, FileMode.Open, access, share), Ownership.Dispose, null); + } + + /// + /// Initializes a new instance of the Disk class. + /// + /// The contents of the disk. + private Disk(DiskImageFile file) + { + _file = file; + } + + /// + /// Gets the capacity of the disk (in bytes). + /// + public override long Capacity + { + get { return _file.Capacity; } + } + + /// + /// Gets the content of the disk as a stream. + /// + /// Note the returned stream is not guaranteed to be at any particular position. The actual position + /// will depend on the last partition table/file system activity, since all access to the disk contents pass + /// through a single stream instance. Set the stream position before accessing the stream. + public override SparseStream Content + { + get { return _file.Content; } + } + + /// + /// Gets the type of disk represented by this object. + /// + public override VirtualDiskClass DiskClass + { + get { return _file.DiskType; } + } + + /// + /// Gets information about the type of disk. + /// + /// This property provides access to meta-data about the disk format, for example whether the + /// BIOS geometry is preserved in the disk file. + public override VirtualDiskTypeInfo DiskTypeInfo + { + get { return DiskFactory.MakeDiskTypeInfo(); } + } + + /// + /// Gets the geometry of the disk. + /// + public override Geometry Geometry + { + get { return _file.Geometry; } + } + + /// + /// Gets the layers that make up the disk. + /// + public override IEnumerable Layers + { + get { yield return _file; } + } + + /// + /// Initializes a stream as an unformatted disk. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The desired capacity of the new disk. + /// An object that accesses the stream as a disk. + public static Disk Initialize(Stream stream, Ownership ownsStream, long capacity) + { + return Initialize(stream, ownsStream, capacity, null); + } + + /// + /// Initializes a stream as an unformatted disk. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The desired capacity of the new disk. + /// The desired geometry of the new disk, or null for default. + /// An object that accesses the stream as a disk. + public static Disk Initialize(Stream stream, Ownership ownsStream, long capacity, Geometry geometry) + { + return new Disk(DiskImageFile.Initialize(stream, ownsStream, capacity, geometry)); + } + + /// + /// Initializes a stream as an unformatted floppy disk. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The type of floppy disk image to create. + /// An object that accesses the stream as a disk. + public static Disk Initialize(Stream stream, Ownership ownsStream, FloppyDiskType type) + { + return new Disk(DiskImageFile.Initialize(stream, ownsStream, type)); + } + + /// + /// Create a new differencing disk, possibly within an existing disk. + /// + /// The file system to create the disk on. + /// The path (or URI) for the disk to create. + /// The newly created disk. + public override VirtualDisk CreateDifferencingDisk(DiscFileSystem fileSystem, string path) + { + throw new NotSupportedException("Differencing disks not supported for raw disks"); + } + + /// + /// Create a new differencing disk. + /// + /// The path (or URI) for the disk to create. + /// The newly created disk. + public override VirtualDisk CreateDifferencingDisk(string path) + { + throw new NotSupportedException("Differencing disks not supported for raw disks"); + } + + /// + /// Disposes of underlying resources. + /// + /// Set to true if called within Dispose(), + /// else false. + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_file != null) + { + _file.Dispose(); + } + + _file = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Raw/DiskFactory.cs b/DiscUtils/Core/Raw/DiskFactory.cs new file mode 100644 index 0000000..3157ff0 --- /dev/null +++ b/DiscUtils/Core/Raw/DiskFactory.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils.Raw +{ + [VirtualDiskFactory("RAW", ".img,.ima,.vfd,.flp,.bif")] + internal sealed class DiskFactory : VirtualDiskFactory + { + public override string[] Variants + { + get { return new string[] { }; } + } + + public override VirtualDiskTypeInfo GetDiskTypeInformation(string variant) + { + return MakeDiskTypeInfo(); + } + + public override DiskImageBuilder GetImageBuilder(string variant) + { + throw new NotSupportedException(); + } + + public override VirtualDisk CreateDisk(FileLocator locator, string variant, string path, + VirtualDiskParameters diskParameters) + { + return Disk.Initialize(locator.Open(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None), + Ownership.Dispose, diskParameters.Capacity, diskParameters.Geometry); + } + + public override VirtualDisk OpenDisk(string path, FileAccess access) + { + return new Disk(path, access); + } + + public override VirtualDisk OpenDisk(FileLocator locator, string path, FileAccess access) + { + FileShare share = access == FileAccess.Read ? FileShare.Read : FileShare.None; + return new Disk(locator.Open(path, FileMode.Open, access, share), Ownership.Dispose); + } + + public override VirtualDiskLayer OpenDiskLayer(FileLocator locator, string path, FileAccess access) + { + return null; + } + + internal static VirtualDiskTypeInfo MakeDiskTypeInfo() + { + return new VirtualDiskTypeInfo + { + Name = "RAW", + Variant = string.Empty, + CanBeHardDisk = true, + DeterministicGeometry = true, + PreservesBiosGeometry = false, + CalcGeometry = c => Geometry.FromCapacity(c) + }; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Raw/DiskImageFile.cs b/DiscUtils/Core/Raw/DiskImageFile.cs new file mode 100644 index 0000000..221c310 --- /dev/null +++ b/DiscUtils/Core/Raw/DiskImageFile.cs @@ -0,0 +1,246 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Partitions; +using DiscUtils.Streams; + +namespace DiscUtils.Raw +{ + /// + /// Represents a single raw disk image file. + /// + public sealed class DiskImageFile : VirtualDiskLayer + { + private readonly Ownership _ownsContent; + + /// + /// Initializes a new instance of the DiskImageFile class. + /// + /// The stream to interpret. + public DiskImageFile(Stream stream) + : this(stream, Ownership.None, null) {} + + /// + /// Initializes a new instance of the DiskImageFile class. + /// + /// The stream to interpret. + /// Indicates if the new instance should control the lifetime of the stream. + /// The emulated geometry of the disk. + public DiskImageFile(Stream stream, Ownership ownsStream, Geometry geometry) + { + Content = stream as SparseStream; + _ownsContent = ownsStream; + + if (Content == null) + { + Content = SparseStream.FromStream(stream, ownsStream); + _ownsContent = Ownership.Dispose; + } + + Geometry = geometry ?? DetectGeometry(Content); + } + + internal override long Capacity + { + get { return Content.Length; } + } + + internal SparseStream Content { get; private set; } + + /// + /// Gets the type of disk represented by this object. + /// + public VirtualDiskClass DiskType + { + get { return DetectDiskType(Capacity); } + } + + /// + /// Gets the geometry of the file. + /// + public override Geometry Geometry { get; } + + /// + /// Gets a value indicating if the layer only stores meaningful sectors. + /// + public override bool IsSparse + { + get { return false; } + } + + /// + /// Gets a value indicating whether the file is a differencing disk. + /// + public override bool NeedsParent + { + get { return false; } + } + + internal override FileLocator RelativeFileLocator + { + get { return null; } + } + + /// + /// Initializes a stream as a raw disk image. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The desired capacity of the new disk. + /// The geometry of the new disk. + /// An object that accesses the stream as a raw disk image. + public static DiskImageFile Initialize(Stream stream, Ownership ownsStream, long capacity, Geometry geometry) + { + stream.SetLength(MathUtilities.RoundUp(capacity, Sizes.Sector)); + + // Wipe any pre-existing master boot record / BPB + stream.Position = 0; + stream.Write(new byte[Sizes.Sector], 0, Sizes.Sector); + stream.Position = 0; + + return new DiskImageFile(stream, ownsStream, geometry); + } + + /// + /// Initializes a stream as an unformatted floppy disk. + /// + /// The stream to initialize. + /// Indicates if the new instance controls the lifetime of the stream. + /// The type of floppy disk image to create. + /// An object that accesses the stream as a disk. + public static DiskImageFile Initialize(Stream stream, Ownership ownsStream, FloppyDiskType type) + { + return Initialize(stream, ownsStream, FloppyCapacity(type), null); + } + + /// + /// Gets the content of this layer. + /// + /// The parent stream (if any). + /// Controls ownership of the parent stream. + /// The content as a stream. + public override SparseStream OpenContent(SparseStream parent, Ownership ownsParent) + { + if (ownsParent == Ownership.Dispose && parent != null) + { + parent.Dispose(); + } + + return SparseStream.FromStream(Content, Ownership.None); + } + + /// + /// Gets the possible locations of the parent file (if any). + /// + /// Array of strings, empty if no parent. + public override string[] GetParentLocations() + { + return new string[0]; + } + + /// + /// Disposes of underlying resources. + /// + /// Set to true if called within Dispose(), + /// else false. + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_ownsContent == Ownership.Dispose && Content != null) + { + Content.Dispose(); + } + + Content = null; + } + } + finally + { + base.Dispose(disposing); + } + } + + /// + /// Calculates the best guess geometry of a disk. + /// + /// The disk to detect the geometry of. + /// The geometry of the disk. + private static Geometry DetectGeometry(Stream disk) + { + long capacity = disk.Length; + + // First, check for floppy disk capacities - these have well-defined geometries + if (capacity == Sizes.Sector * 1440) + { + return new Geometry(80, 2, 9); + } + if (capacity == Sizes.Sector * 2880) + { + return new Geometry(80, 2, 18); + } + if (capacity == Sizes.Sector * 5760) + { + return new Geometry(80, 2, 36); + } + + // Failing that, try to detect the geometry from any partition table. + // Note: this call falls back to guessing the geometry from the capacity + return BiosPartitionTable.DetectGeometry(disk); + } + + /// + /// Calculates the best guess disk type (i.e. floppy or hard disk). + /// + /// The capacity of the disk. + /// The disk type. + private static VirtualDiskClass DetectDiskType(long capacity) + { + if (capacity == Sizes.Sector * 1440 + || capacity == Sizes.Sector * 2880 + || capacity == Sizes.Sector * 5760) + { + return VirtualDiskClass.FloppyDisk; + } + return VirtualDiskClass.HardDisk; + } + + private static long FloppyCapacity(FloppyDiskType type) + { + switch (type) + { + case FloppyDiskType.DoubleDensity: + return Sizes.Sector * 1440; + case FloppyDiskType.HighDensity: + return Sizes.Sector * 2880; + case FloppyDiskType.Extended: + return Sizes.Sector * 5760; + default: + throw new ArgumentException("Invalid floppy disk type", nameof(type)); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/ReadOnlyDiscFileSystem.cs b/DiscUtils/Core/ReadOnlyDiscFileSystem.cs new file mode 100644 index 0000000..ea6a7c0 --- /dev/null +++ b/DiscUtils/Core/ReadOnlyDiscFileSystem.cs @@ -0,0 +1,166 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Base class for file systems that are by their nature read-only, causes NotSupportedException to be thrown + /// from all methods that are always invalid. + /// + public abstract class ReadOnlyDiscFileSystem : DiscFileSystem + { + /// + /// Initializes a new instance of the ReadOnlyDiscFileSystem class. + /// + protected ReadOnlyDiscFileSystem() {} + + /// + /// Initializes a new instance of the ReadOnlyDiscFileSystem class. + /// + /// The options instance to use for this file system instance. + protected ReadOnlyDiscFileSystem(DiscFileSystemOptions defaultOptions) + : base(defaultOptions) {} + + /// + /// Indicates whether the file system is read-only or read-write. + /// + /// Always false. + public override bool CanWrite + { + get { return false; } + } + + /// + /// Copies a file - not supported on read-only file systems. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + throw new NotSupportedException(); + } + + /// + /// Creates a directory - not supported on read-only file systems. + /// + /// The path of the new directory. + public override void CreateDirectory(string path) + { + throw new NotSupportedException(); + } + + /// + /// Deletes a directory - not supported on read-only file systems. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + throw new NotSupportedException(); + } + + /// + /// Deletes a file - not supported on read-only file systems. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + throw new NotSupportedException(); + } + + /// + /// Moves a directory - not supported on read-only file systems. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + throw new NotSupportedException(); + } + + /// + /// Moves a file - not supported on read-only file systems. + /// + /// The file to move. + /// The target file name. + /// Whether to allow an existing file to be overwritten. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + throw new NotSupportedException(); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode) + { + return OpenFile(path, mode, FileAccess.Read); + } + + /// + /// Sets the attributes of a file or directory - not supported on read-only file systems. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + throw new NotSupportedException(); + } + + /// + /// Sets the creation time (in UTC) of a file or directory - not supported on read-only file systems. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + throw new NotSupportedException(); + } + + /// + /// Sets the last access time (in UTC) of a file or directory - not supported on read-only file systems. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + throw new NotSupportedException(); + } + + /// + /// Sets the last modification time (in UTC) of a file or directory - not supported on read-only file systems. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/ReparsePoint.cs b/DiscUtils/Core/ReparsePoint.cs new file mode 100644 index 0000000..02161d3 --- /dev/null +++ b/DiscUtils/Core/ReparsePoint.cs @@ -0,0 +1,51 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// Represents a Reparse Point, which can be associated with a file or directory. + /// + public sealed class ReparsePoint + { + /// + /// Initializes a new instance of the ReparsePoint class. + /// + /// The defined reparse point tag. + /// The reparse point's content. + public ReparsePoint(int tag, byte[] content) + { + Tag = tag; + Content = content; + } + + /// + /// Gets or sets the reparse point's content. + /// + public byte[] Content { get; set; } + + /// + /// Gets or sets the defined reparse point tag. + /// + public int Tag { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/ReportLevels.cs b/DiscUtils/Core/ReportLevels.cs new file mode 100644 index 0000000..7973866 --- /dev/null +++ b/DiscUtils/Core/ReportLevels.cs @@ -0,0 +1,36 @@ +using System; + +namespace DiscUtils +{ + /// + /// Flags for the amount of detail to include in a report. + /// + [Flags] + public enum ReportLevels + { + /// + /// Report no information. + /// + None = 0x00, + + /// + /// Report informational level items. + /// + Information = 0x01, + + /// + /// Report warning level items. + /// + Warnings = 0x02, + + /// + /// Report error level items. + /// + Errors = 0x04, + + /// + /// Report all items. + /// + All = 0x07 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Setup/FileOpenEventArgs.cs b/DiscUtils/Core/Setup/FileOpenEventArgs.cs new file mode 100644 index 0000000..79c4c0d --- /dev/null +++ b/DiscUtils/Core/Setup/FileOpenEventArgs.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) 2017, Bianco Veigel +// +// 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; + +namespace DiscUtils.Setup +{ + internal delegate Stream FileOpenDelegate(string fileName, FileMode mode, FileAccess access, FileShare share); + + /// + /// Event arguments for opening a file + /// + public class FileOpenEventArgs:EventArgs + { + private FileOpenDelegate _opener; + + internal FileOpenEventArgs(string fileName, FileMode mode, FileAccess access, FileShare share, FileOpenDelegate opener) + { + FileName = fileName; + FileMode = mode; + FileAccess = access; + FileShare = share; + _opener = opener; + } + + /// + /// Gets or sets the filename to open + /// + public string FileName { get; set; } + + /// + /// Gets or sets the + /// + public FileMode FileMode { get; set; } + + /// + /// Gets or sets the + /// + public FileAccess FileAccess { get; set; } + + /// + /// Gets or sets the + /// + public FileShare FileShare { get; set; } + + /// + /// The resulting stream. + /// + /// + /// If this is set to a non null value, this stream is used instead of opening the supplied + /// + public Stream Result { get; set; } + + /// + /// returns the result from the builtin FileLocator + /// + /// + public Stream GetFileStream() + { + return _opener(FileName, FileMode, FileAccess, FileShare); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Setup/SetupHelper.cs b/DiscUtils/Core/Setup/SetupHelper.cs new file mode 100644 index 0000000..15e6abe --- /dev/null +++ b/DiscUtils/Core/Setup/SetupHelper.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using DiscUtils.CoreCompat; + +namespace DiscUtils.Setup +{ + /// + /// Helps setup new DiscUtils dependencies, when loaded into target programs + /// + public static class SetupHelper + { + private static readonly HashSet _alreadyLoaded; + + static SetupHelper() + { + _alreadyLoaded = new HashSet(); + + // Register the core DiscUtils lib + RegisterAssembly(ReflectionHelper.GetAssembly(typeof(SetupHelper))); + } + + /// + /// Registers the types provided by an assembly to all relevant DiscUtils managers + /// + /// + public static void RegisterAssembly(Assembly assembly) + { + lock (_alreadyLoaded) + { + if (!_alreadyLoaded.Add(assembly.FullName)) + return; + + FileSystemManager.RegisterFileSystems(assembly); + VirtualDiskManager.RegisterVirtualDiskTypes(assembly); + VolumeManager.RegisterLogicalVolumeFactory(assembly); + } + } + + /// + /// Allows intercepting any file open operation + /// + /// + /// Can be used to wrap the opened file for special use cases, + /// modify the parameters for opening files, validate file names + /// and many more. + /// + public static event EventHandler OpeningFile; + + internal static void OnOpeningFile(object sender, FileOpenEventArgs e) + { + OpeningFile?.Invoke(sender, e); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/System/DateTimeOffsetExtensions.cs b/DiscUtils/Core/System/DateTimeOffsetExtensions.cs new file mode 100644 index 0000000..3e9f2ea --- /dev/null +++ b/DiscUtils/Core/System/DateTimeOffsetExtensions.cs @@ -0,0 +1,42 @@ +namespace System +{ + /// + /// DateTimeOffset extension methods. + /// + public static class DateTimeOffsetExtensions + { + /// + /// The Epoch common to most (all?) Unix systems. + /// + public static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Converts the current Unix time to a DateTimeOffset. + /// + /// Seconds since UnixEpoch. + /// DateTimeOffset. + public static DateTimeOffset FromUnixTimeSeconds(this long seconds) + { +#if NETSTANDARD + return DateTimeOffset.FromUnixTimeSeconds(seconds); +#else + DateTimeOffset dateTimeOffset = new DateTimeOffset(DateTimeOffsetExtensions.UnixEpoch); + dateTimeOffset = dateTimeOffset.AddSeconds(seconds); + return dateTimeOffset; +#endif + } + +#if !NETSTANDARD1_5 + /// + /// Converts the current DateTimeOffset to Unix time. + /// + /// DateTimeOffset. + /// Seconds since UnixEpoch. + public static long ToUnixTimeSeconds(this DateTimeOffset dateTimeOffset) + { + long unixTimeStampInTicks = (dateTimeOffset.ToUniversalTime() - DateTimeOffsetExtensions.UnixEpoch).Ticks; + return unixTimeStampInTicks / TimeSpan.TicksPerSecond; + } +#endif + } +} \ No newline at end of file diff --git a/DiscUtils/Core/System/ExtensionAttribute.cs b/DiscUtils/Core/System/ExtensionAttribute.cs new file mode 100644 index 0000000..6c25fdd --- /dev/null +++ b/DiscUtils/Core/System/ExtensionAttribute.cs @@ -0,0 +1,11 @@ +#if NET20 +namespace System.Runtime.CompilerServices +{ + /// + /// Indicates that a method is an extension method, or that a class or assembly contains + /// extension methods. + /// + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] + public sealed class ExtensionAttribute : Attribute { } +} +#endif \ No newline at end of file diff --git a/DiscUtils/Core/System/HashSet.cs b/DiscUtils/Core/System/HashSet.cs new file mode 100644 index 0000000..b070d50 --- /dev/null +++ b/DiscUtils/Core/System/HashSet.cs @@ -0,0 +1,125 @@ +#if NET20 +using System.Collections.Generic; + +namespace System.Collections.Generic +{ + /// + /// Represents a set of values. + /// + /// The type of elements in the HashSet. + public class HashSet : ICollection + { + private Dictionary _innerDictionary; + + /// + /// Initializes a new instance of a HashSet + /// that is empty and uses the default equality comparer for the set type. + /// + public HashSet() + { + _innerDictionary = new Dictionary(); + } + + /// + /// Adds the specified element to a HashSet. + /// + /// The element to add to the set. + void ICollection.Add(T item) + { + AddInternal(item); + } + + private void AddInternal(T item) + { + _innerDictionary.Add(item, false); + } + + /// + /// Adds the specified element to a HashSet. + /// + /// The element to add to the set. + /// true if the element is added to the HashSet object; + /// false if the element is already present. + public bool Add(T item) + { + if (_innerDictionary.ContainsKey(item)) + return false; + + AddInternal(item); + return true; + } + + /// + /// Removes all elements from a HashSet. + /// + public void Clear() + { + _innerDictionary.Clear(); + _innerDictionary = new Dictionary(); + } + + /// + /// Determines whether a HashSet contains the specified element. + /// + /// The element to locate in the HashSet. + /// true if the HashSet contains the specified element; otherwise, false. + public bool Contains(T item) + { + return _innerDictionary.ContainsKey(item); + } + + /// + /// Copies the elements of a HashSet to an array, starting at the specified array index. + /// + /// The one-dimensional array that is the destination of the elements copied from + /// the HashSet. The array must have zero-based indexing + /// The zero-based index in array at which copying begins. + public void CopyTo(T[] array, int arrayIndex) + { + _innerDictionary.Keys.CopyTo(array, arrayIndex); + } + + /// + /// Gets the number of elements that are contained in a HashSet. + /// + public int Count + { + get { return _innerDictionary.Keys.Count; } + } + + /// + /// Gets a value indicating whether the HashSet is read-only. + /// This property is always false. + /// + public bool IsReadOnly + { + get { return false; } + } + + /// + /// Removes the specified element from a HashSet. + /// + /// The element to remove. + /// true if the element is successfully found and removed; otherwise, false. This + /// method returns false if item is not found in the HashSet + public bool Remove(T item) + { + return _innerDictionary.Remove(item); + } + + /// + /// Returns an enumerator that iterates through a HashSet. + /// + /// A HashSet.Enumerator object for the HashSet. + public IEnumerator GetEnumerator() + { + return _innerDictionary.Keys.GetEnumerator(); + } + + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} +#endif \ No newline at end of file diff --git a/DiscUtils/Core/System/Tuple.cs b/DiscUtils/Core/System/Tuple.cs new file mode 100644 index 0000000..e1cf270 --- /dev/null +++ b/DiscUtils/Core/System/Tuple.cs @@ -0,0 +1,116 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + + +#if NET20 + +// ReSharper disable once CheckNamespace +namespace System +{ + internal class Tuple + { + public A Item1 { get; } + public B Item2 { get; } + + public Tuple(A item1, B item2) + { + Item1 = item1; + Item2 = item2; + } + + protected static bool Equals(V a, V b) + { + if (a == null && b == null) + return true; + + if (a == null) + return false; + + return a.Equals(b); + } + + public override bool Equals(object obj) + { + Tuple asType = obj as Tuple; + if (asType == null) + { + return false; + } + + return Equals(Item1, asType.Item1) && Equals(Item2, asType.Item2); + } + + public override int GetHashCode() + { + return ((Item1 == null) ? 0x14AB32BC : Item1.GetHashCode()) ^ ((Item2 == null) ? 0x65BC32DE : Item2.GetHashCode()); + } + } +} +#endif + +#if NET20 + +// ReSharper disable once CheckNamespace +namespace System +{ + internal class Tuple + { + public A Item1 { get; } + public B Item2 { get; } + public C Item3 { get; } + + public Tuple(A item1, B item2, C item3) + { + Item1 = item1; + Item2 = item2; + Item3 = item3; + } + + protected static bool Equals(V a, V b) + { + if (a == null && b == null) + return true; + + if (a == null) + return false; + + return a.Equals(b); + } + + public override bool Equals(object obj) + { + Tuple asType = obj as Tuple; + if (asType == null) + { + return false; + } + + return Equals(Item1, asType.Item1) && Equals(Item2, asType.Item2) && Equals(Item3, asType.Item3); + } + + public override int GetHashCode() + { + return ((Item1 == null) ? 0x14AB32BC : Item1.GetHashCode()) ^ ((Item2 == null) ? 0x65BC32DE : Item2.GetHashCode()) ^ ((Item3 == null) ? 0x2D4C25CF : Item3.GetHashCode()); + } + } +} +#endif \ No newline at end of file diff --git a/DiscUtils/Core/TimeConverter.cs b/DiscUtils/Core/TimeConverter.cs new file mode 100644 index 0000000..1e8777d --- /dev/null +++ b/DiscUtils/Core/TimeConverter.cs @@ -0,0 +1,12 @@ +using System; + +namespace DiscUtils +{ + /// + /// Converts a time to/from UTC. + /// + /// The time to convert. + /// true to convert FAT time to UTC, false to convert UTC to FAT time. + /// The converted time. + public delegate DateTime TimeConverter(DateTime time, bool toUtc); +} \ No newline at end of file diff --git a/DiscUtils/Core/UnixFilePermissions.cs b/DiscUtils/Core/UnixFilePermissions.cs new file mode 100644 index 0000000..d1088a7 --- /dev/null +++ b/DiscUtils/Core/UnixFilePermissions.cs @@ -0,0 +1,113 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils +{ + /// + /// Standard Unix-style file system permissions. + /// + [Flags] + public enum UnixFilePermissions + { + /// + /// No permissions. + /// + None = 0, + + /// + /// Any user execute permission. + /// + OthersExecute = 0x001, + + /// + /// Any user write permission. + /// + OthersWrite = 0x002, + + /// + /// Any user read permission. + /// + OthersRead = 0x004, + + /// + /// Any user all permissions. + /// + OthersAll = OthersExecute | OthersWrite | OthersRead, + + /// + /// Group execute permission. + /// + GroupExecute = 0x008, + + /// + /// Group write permission. + /// + GroupWrite = 0x010, + + /// + /// Group read permission. + /// + GroupRead = 0x020, + + /// + /// Group all permissions. + /// + GroupAll = GroupExecute | GroupWrite | GroupRead, + + /// + /// Owner execute permission. + /// + OwnerExecute = 0x040, + + /// + /// Owner write permission. + /// + OwnerWrite = 0x080, + + /// + /// Owner read permission. + /// + OwnerRead = 0x100, + + /// + /// Owner all permissions. + /// + OwnerAll = OwnerExecute | OwnerWrite | OwnerRead, + + /// + /// Sticky bit (meaning ill-defined). + /// + Sticky = 0x200, + + /// + /// Set GUID on execute. + /// + SetGroupId = 0x400, + + /// + /// Set UID on execute. + /// + SetUserId = 0x800 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/UnixFileSystemInfo.cs b/DiscUtils/Core/UnixFileSystemInfo.cs new file mode 100644 index 0000000..30b9135 --- /dev/null +++ b/DiscUtils/Core/UnixFileSystemInfo.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// Information about a file or directory common to most Unix systems. + /// + public sealed class UnixFileSystemInfo + { + /// + /// Gets or sets the device id of the referenced device (for character and block devices). + /// + public long DeviceId { get; set; } + + /// + /// Gets or sets the file's type. + /// + public UnixFileType FileType { get; set; } + + /// + /// Gets or sets the group that owns this file or directory. + /// + public int GroupId { get; set; } + + /// + /// Gets or sets the file's serial number (unique within file system). + /// + public long Inode { get; set; } + + /// + /// Gets or sets the number of hard links to this file. + /// + public int LinkCount { get; set; } + + /// + /// Gets or sets the file permissions (aka flags) for this file or directory. + /// + public UnixFilePermissions Permissions { get; set; } + + /// + /// Gets or sets the user that owns this file or directory. + /// + public int UserId { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/UnixFileType.cs b/DiscUtils/Core/UnixFileType.cs new file mode 100644 index 0000000..886c21e --- /dev/null +++ b/DiscUtils/Core/UnixFileType.cs @@ -0,0 +1,70 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// Standard Unix-style file type. + /// + public enum UnixFileType + { + /// + /// No type specified. + /// + None = 0, + + /// + /// A FIFO / Named Pipe. + /// + Fifo = 0x1, + + /// + /// A character device. + /// + Character = 0x2, + + /// + /// A normal directory. + /// + Directory = 0x4, + + /// + /// A block device. + /// + Block = 0x6, + + /// + /// A regular file. + /// + Regular = 0x8, + + /// + /// A soft link. + /// + Link = 0xA, + + /// + /// A unix socket. + /// + Socket = 0xC + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/IVfsDirectory.cs b/DiscUtils/Core/Vfs/IVfsDirectory.cs new file mode 100644 index 0000000..138efcd --- /dev/null +++ b/DiscUtils/Core/Vfs/IVfsDirectory.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; + +namespace DiscUtils.Vfs +{ + /// + /// Interface implemented by classes representing a directory. + /// + /// Concrete type representing directory entries. + /// Concrete type representing files. + public interface IVfsDirectory : IVfsFile + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + { + /// + /// Gets all of the directory entries. + /// + ICollection AllEntries { get; } + + /// + /// Gets a self-reference, if available. + /// + TDirEntry Self { get; } + + /// + /// Gets a specific directory entry, by name. + /// + /// The name of the directory entry. + /// The directory entry, or null if not found. + TDirEntry GetEntryByName(string name); + + /// + /// Creates a new file. + /// + /// The name of the file (relative to this directory). + /// The newly created file. + TDirEntry CreateNewFile(string name); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/IVfsFile.cs b/DiscUtils/Core/Vfs/IVfsFile.cs new file mode 100644 index 0000000..0e71e25 --- /dev/null +++ b/DiscUtils/Core/Vfs/IVfsFile.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Vfs +{ + /// + /// Interface implemented by a class representing a file. + /// + /// + /// File system implementations should have a class that implements this + /// interface. If the file system implementation is read-only, it is + /// acceptable to throw NotImplementedException from setters. + /// + public interface IVfsFile + { + /// + /// Gets or sets the last creation time in UTC. + /// + DateTime CreationTimeUtc { get; set; } + + /// + /// Gets or sets the file's attributes. + /// + FileAttributes FileAttributes { get; set; } + + /// + /// Gets a buffer to access the file's contents. + /// + IBuffer FileContent { get; } + + /// + /// Gets the length of the file. + /// + long FileLength { get; } + + /// + /// Gets or sets the last access time in UTC. + /// + DateTime LastAccessTimeUtc { get; set; } + + /// + /// Gets or sets the last write time in UTC. + /// + DateTime LastWriteTimeUtc { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/IVfsFileWithStreams.cs b/DiscUtils/Core/Vfs/IVfsFileWithStreams.cs new file mode 100644 index 0000000..e29249b --- /dev/null +++ b/DiscUtils/Core/Vfs/IVfsFileWithStreams.cs @@ -0,0 +1,26 @@ +using DiscUtils.Streams; + +namespace DiscUtils.Vfs +{ + /// + /// Interface implemented by classes representing files, in file systems that support multi-stream files. + /// + public interface IVfsFileWithStreams : IVfsFile + { + /// + /// Creates a new stream. + /// + /// The name of the stream. + /// An object representing the stream. + SparseStream CreateStream(string name); + + /// + /// Opens an existing stream. + /// + /// The name of the stream. + /// An object representing the stream. + /// The implementation must not implicitly create the stream if it doesn't already + /// exist. + SparseStream OpenExistingStream(string name); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/IVfsSymlink.cs b/DiscUtils/Core/Vfs/IVfsSymlink.cs new file mode 100644 index 0000000..94952c8 --- /dev/null +++ b/DiscUtils/Core/Vfs/IVfsSymlink.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Vfs +{ + /// + /// Interface implemented by classes representing a directory. + /// + /// Concrete type representing directory entries. + /// Concrete type representing files. + public interface IVfsSymlink : IVfsFile + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + { + /// + /// Gets the target path for this symlink. + /// + string TargetPath { get; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/VfsContext.cs b/DiscUtils/Core/Vfs/VfsContext.cs new file mode 100644 index 0000000..90cfc12 --- /dev/null +++ b/DiscUtils/Core/Vfs/VfsContext.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Vfs +{ + /// + /// Base class for a context object that holds global state for file system implementations. + /// + public abstract class VfsContext {} +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/VfsDirEntry.cs b/DiscUtils/Core/Vfs/VfsDirEntry.cs new file mode 100644 index 0000000..5536bf2 --- /dev/null +++ b/DiscUtils/Core/Vfs/VfsDirEntry.cs @@ -0,0 +1,128 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Vfs +{ + /// + /// Base class for directory entries in a file system. + /// + /// + /// File system implementations should have a class that derives from + /// this abstract class. If the file system implementation is read-only, + /// it is acceptable to throw NotImplementedException from methods + /// that attempt to modify the file system. + /// + public abstract class VfsDirEntry + { + /// + /// Gets the creation time of the file or directory. + /// + /// + /// May throw NotSupportedException if HasVfsTimeInfo is false. + /// + public abstract DateTime CreationTimeUtc { get; } + + /// + /// Gets the file attributes from the directory entry. + /// + /// + /// May throw NotSupportedException if HasVfsFileAttributes is false. + /// + public abstract FileAttributes FileAttributes { get; } + + /// + /// Gets the name of this directory entry. + /// + public abstract string FileName { get; } + + /// + /// Gets a value indicating whether this directory entry contains file attribute information. + /// + /// + /// Typically either always returns true or false. + /// + public abstract bool HasVfsFileAttributes { get; } + + /// + /// Gets a value indicating whether this directory entry contains time information. + /// + /// + /// Typically either always returns true or false. + /// + public abstract bool HasVfsTimeInfo { get; } + + /// + /// Gets a value indicating whether this directory entry represents a directory (rather than a file). + /// + public abstract bool IsDirectory { get; } + + /// + /// Gets a value indicating whether this directory entry represents a symlink (rather than a file or directory). + /// + public abstract bool IsSymlink { get; } + + /// + /// Gets the last access time of the file or directory. + /// + /// + /// May throw NotSupportedException if HasVfsTimeInfo is false. + /// + public abstract DateTime LastAccessTimeUtc { get; } + + /// + /// Gets the last write time of the file or directory. + /// + /// + /// May throw NotSupportedException if HasVfsTimeInfo is false. + /// + public abstract DateTime LastWriteTimeUtc { get; } + + /// + /// Gets a version of FileName that can be used in wildcard matches. + /// + /// + /// The returned name, must have an extension separator '.', and not have any optional version + /// information found in some files. The returned name is matched against a wildcard patterns + /// such as "*.*". + /// + public virtual string SearchName + { + get + { + string fileName = FileName; + if (fileName.IndexOf('.') == -1) + { + return fileName + "."; + } + return fileName; + } + } + + /// + /// Gets a unique id for the file or directory represented by this directory entry. + /// + public abstract long UniqueCacheId { get; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/VfsFileSystem.cs b/DiscUtils/Core/Vfs/VfsFileSystem.cs new file mode 100644 index 0000000..e387887 --- /dev/null +++ b/DiscUtils/Core/Vfs/VfsFileSystem.cs @@ -0,0 +1,749 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using System.Text.RegularExpressions; +using DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils.Vfs +{ + /// + /// Base class for VFS file systems. + /// + /// The concrete type representing directory entries. + /// The concrete type representing files. + /// The concrete type representing directories. + /// The concrete type holding global state. + public abstract class VfsFileSystem : DiscFileSystem + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + where TDirectory : class, IVfsDirectory, TFile + where TContext : VfsContext + { + private readonly ObjectCache _fileCache; + + /// + /// Initializes a new instance of the VfsFileSystem class. + /// + /// The default file system options. + protected VfsFileSystem(DiscFileSystemOptions defaultOptions) + : base(defaultOptions) + { + _fileCache = new ObjectCache(); + } + + /// + /// Gets or sets the global shared state. + /// + protected TContext Context { get; set; } + + /// + /// Gets or sets the object representing the root directory. + /// + protected TDirectory RootDirectory { get; set; } + + /// + /// Gets the volume label. + /// + public abstract override string VolumeLabel { get; } + + /// + /// Copies a file - not supported on read-only file systems. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + throw new NotImplementedException(); + } + + /// + /// Creates a directory - not supported on read-only file systems. + /// + /// The path of the new directory. + public override void CreateDirectory(string path) + { + throw new NotImplementedException(); + } + + /// + /// Deletes a directory - not supported on read-only file systems. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + throw new NotImplementedException(); + } + + /// + /// Deletes a file - not supported on read-only file systems. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + throw new NotImplementedException(); + } + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + public override bool DirectoryExists(string path) + { + if (IsRoot(path)) + { + return true; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + + if (dirEntry != null) + { + return dirEntry.IsDirectory; + } + + return false; + } + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + public override bool FileExists(string path) + { + TDirEntry dirEntry = GetDirectoryEntry(path); + + if (dirEntry != null) + { + return !dirEntry.IsDirectory; + } + + return false; + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern, SearchOption searchOption) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + List dirs = new List(); + DoSearch(dirs, path, re, searchOption == SearchOption.AllDirectories, true, false); + return dirs.ToArray(); + } + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern, SearchOption searchOption) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + List results = new List(); + DoSearch(results, path, re, searchOption == SearchOption.AllDirectories, false, true); + return results.ToArray(); + } + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path) + { + string fullPath = path; + if (!fullPath.StartsWith(@"\", StringComparison.OrdinalIgnoreCase)) + { + fullPath = @"\" + fullPath; + } + + TDirectory parentDir = GetDirectory(fullPath); + return Utilities.Map(parentDir.AllEntries, + m => Utilities.CombinePaths(fullPath, FormatFileName(m.FileName))); + } + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path, string searchPattern) + { + Regex re = Utilities.ConvertWildcardsToRegEx(searchPattern); + + TDirectory parentDir = GetDirectory(path); + + List result = new List(); + foreach (TDirEntry dirEntry in parentDir.AllEntries) + { + if (re.IsMatch(dirEntry.SearchName)) + { + result.Add(Utilities.CombinePaths(path, dirEntry.FileName)); + } + } + + return result.ToArray(); + } + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + throw new NotImplementedException(); + } + + /// + /// Moves a file. + /// + /// The file to move. + /// The target file name. + /// Overwrite any existing file. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + throw new NotImplementedException(); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode, FileAccess access) + { + if (!CanWrite) + { + if (mode != FileMode.Open) + { + throw new NotSupportedException("Only existing files can be opened"); + } + + if (access != FileAccess.Read) + { + throw new NotSupportedException("Files cannot be opened for write"); + } + } + + string fileName = Utilities.GetFileFromPath(path); + string attributeName = null; + + int streamSepPos = fileName.IndexOf(':'); + if (streamSepPos >= 0) + { + attributeName = fileName.Substring(streamSepPos + 1); + } + + string dirName; + try + { + dirName = Utilities.GetDirectoryFromPath(path); + } + catch (ArgumentException) + { + throw new IOException("Invalid path: " + path); + } + + string entryPath = Utilities.CombinePaths(dirName, fileName); + TDirEntry entry = GetDirectoryEntry(entryPath); + if (entry == null) + { + if (mode == FileMode.Open) + { + throw new FileNotFoundException("No such file", path); + } + TDirectory parentDir = GetDirectory(Utilities.GetDirectoryFromPath(path)); + entry = parentDir.CreateNewFile(Utilities.GetFileFromPath(path)); + } + else if (mode == FileMode.CreateNew) + { + throw new IOException("File already exists"); + } + + if (entry.IsSymlink) + { + entry = ResolveSymlink(entry, entryPath); + } + + if (entry.IsDirectory) + { + throw new IOException("Attempt to open directory as a file"); + } + TFile file = GetFile(entry); + + SparseStream stream = null; + if (string.IsNullOrEmpty(attributeName)) + { + stream = new BufferStream(file.FileContent, access); + } + else + { + IVfsFileWithStreams fileStreams = file as IVfsFileWithStreams; + if (fileStreams != null) + { + stream = fileStreams.OpenExistingStream(attributeName); + if (stream == null) + { + if (mode == FileMode.Create || mode == FileMode.OpenOrCreate) + { + stream = fileStreams.CreateStream(attributeName); + } + else + { + throw new FileNotFoundException("No such attribute on file", path); + } + } + } + else + { + throw new NotSupportedException( + "Attempt to open a file stream on a file system that doesn't support them"); + } + } + + if (mode == FileMode.Create || mode == FileMode.Truncate) + { + stream.SetLength(0); + } + + return stream; + } + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + public override FileAttributes GetAttributes(string path) + { + if (IsRoot(path)) + { + return RootDirectory.FileAttributes; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("File not found", path); + } + + if (dirEntry.HasVfsFileAttributes) + { + return dirEntry.FileAttributes; + } + return GetFile(dirEntry).FileAttributes; + } + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + throw new NotImplementedException(); + } + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTimeUtc(string path) + { + if (IsRoot(path)) + { + return RootDirectory.CreationTimeUtc; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("No such file or directory", path); + } + + if (dirEntry.HasVfsTimeInfo) + { + return dirEntry.CreationTimeUtc; + } + return GetFile(dirEntry).CreationTimeUtc; + } + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + throw new NotImplementedException(); + } + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public override DateTime GetLastAccessTimeUtc(string path) + { + if (IsRoot(path)) + { + return RootDirectory.LastAccessTimeUtc; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("No such file or directory", path); + } + + if (dirEntry.HasVfsTimeInfo) + { + return dirEntry.LastAccessTimeUtc; + } + return GetFile(dirEntry).LastAccessTimeUtc; + } + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + throw new NotImplementedException(); + } + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public override DateTime GetLastWriteTimeUtc(string path) + { + if (IsRoot(path)) + { + return RootDirectory.LastWriteTimeUtc; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("No such file or directory", path); + } + + if (dirEntry.HasVfsTimeInfo) + { + return dirEntry.LastWriteTimeUtc; + } + return GetFile(dirEntry).LastWriteTimeUtc; + } + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + throw new NotImplementedException(); + } + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + public override long GetFileLength(string path) + { + TFile file = GetFile(path); + if (file == null || (file.FileAttributes & FileAttributes.Directory) != 0) + { + throw new FileNotFoundException("No such file", path); + } + + return file.FileLength; + } + + public TFile GetFile(TDirEntry dirEntry) + { + long cacheKey = dirEntry.UniqueCacheId; + + TFile file = _fileCache[cacheKey]; + if (file == null) + { + file = ConvertDirEntryToFile(dirEntry); + _fileCache[cacheKey] = file; + } + + return file; + } + + public TDirectory GetDirectory(string path) + { + if (IsRoot(path)) + { + return RootDirectory; + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + + if (dirEntry != null && dirEntry.IsSymlink) + { + dirEntry = ResolveSymlink(dirEntry, path); + } + + if (dirEntry == null || !dirEntry.IsDirectory) + { + throw new DirectoryNotFoundException("No such directory: " + path); + } + + return (TDirectory)GetFile(dirEntry); + } + + public TDirEntry GetDirectoryEntry(string path) + { + return GetDirectoryEntry(RootDirectory, path); + } + + /// + /// Gets all directory entries in the specified directory and sub-directories. + /// + /// The path to inspect. + /// Delegate invoked for each directory entry. + protected void ForAllDirEntries(string path, DirEntryHandler handler) + { + TDirectory dir = null; + TDirEntry self = GetDirectoryEntry(path); + + if (self != null) + { + handler(path, self); + if (self.IsDirectory) + { + dir = GetFile(self) as TDirectory; + } + } + else + { + dir = GetFile(path) as TDirectory; + } + + if (dir != null) + { + foreach (TDirEntry subentry in dir.AllEntries) + { + ForAllDirEntries(Utilities.CombinePaths(path, subentry.FileName), handler); + } + } + } + + /// + /// Gets the file object for a given path. + /// + /// The path to query. + /// The file object corresponding to the path. + protected TFile GetFile(string path) + { + if (IsRoot(path)) + { + return RootDirectory; + } + if (path == null) + { + return default(TFile); + } + + TDirEntry dirEntry = GetDirectoryEntry(path); + if (dirEntry == null) + { + throw new FileNotFoundException("No such file or directory", path); + } + + return GetFile(dirEntry); + } + + /// + /// Converts a directory entry to an object representing a file. + /// + /// The directory entry to convert. + /// The corresponding file object. + protected abstract TFile ConvertDirEntryToFile(TDirEntry dirEntry); + + /// + /// Converts an public directory entry name into an external one. + /// + /// The name to convert. + /// The external name. + /// + /// This method is called on a single path element (i.e. name contains no path + /// separators). + /// + protected virtual string FormatFileName(string name) + { + return name; + } + + private static bool IsRoot(string path) + { + return string.IsNullOrEmpty(path) || path == @"\"; + } + + private TDirEntry GetDirectoryEntry(TDirectory dir, string path) + { + string[] pathElements = path.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + return GetDirectoryEntry(dir, pathElements, 0); + } + + private TDirEntry GetDirectoryEntry(TDirectory dir, string[] pathEntries, int pathOffset) + { + TDirEntry entry; + + if (pathEntries.Length == 0) + { + return dir.Self; + } + entry = dir.GetEntryByName(pathEntries[pathOffset]); + if (entry != null) + { + if (pathOffset == pathEntries.Length - 1) + { + return entry; + } + if (entry.IsDirectory) + { + return GetDirectoryEntry((TDirectory)ConvertDirEntryToFile(entry), pathEntries, pathOffset + 1); + } + throw new IOException(string.Format(CultureInfo.InvariantCulture, + "{0} is a file, not a directory", pathEntries[pathOffset])); + } + return null; + } + + private void DoSearch(List results, string path, Regex regex, bool subFolders, bool dirs, bool files) + { + TDirectory parentDir = GetDirectory(path); + if (parentDir == null) + { + throw new DirectoryNotFoundException(string.Format(CultureInfo.InvariantCulture, + "The directory '{0}' was not found", path)); + } + + string resultPrefixPath = path; + if (IsRoot(path)) + { + resultPrefixPath = @"\"; + } + + foreach (TDirEntry de in parentDir.AllEntries) + { + TDirEntry entry = de; + + if (entry.IsSymlink) + { + entry = ResolveSymlink(entry, path + "\\" + entry.FileName); + } + + bool isDir = entry.IsDirectory; + + if ((isDir && dirs) || (!isDir && files)) + { + if (regex.IsMatch(de.SearchName)) + { + results.Add(Utilities.CombinePaths(resultPrefixPath, FormatFileName(entry.FileName))); + } + } + + if (subFolders && isDir) + { + DoSearch(results, Utilities.CombinePaths(resultPrefixPath, FormatFileName(entry.FileName)), regex, + subFolders, dirs, files); + } + } + } + + private TDirEntry ResolveSymlink(TDirEntry entry, string path) + { + TDirEntry currentEntry = entry; + if (path.Length > 0 && path[0] != '\\') + { + path = '\\' + path; + } + string currentPath = path; + int resolvesLeft = 20; + while (currentEntry.IsSymlink && resolvesLeft > 0) + { + IVfsSymlink symlink = GetFile(currentEntry) as IVfsSymlink; + if (symlink == null) + { + throw new FileNotFoundException("Unable to resolve symlink", path); + } + + currentPath = Utilities.ResolvePath(currentPath.TrimEnd('\\'), symlink.TargetPath); + currentEntry = GetDirectoryEntry(currentPath); + if (currentEntry == null) + { + throw new FileNotFoundException("Unable to resolve symlink", path); + } + + --resolvesLeft; + } + + if (currentEntry.IsSymlink) + { + throw new FileNotFoundException("Unable to resolve symlink - too many links", path); + } + + return currentEntry; + } + + /// + /// Delegate for processing directory entries. + /// + /// Full path to the directory entry. + /// The directory entry itself. + protected delegate void DirEntryHandler(string path, TDirEntry dirEntry); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/VfsFileSystemFacade.cs b/DiscUtils/Core/Vfs/VfsFileSystemFacade.cs new file mode 100644 index 0000000..97887f8 --- /dev/null +++ b/DiscUtils/Core/Vfs/VfsFileSystemFacade.cs @@ -0,0 +1,567 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Vfs +{ + /// + /// Base class for the public facade on a file system. + /// + /// + /// The derived class can extend the functionality available from a file system + /// beyond that defined by DiscFileSystem. + /// + public abstract class VfsFileSystemFacade : DiscFileSystem + { + private readonly DiscFileSystem _wrapped; + + /// + /// Initializes a new instance of the VfsFileSystemFacade class. + /// + /// The actual file system instance. + protected VfsFileSystemFacade(DiscFileSystem toWrap) + { + _wrapped = toWrap; + } + + /// + /// Indicates whether the file system is read-only or read-write. + /// + /// true if the file system is read-write. + public override bool CanWrite + { + get { return _wrapped.CanWrite; } + } + + /// + /// Gets a friendly name for the file system. + /// + public override string FriendlyName + { + get { return _wrapped.FriendlyName; } + } + + /// + /// Gets a value indicating whether the file system is thread-safe. + /// + public override bool IsThreadSafe + { + get { return _wrapped.IsThreadSafe; } + } + + /// + /// Gets the file system options, which can be modified. + /// + public override DiscFileSystemOptions Options + { + get { return _wrapped.Options; } + } + + /// + /// Gets the root directory of the file system. + /// + public override DiscDirectoryInfo Root + { + get { return new DiscDirectoryInfo(this, string.Empty); } + } + + /// + /// Gets the volume label. + /// + public override string VolumeLabel + { + get { return _wrapped.VolumeLabel; } + } + + /// + /// Copies an existing file to a new file. + /// + /// The source file. + /// The destination file. + public override void CopyFile(string sourceFile, string destinationFile) + { + _wrapped.CopyFile(sourceFile, destinationFile); + } + + /// + /// Copies an existing file to a new file. + /// + /// The source file. + /// The destination file. + /// Overwrite any existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + _wrapped.CopyFile(sourceFile, destinationFile, overwrite); + } + + /// + /// Creates a directory. + /// + /// The path of the new directory. + public override void CreateDirectory(string path) + { + _wrapped.CreateDirectory(path); + } + + /// + /// Deletes a directory. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + _wrapped.DeleteDirectory(path); + } + + /// + /// Deletes a directory, optionally with all descendants. + /// + /// The path of the directory to delete. + /// Determines if the all descendants should be deleted. + public override void DeleteDirectory(string path, bool recursive) + { + _wrapped.DeleteDirectory(path, recursive); + } + + /// + /// Deletes a file. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + _wrapped.DeleteFile(path); + } + + /// + /// Indicates if a directory exists. + /// + /// The path to test. + /// true if the directory exists. + public override bool DirectoryExists(string path) + { + return _wrapped.DirectoryExists(path); + } + + /// + /// Indicates if a file exists. + /// + /// The path to test. + /// true if the file exists. + public override bool FileExists(string path) + { + return _wrapped.FileExists(path); + } + + /// + /// Indicates if a file or directory exists. + /// + /// The path to test. + /// true if the file or directory exists. + public override bool Exists(string path) + { + return _wrapped.Exists(path); + } + + /// + /// Gets the names of subdirectories in a specified directory. + /// + /// The path to search. + /// Array of directories. + public override string[] GetDirectories(string path) + { + return _wrapped.GetDirectories(path); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern) + { + return _wrapped.GetDirectories(path, searchPattern); + } + + /// + /// Gets the names of subdirectories in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of directories matching the search pattern. + public override string[] GetDirectories(string path, string searchPattern, SearchOption searchOption) + { + return _wrapped.GetDirectories(path, searchPattern, searchOption); + } + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// Array of files. + public override string[] GetFiles(string path) + { + return _wrapped.GetFiles(path); + } + + /// + /// Gets the names of files in a specified directory. + /// + /// The path to search. + /// The search string to match against. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern) + { + return _wrapped.GetFiles(path, searchPattern); + } + + /// + /// Gets the names of files in a specified directory matching a specified + /// search pattern, using a value to determine whether to search subdirectories. + /// + /// The path to search. + /// The search string to match against. + /// Indicates whether to search subdirectories. + /// Array of files matching the search pattern. + public override string[] GetFiles(string path, string searchPattern, SearchOption searchOption) + { + return _wrapped.GetFiles(path, searchPattern, searchOption); + } + + /// + /// Gets the names of all files and subdirectories in a specified directory. + /// + /// The path to search. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path) + { + return _wrapped.GetFileSystemEntries(path); + } + + /// + /// Gets the names of files and subdirectories in a specified directory matching a specified + /// search pattern. + /// + /// The path to search. + /// The search string to match against. + /// Array of files and subdirectories matching the search pattern. + public override string[] GetFileSystemEntries(string path, string searchPattern) + { + return _wrapped.GetFileSystemEntries(path, searchPattern); + } + + /// + /// Moves a directory. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + _wrapped.MoveDirectory(sourceDirectoryName, destinationDirectoryName); + } + + /// + /// Moves a file. + /// + /// The file to move. + /// The target file name. + public override void MoveFile(string sourceName, string destinationName) + { + _wrapped.MoveFile(sourceName, destinationName); + } + + /// + /// Moves a file, allowing an existing file to be overwritten. + /// + /// The file to move. + /// The target file name. + /// Whether to permit a destination file to be overwritten. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + _wrapped.MoveFile(sourceName, destinationName, overwrite); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode) + { + return _wrapped.OpenFile(path, mode); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The access permissions for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode, FileAccess access) + { + return _wrapped.OpenFile(path, mode, access); + } + + /// + /// Gets the attributes of a file or directory. + /// + /// The file or directory to inspect. + /// The attributes of the file or directory. + public override FileAttributes GetAttributes(string path) + { + return _wrapped.GetAttributes(path); + } + + /// + /// Sets the attributes of a file or directory. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + _wrapped.SetAttributes(path, newValue); + } + + /// + /// Gets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTime(string path) + { + return _wrapped.GetCreationTime(path); + } + + /// + /// Sets the creation time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTime(string path, DateTime newTime) + { + _wrapped.SetCreationTime(path, newTime); + } + + /// + /// Gets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The creation time. + public override DateTime GetCreationTimeUtc(string path) + { + return _wrapped.GetCreationTimeUtc(path); + } + + /// + /// Sets the creation time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + _wrapped.SetCreationTimeUtc(path, newTime); + } + + /// + /// Gets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public override DateTime GetLastAccessTime(string path) + { + return _wrapped.GetLastAccessTime(path); + } + + /// + /// Sets the last access time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTime(string path, DateTime newTime) + { + _wrapped.SetLastAccessTime(path, newTime); + } + + /// + /// Gets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last access time. + public override DateTime GetLastAccessTimeUtc(string path) + { + return _wrapped.GetLastAccessTimeUtc(path); + } + + /// + /// Sets the last access time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + _wrapped.SetLastAccessTimeUtc(path, newTime); + } + + /// + /// Gets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public override DateTime GetLastWriteTime(string path) + { + return _wrapped.GetLastWriteTime(path); + } + + /// + /// Sets the last modification time (in local time) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTime(string path, DateTime newTime) + { + _wrapped.SetLastWriteTime(path, newTime); + } + + /// + /// Gets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The last write time. + public override DateTime GetLastWriteTimeUtc(string path) + { + return _wrapped.GetLastWriteTimeUtc(path); + } + + /// + /// Sets the last modification time (in UTC) of a file or directory. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + _wrapped.SetLastWriteTimeUtc(path, newTime); + } + + /// + /// Gets the length of a file. + /// + /// The path to the file. + /// The length in bytes. + public override long GetFileLength(string path) + { + return _wrapped.GetFileLength(path); + } + + /// + /// Gets an object representing a possible file. + /// + /// The file path. + /// The representing object. + /// The file does not need to exist. + public override DiscFileInfo GetFileInfo(string path) + { + return new DiscFileInfo(this, path); + } + + /// + /// Gets an object representing a possible directory. + /// + /// The directory path. + /// The representing object. + /// The directory does not need to exist. + public override DiscDirectoryInfo GetDirectoryInfo(string path) + { + return new DiscDirectoryInfo(this, path); + } + + /// + /// Gets an object representing a possible file system object (file or directory). + /// + /// The file system path. + /// The representing object. + /// The file system object does not need to exist. + public override DiscFileSystemInfo GetFileSystemInfo(string path) + { + return new DiscFileSystemInfo(this, path); + } + + /// + /// Size of the Filesystem in bytes + /// + public override long Size + { + get { return _wrapped.Size; } + } + + /// + /// Used space of the Filesystem in bytes + /// + public override long UsedSpace + { + get { return _wrapped.UsedSpace; } + } + + /// + /// Available space of the Filesystem in bytes + /// + public override long AvailableSpace + { + get { return _wrapped.AvailableSpace; } + } + + /// + /// Provides access to the actual file system implementation. + /// + /// The concrete type representing directory entries. + /// The concrete type representing files. + /// The concrete type representing directories. + /// The concrete type holding global state. + /// The actual file system instance. + protected VfsFileSystem GetRealFileSystem + () + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + where TDirectory : class, IVfsDirectory, TFile + where TContext : VfsContext + { + return (VfsFileSystem)_wrapped; + } + + /// + /// Provides access to the actual file system implementation. + /// + /// The concrete type of the actual file system. + /// The actual file system instance. + protected T GetRealFileSystem() + where T : DiscFileSystem + { + return (T)_wrapped; + } + } +} diff --git a/DiscUtils/Core/Vfs/VfsFileSystemFactory.cs b/DiscUtils/Core/Vfs/VfsFileSystemFactory.cs new file mode 100644 index 0000000..4c93880 --- /dev/null +++ b/DiscUtils/Core/Vfs/VfsFileSystemFactory.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Vfs +{ + /// + /// Base class for logic to detect file systems. + /// + public abstract class VfsFileSystemFactory + { + /// + /// Detects if a stream contains any known file systems. + /// + /// The stream to inspect. + /// A list of file systems (may be empty). + public FileSystemInfo[] Detect(Stream stream) + { + return Detect(stream, null); + } + + /// + /// Detects if a volume contains any known file systems. + /// + /// The volume to inspect. + /// A list of file systems (may be empty). + public FileSystemInfo[] Detect(VolumeInfo volume) + { + using (Stream stream = volume.Open()) + { + return Detect(stream, volume); + } + } + + /// + /// The logic for detecting file systems. + /// + /// The stream to inspect. + /// Optionally, information about the volume. + /// A list of file systems detected (may be empty). + public abstract FileSystemInfo[] Detect(Stream stream, VolumeInfo volumeInfo); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/VfsFileSystemFactoryAttribute.cs b/DiscUtils/Core/Vfs/VfsFileSystemFactoryAttribute.cs new file mode 100644 index 0000000..aa370e0 --- /dev/null +++ b/DiscUtils/Core/Vfs/VfsFileSystemFactoryAttribute.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Vfs +{ + /// + /// Attribute identifying file system factory classes. + /// + [AttributeUsage(AttributeTargets.Class)] + public sealed class VfsFileSystemFactoryAttribute : Attribute {} +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/VfsFileSystemInfo.cs b/DiscUtils/Core/Vfs/VfsFileSystemInfo.cs new file mode 100644 index 0000000..685f1d2 --- /dev/null +++ b/DiscUtils/Core/Vfs/VfsFileSystemInfo.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Vfs +{ + /// + /// Class holding information about a file system. + /// + public sealed class VfsFileSystemInfo : FileSystemInfo + { + private readonly VfsFileSystemOpener _openDelegate; + + /// + /// Initializes a new instance of the VfsFileSystemInfo class. + /// + /// The name of the file system. + /// A one-line description of the file system. + /// A delegate that can open streams as the indicated file system. + public VfsFileSystemInfo(string name, string description, VfsFileSystemOpener openDelegate) + { + Name = name; + Description = description; + _openDelegate = openDelegate; + } + + /// + /// Gets a one-line description of the file system. + /// + public override string Description { get; } + + /// + /// Gets the name of the file system. + /// + public override string Name { get; } + + /// + /// Opens a volume using the file system. + /// + /// The volume to access. + /// Parameters for the file system. + /// A file system instance. + public override DiscFileSystem Open(VolumeInfo volume, FileSystemParameters parameters) + { + return _openDelegate(volume.Open(), volume, parameters); + } + + /// + /// Opens a stream using the file system. + /// + /// The stream to access. + /// Parameters for the file system. + /// A file system instance. + public override DiscFileSystem Open(Stream stream, FileSystemParameters parameters) + { + return _openDelegate(stream, null, parameters); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/VfsFileSystemOpener.cs b/DiscUtils/Core/Vfs/VfsFileSystemOpener.cs new file mode 100644 index 0000000..cbda9e8 --- /dev/null +++ b/DiscUtils/Core/Vfs/VfsFileSystemOpener.cs @@ -0,0 +1,14 @@ +using System.IO; + +namespace DiscUtils.Vfs +{ + /// + /// Delegate for instantiating a file system. + /// + /// The stream containing the file system. + /// Optional, information about the volume the file system is on. + /// Parameters for the file system. + /// A file system implementation. + public delegate DiscFileSystem VfsFileSystemOpener( + Stream stream, VolumeInfo volumeInfo, FileSystemParameters parameters); +} \ No newline at end of file diff --git a/DiscUtils/Core/Vfs/VfsReadOnlyFileSystem.cs b/DiscUtils/Core/Vfs/VfsReadOnlyFileSystem.cs new file mode 100644 index 0000000..c7528f6 --- /dev/null +++ b/DiscUtils/Core/Vfs/VfsReadOnlyFileSystem.cs @@ -0,0 +1,169 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Vfs +{ + /// + /// Base class for read-only file system implementations. + /// + /// The concrete type representing directory entries. + /// The concrete type representing files. + /// The concrete type representing directories. + /// The concrete type holding global state. + public abstract class VfsReadOnlyFileSystem : + VfsFileSystem + where TDirEntry : VfsDirEntry + where TFile : IVfsFile + where TDirectory : class, IVfsDirectory, TFile + where TContext : VfsContext + { + /// + /// Initializes a new instance of the VfsReadOnlyFileSystem class. + /// + /// The default file system options. + protected VfsReadOnlyFileSystem(DiscFileSystemOptions defaultOptions) + : base(defaultOptions) {} + + /// + /// Indicates whether the file system is read-only or read-write. + /// + /// Always false. + public override bool CanWrite + { + get { return false; } + } + + /// + /// Copies a file - not supported on read-only file systems. + /// + /// The source file. + /// The destination file. + /// Whether to permit over-writing of an existing file. + public override void CopyFile(string sourceFile, string destinationFile, bool overwrite) + { + throw new NotSupportedException(); + } + + /// + /// Creates a directory - not supported on read-only file systems. + /// + /// The path of the new directory. + public override void CreateDirectory(string path) + { + throw new NotSupportedException(); + } + + /// + /// Deletes a directory - not supported on read-only file systems. + /// + /// The path of the directory to delete. + public override void DeleteDirectory(string path) + { + throw new NotSupportedException(); + } + + /// + /// Deletes a file - not supported on read-only file systems. + /// + /// The path of the file to delete. + public override void DeleteFile(string path) + { + throw new NotSupportedException(); + } + + /// + /// Moves a directory - not supported on read-only file systems. + /// + /// The directory to move. + /// The target directory name. + public override void MoveDirectory(string sourceDirectoryName, string destinationDirectoryName) + { + throw new NotSupportedException(); + } + + /// + /// Moves a file - not supported on read-only file systems. + /// + /// The file to move. + /// The target file name. + /// Whether to allow an existing file to be overwritten. + public override void MoveFile(string sourceName, string destinationName, bool overwrite) + { + throw new NotSupportedException(); + } + + /// + /// Opens the specified file. + /// + /// The full path of the file to open. + /// The file mode for the created stream. + /// The new stream. + public override SparseStream OpenFile(string path, FileMode mode) + { + return OpenFile(path, mode, FileAccess.Read); + } + + /// + /// Sets the attributes of a file or directory - not supported on read-only file systems. + /// + /// The file or directory to change. + /// The new attributes of the file or directory. + public override void SetAttributes(string path, FileAttributes newValue) + { + throw new NotSupportedException(); + } + + /// + /// Sets the creation time (in UTC) of a file or directory - not supported on read-only file systems. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetCreationTimeUtc(string path, DateTime newTime) + { + throw new NotSupportedException(); + } + + /// + /// Sets the last access time (in UTC) of a file or directory - not supported on read-only file systems. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastAccessTimeUtc(string path, DateTime newTime) + { + throw new NotSupportedException(); + } + + /// + /// Sets the last modification time (in UTC) of a file or directory - not supported on read-only file systems. + /// + /// The path of the file or directory. + /// The new time to set. + public override void SetLastWriteTimeUtc(string path, DateTime newTime) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/VirtualDisk.cs b/DiscUtils/Core/VirtualDisk.cs new file mode 100644 index 0000000..905c5f9 --- /dev/null +++ b/DiscUtils/Core/VirtualDisk.cs @@ -0,0 +1,641 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using DiscUtils.Internal; +using DiscUtils.Partitions; +using DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Base class representing virtual hard disks. + /// + public abstract class VirtualDisk : +#if !NETSTANDARD + MarshalByRefObject, +#endif + IDisposable + { + private VirtualDiskTransport _transport; + + /// + /// Finalizes an instance of the VirtualDisk class. + /// + ~VirtualDisk() + { + Dispose(false); + } + + /// + /// Gets the set of disk formats supported as an array of file extensions. + /// + [Obsolete("Use VirtualDiskManager.SupportedDiskFormats")] + public static ICollection SupportedDiskFormats + { + get { return VirtualDiskManager.SupportedDiskFormats; } + } + + /// + /// Gets the set of disk types supported, as an array of identifiers. + /// + [Obsolete("Use VirtualDiskManager.SupportedDiskTypes")] + public static ICollection SupportedDiskTypes + { + get { return VirtualDiskManager.SupportedDiskTypes; } + } + + /// + /// Gets the geometry of the disk. + /// + public abstract Geometry Geometry { get; } + + /// + /// Gets the geometry of the disk as it is anticipated a hypervisor BIOS will represent it. + /// + public virtual Geometry BiosGeometry + { + get { return Geometry.MakeBiosSafe(Geometry, Capacity); } + } + + /// + /// Gets the type of disk represented by this object. + /// + public abstract VirtualDiskClass DiskClass { get; } + + /// + /// Gets the capacity of the disk (in bytes). + /// + public abstract long Capacity { get; } + + /// + /// Gets the size of the disk's logical blocks (aka sector size), in bytes. + /// + public virtual int BlockSize + { + get { return Sizes.Sector; } + } + + /// + /// Gets the logical sector size of the disk, in bytes. + /// + /// This is an alias for the BlockSize property. + public int SectorSize + { + get { return BlockSize; } + } + + /// + /// Gets the content of the disk as a stream. + /// + /// Note the returned stream is not guaranteed to be at any particular position. The actual position + /// will depend on the last partition table/file system activity, since all access to the disk contents pass + /// through a single stream instance. Set the stream position before accessing the stream. + public abstract SparseStream Content { get; } + + /// + /// Gets the layers that make up the disk. + /// + public abstract IEnumerable Layers { get; } + + /// + /// Gets or sets the Windows disk signature of the disk, which uniquely identifies the disk. + /// + public virtual int Signature + { + get { return EndianUtilities.ToInt32LittleEndian(GetMasterBootRecord(), 0x01B8); } + + set + { + byte[] mbr = GetMasterBootRecord(); + EndianUtilities.WriteBytesLittleEndian(value, mbr, 0x01B8); + SetMasterBootRecord(mbr); + } + } + + /// + /// Gets a value indicating whether the disk appears to have a valid partition table. + /// + /// There is no reliable way to determine whether a disk has a valid partition + /// table. The 'guess' consists of checking for basic indicators and looking for obviously + /// invalid data, such as overlapping partitions. + public virtual bool IsPartitioned + { + get { return PartitionTable.IsPartitioned(Content); } + } + + /// + /// Gets the object that interprets the partition structure. + /// + /// It is theoretically possible for a disk to contain two independent partition structures - a + /// BIOS/GPT one and an Apple one, for example. This method will return in order of preference, + /// a GUID partition table, a BIOS partition table, then in undefined preference one of any other partition + /// tables found. See PartitionTable.GetPartitionTables to gain access to all the discovered partition + /// tables on a disk. + public virtual PartitionTable Partitions + { + get + { + IList tables = PartitionTable.GetPartitionTables(this); + if (tables == null || tables.Count == 0) + { + return null; + } + if (tables.Count == 1) + { + return tables[0]; + } + PartitionTable best = null; + int bestScore = -1; + for (int i = 0; i < tables.Count; ++i) + { + int newScore = 0; + if (tables[i] is GuidPartitionTable) + { + newScore = 2; + } + else if (tables[i] is BiosPartitionTable) + { + newScore = 1; + } + + if (newScore > bestScore) + { + bestScore = newScore; + best = tables[i]; + } + } + + return best; + } + } + + /// + /// Gets the parameters of the disk. + /// + /// Most of the parameters are also available individually, such as DiskType and Capacity. + public virtual VirtualDiskParameters Parameters + { + get + { + return new VirtualDiskParameters + { + DiskType = DiskClass, + Capacity = Capacity, + Geometry = Geometry, + BiosGeometry = BiosGeometry, + AdapterType = GenericDiskAdapterType.Ide + }; + } + } + + /// + /// Gets information about the type of disk. + /// + /// This property provides access to meta-data about the disk format, for example whether the + /// BIOS geometry is preserved in the disk file. + public abstract VirtualDiskTypeInfo DiskTypeInfo { get; } + + /// + /// Gets the set of supported variants of a type of virtual disk. + /// + /// A type, as returned by . + /// A collection of identifiers, or empty if there is no variant concept for this type of disk. + public static ICollection GetSupportedDiskVariants(string type) + { + return VirtualDiskManager.TypeMap[type].Variants; + } + + /// + /// Gets information about disk type. + /// + /// The disk type, as returned by . + /// The variant of the disk type. + /// Information about the disk type. + public static VirtualDiskTypeInfo GetDiskType(string type, string variant) + { + return VirtualDiskManager.TypeMap[type].GetDiskTypeInformation(variant); + } + + /// + /// Create a new virtual disk, possibly within an existing disk. + /// + /// The file system to create the disk on. + /// The type of disk to create (see ). + /// The variant of the type to create (see ). + /// The path (or URI) for the disk to create. + /// The capacity of the new disk. + /// The geometry of the new disk (or null). + /// Untyped parameters controlling the creation process (TBD). + /// The newly created disk. + public static VirtualDisk CreateDisk(DiscFileSystem fileSystem, string type, string variant, string path, long capacity, Geometry geometry, Dictionary parameters) + { + VirtualDiskFactory factory = VirtualDiskManager.TypeMap[type]; + + VirtualDiskParameters diskParams = new VirtualDiskParameters + { + AdapterType = GenericDiskAdapterType.Scsi, + Capacity = capacity, + Geometry = geometry + }; + + if (parameters != null) + { + foreach (string key in parameters.Keys) + { + diskParams.ExtendedParameters[key] = parameters[key]; + } + } + + return factory.CreateDisk(new DiscFileLocator(fileSystem, Utilities.GetDirectoryFromPath(path)), variant.ToLowerInvariant(), Utilities.GetFileFromPath(path), diskParams); + } + + /// + /// Create a new virtual disk. + /// + /// The type of disk to create (see ). + /// The variant of the type to create (see ). + /// The path (or URI) for the disk to create. + /// The capacity of the new disk. + /// The geometry of the new disk (or null). + /// Untyped parameters controlling the creation process (TBD). + /// The newly created disk. + public static VirtualDisk CreateDisk(string type, string variant, string path, long capacity, Geometry geometry, Dictionary parameters) + { + return CreateDisk(type, variant, path, capacity, geometry, null, null, parameters); + } + + /// + /// Create a new virtual disk. + /// + /// The type of disk to create (see ). + /// The variant of the type to create (see ). + /// The path (or URI) for the disk to create. + /// The capacity of the new disk. + /// The geometry of the new disk (or null). + /// The user identity to use when accessing the path (or null). + /// The password to use when accessing the path (or null). + /// Untyped parameters controlling the creation process (TBD). + /// The newly created disk. + public static VirtualDisk CreateDisk(string type, string variant, string path, long capacity, Geometry geometry, string user, string password, Dictionary parameters) + { + VirtualDiskParameters diskParams = new VirtualDiskParameters + { + AdapterType = GenericDiskAdapterType.Scsi, + Capacity = capacity, + Geometry = geometry + }; + + if (parameters != null) + { + foreach (string key in parameters.Keys) + { + diskParams.ExtendedParameters[key] = parameters[key]; + } + } + + return CreateDisk(type, variant, path, diskParams, user, password); + } + + /// + /// Create a new virtual disk. + /// + /// The type of disk to create (see ). + /// The variant of the type to create (see ). + /// The path (or URI) for the disk to create. + /// Parameters controlling the capacity, geometry, etc of the new disk. + /// The user identity to use when accessing the path (or null). + /// The password to use when accessing the path (or null). + /// The newly created disk. + public static VirtualDisk CreateDisk(string type, string variant, string path, VirtualDiskParameters diskParameters, string user, string password) + { + Uri uri = PathToUri(path); + VirtualDisk result = null; + + Type transportType; + if (!VirtualDiskManager.DiskTransports.TryGetValue(uri.Scheme.ToUpperInvariant(), out transportType)) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "Unable to parse path '{0}'", path), path); + } + + VirtualDiskTransport transport = (VirtualDiskTransport)Activator.CreateInstance(transportType); + + try + { + transport.Connect(uri, user, password); + + if (transport.IsRawDisk) + { + result = transport.OpenDisk(FileAccess.ReadWrite); + } + else + { + VirtualDiskFactory factory = VirtualDiskManager.TypeMap[type]; + + result = factory.CreateDisk(transport.GetFileLocator(), variant.ToLowerInvariant(), Utilities.GetFileFromPath(path), diskParameters); + } + + if (result != null) + { + result._transport = transport; + transport = null; + } + + return result; + } + finally + { + if (transport != null) + { + transport.Dispose(); + } + } + } + + /// + /// Opens an existing virtual disk. + /// + /// The path of the virtual disk to open, can be a URI. + /// The desired access to the disk. + /// The Virtual Disk, or null if an unknown disk format. + public static VirtualDisk OpenDisk(string path, FileAccess access) + { + return OpenDisk(path, null, access, null, null); + } + + /// + /// Opens an existing virtual disk. + /// + /// The path of the virtual disk to open, can be a URI. + /// The desired access to the disk. + /// The user name to use for authentication (if necessary). + /// The password to use for authentication (if necessary). + /// The Virtual Disk, or null if an unknown disk format. + public static VirtualDisk OpenDisk(string path, FileAccess access, string user, string password) + { + return OpenDisk(path, null, access, user, password); + } + + /// + /// Opens an existing virtual disk. + /// + /// The path of the virtual disk to open, can be a URI. + /// Force the detected disk type (null to detect). + /// The desired access to the disk. + /// The user name to use for authentication (if necessary). + /// The password to use for authentication (if necessary). + /// The Virtual Disk, or null if an unknown disk format. + /// + /// The detected disk type can be forced by specifying a known disk type: + /// RAW, VHD, VMDK, etc. + /// + public static VirtualDisk OpenDisk(string path, string forceType, FileAccess access, string user, string password) + { + Uri uri = PathToUri(path); + VirtualDisk result = null; + + Type transportType; + if (!VirtualDiskManager.DiskTransports.TryGetValue(uri.Scheme.ToUpperInvariant(), out transportType)) + { + throw new FileNotFoundException(string.Format(CultureInfo.InvariantCulture, "Unable to parse path '{0}'", path), path); + } + + VirtualDiskTransport transport = (VirtualDiskTransport)Activator.CreateInstance(transportType); + + try + { + transport.Connect(uri, user, password); + + if (transport.IsRawDisk) + { + result = transport.OpenDisk(access); + } + else + { + bool foundFactory; + VirtualDiskFactory factory; + + if (!string.IsNullOrEmpty(forceType)) + { + foundFactory = VirtualDiskManager.TypeMap.TryGetValue(forceType, out factory); + } + else + { + string extension = Path.GetExtension(uri.AbsolutePath).ToUpperInvariant(); + if (extension.StartsWith(".", StringComparison.Ordinal)) + { + extension = extension.Substring(1); + } + + foundFactory = VirtualDiskManager.ExtensionMap.TryGetValue(extension, out factory); + } + + if (foundFactory) + { + result = factory.OpenDisk(transport.GetFileLocator(), transport.GetFileName(), access); + } + } + + if (result != null) + { + result._transport = transport; + transport = null; + } + + return result; + } + finally + { + if (transport != null) + { + transport.Dispose(); + } + } + } + + /// + /// Opens an existing virtual disk, possibly from within an existing disk. + /// + /// The file system to open the disk on. + /// The path of the virtual disk to open. + /// The desired access to the disk. + /// The Virtual Disk, or null if an unknown disk format. + public static VirtualDisk OpenDisk(DiscFileSystem fs, string path, FileAccess access) + { + if (fs == null) + { + return OpenDisk(path, access); + } + + string extension = Path.GetExtension(path).ToUpperInvariant(); + if (extension.StartsWith(".", StringComparison.Ordinal)) + { + extension = extension.Substring(1); + } + + VirtualDiskFactory factory; + if (VirtualDiskManager.ExtensionMap.TryGetValue(extension, out factory)) + { + return factory.OpenDisk(fs, path, access); + } + + return null; + } + + /// + /// Disposes of this instance, freeing underlying resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Reads the first sector of the disk, known as the Master Boot Record. + /// + /// The MBR as a byte array. + public virtual byte[] GetMasterBootRecord() + { + byte[] sector = new byte[Sizes.Sector]; + + long oldPos = Content.Position; + Content.Position = 0; + StreamUtilities.ReadExact(Content, sector, 0, Sizes.Sector); + Content.Position = oldPos; + + return sector; + } + + /// + /// Overwrites the first sector of the disk, known as the Master Boot Record. + /// + /// The master boot record, must be 512 bytes in length. + public virtual void SetMasterBootRecord(byte[] data) + { + if (data == null) + { + throw new ArgumentNullException(nameof(data)); + } + if (data.Length != Sizes.Sector) + { + throw new ArgumentException("The Master Boot Record must be exactly 512 bytes in length", nameof(data)); + } + + long oldPos = Content.Position; + Content.Position = 0; + Content.Write(data, 0, Sizes.Sector); + Content.Position = oldPos; + } + + /// + /// Create a new differencing disk, possibly within an existing disk. + /// + /// The file system to create the disk on. + /// The path (or URI) for the disk to create. + /// The newly created disk. + public abstract VirtualDisk CreateDifferencingDisk(DiscFileSystem fileSystem, string path); + + /// + /// Create a new differencing disk. + /// + /// The path (or URI) for the disk to create. + /// The newly created disk. + public abstract VirtualDisk CreateDifferencingDisk(string path); + + internal static VirtualDiskLayer OpenDiskLayer(FileLocator locator, string path, FileAccess access) + { + string extension = Path.GetExtension(path).ToUpperInvariant(); + if (extension.StartsWith(".", StringComparison.Ordinal)) + { + extension = extension.Substring(1); + } + + VirtualDiskFactory factory; + if (VirtualDiskManager.ExtensionMap.TryGetValue(extension, out factory)) + { + return factory.OpenDiskLayer(locator, path, access); + } + + return null; + } + + /// + /// Disposes of underlying resources. + /// + /// true if running inside Dispose(), indicating + /// graceful cleanup of all managed objects should be performed, or false + /// if running inside destructor. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_transport != null) + { + _transport.Dispose(); + } + + _transport = null; + } + } + + private static Uri PathToUri(string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("Path must not be null or empty", nameof(path)); + } + + if (path.Contains("://")) + { + return new Uri(path); + } + + if (!Path.IsPathRooted(path)) + { + path = Path.GetFullPath(path); + } + + // Built-in Uri class does cope well with query params on file Uris, so do some + // parsing ourselves... + if (path.Length >= 1 && path[0] == '\\') + { + UriBuilder builder = new UriBuilder("file:" + path.Replace('\\', '/')); + return builder.Uri; + } + if (path.StartsWith("//", StringComparison.OrdinalIgnoreCase)) + { + UriBuilder builder = new UriBuilder("file:" + path); + return builder.Uri; + } + if (path.Length >= 2 && path[1] == ':') + { + UriBuilder builder = new UriBuilder("file:///" + path.Replace('\\', '/')); + return builder.Uri; + } + return new Uri(path); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/VirtualDiskClass.cs b/DiscUtils/Core/VirtualDiskClass.cs new file mode 100644 index 0000000..9c4d4ba --- /dev/null +++ b/DiscUtils/Core/VirtualDiskClass.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// Enumeration of different classes of disk. + /// + public enum VirtualDiskClass + { + /// + /// Unknown (or unspecified) type. + /// + None = 0, + + /// + /// Hard disk. + /// + HardDisk = 1, + + /// + /// Optical disk, such as CD or DVD. + /// + OpticalDisk = 2, + + /// + /// Floppy disk. + /// + FloppyDisk = 3 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/VirtualDiskExtent.cs b/DiscUtils/Core/VirtualDiskExtent.cs new file mode 100644 index 0000000..86eb3c1 --- /dev/null +++ b/DiscUtils/Core/VirtualDiskExtent.cs @@ -0,0 +1,78 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Base class represented a stored extent of a virtual disk. + /// + /// + /// Some file formats can divide a logical disk layer into multiple extents, stored in + /// different files. This class represents those extents. Normally, all virtual disks + /// have at least one extent. + /// + public abstract class VirtualDiskExtent : IDisposable + { + /// + /// Gets the capacity of the extent (in bytes). + /// + public abstract long Capacity { get; } + + /// + /// Gets a value indicating whether the extent only stores meaningful sectors. + /// + public abstract bool IsSparse { get; } + + /// + /// Gets the size of the extent (in bytes) on underlying storage. + /// + public abstract long StoredSize { get; } + + /// + /// Disposes of this instance, freeing underlying resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Gets the content of this extent. + /// + /// The parent stream (if any). + /// Controls ownership of the parent stream. + /// The content as a stream. + public abstract MappedStream OpenContent(SparseStream parent, Ownership ownsParent); + + /// + /// Disposes of underlying resources. + /// + /// true if running inside Dispose(), indicating + /// graceful cleanup of all managed objects should be performed, or false + /// if running inside destructor. + protected virtual void Dispose(bool disposing) {} + } +} \ No newline at end of file diff --git a/DiscUtils/Core/VirtualDiskLayer.cs b/DiscUtils/Core/VirtualDiskLayer.cs new file mode 100644 index 0000000..eb6eb7b --- /dev/null +++ b/DiscUtils/Core/VirtualDiskLayer.cs @@ -0,0 +1,126 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Represents the base layer, or a differencing layer of a VirtualDisk. + /// + /// + /// VirtualDisks are composed of one or more layers - a base layer + /// which represents the entire disk (even if not all bytes are actually stored), + /// and a number of differencing layers that store the disk sectors that are + /// logically different to the base layer. + /// Disk Layers may not store all sectors. Any sectors that are not stored + /// are logically zero's (for base layers), or holes through to the layer underneath + /// (all other layers). + /// + public abstract class VirtualDiskLayer : IDisposable + { + /// + /// Gets the capacity of the disk (in bytes). + /// + internal abstract long Capacity { get; } + + /// + /// Gets and sets the logical extents that make up this layer. + /// + public virtual IList Extents + { + get { return new List(); } + } + + /// + /// Gets the full path to this disk layer, or empty string. + /// + public virtual string FullPath + { + get { return string.Empty; } + } + + /// + /// Gets the geometry of the virtual disk layer. + /// + public abstract Geometry Geometry { get; } + + /// + /// Gets a value indicating whether the layer only stores meaningful sectors. + /// + public abstract bool IsSparse { get; } + + /// + /// Gets a value indicating whether this is a differential disk. + /// + public abstract bool NeedsParent { get; } + + /// + /// Gets a FileLocator that can resolve relative paths, or null. + /// + /// + /// Typically used to locate parent disks. + /// + internal abstract FileLocator RelativeFileLocator { get; } + + /// + /// Disposes of this instance, freeing underlying resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Finalizes an instance of the VirtualDiskLayer class. + /// + ~VirtualDiskLayer() + { + Dispose(false); + } + + /// + /// Gets the content of this layer. + /// + /// The parent stream (if any). + /// Controls ownership of the parent stream. + /// The content as a stream. + public abstract SparseStream OpenContent(SparseStream parent, Ownership ownsParent); + + /// + /// Gets the possible locations of the parent file (if any). + /// + /// Array of strings, empty if no parent. + public abstract string[] GetParentLocations(); + + /// + /// Disposes of underlying resources. + /// + /// true if running inside Dispose(), indicating + /// graceful cleanup of all managed objects should be performed, or false + /// if running inside destructor. + protected virtual void Dispose(bool disposing) {} + } +} \ No newline at end of file diff --git a/DiscUtils/Core/VirtualDiskManager.cs b/DiscUtils/Core/VirtualDiskManager.cs new file mode 100644 index 0000000..d46226a --- /dev/null +++ b/DiscUtils/Core/VirtualDiskManager.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using DiscUtils.CoreCompat; +using DiscUtils.Internal; + +namespace DiscUtils +{ + /// + /// Helps discover and use VirtualDiskFactory's + /// + public static class VirtualDiskManager + { + static VirtualDiskManager() + { + ExtensionMap = new Dictionary(); + TypeMap = new Dictionary(); + DiskTransports = new Dictionary(); + } + + internal static Dictionary DiskTransports { get; } + internal static Dictionary ExtensionMap { get; } + + /// + /// Gets the set of disk formats supported as an array of file extensions. + /// + public static ICollection SupportedDiskFormats + { + get { return ExtensionMap.Keys; } + } + + /// + /// Gets the set of disk types supported, as an array of identifiers. + /// + public static ICollection SupportedDiskTypes + { + get { return TypeMap.Keys; } + } + + internal static Dictionary TypeMap { get; } + + /// + /// Locates VirtualDiskFactory factories attributed with VirtualDiskFactoryAttribute, and types marked with VirtualDiskTransportAttribute, that are able to work with Virtual Disk types. + /// + /// An assembly to scan + public static void RegisterVirtualDiskTypes(Assembly assembly) + { + foreach (Type type in assembly.GetTypes()) + { + VirtualDiskFactoryAttribute diskFactoryAttribute = (VirtualDiskFactoryAttribute)ReflectionHelper.GetCustomAttribute(type, typeof(VirtualDiskFactoryAttribute), false); + if (diskFactoryAttribute != null) + { + VirtualDiskFactory factory = (VirtualDiskFactory)Activator.CreateInstance(type); + TypeMap.Add(diskFactoryAttribute.Type, factory); + + foreach (string extension in diskFactoryAttribute.FileExtensions) + { + ExtensionMap.Add(extension.ToUpperInvariant(), factory); + } + } + + VirtualDiskTransportAttribute diskTransportAttribute = ReflectionHelper.GetCustomAttribute(type, typeof(VirtualDiskTransportAttribute), false) as VirtualDiskTransportAttribute; + if (diskTransportAttribute != null) + { + DiskTransports.Add(diskTransportAttribute.Scheme.ToUpperInvariant(), type); + } + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/VirtualDiskParameters.cs b/DiscUtils/Core/VirtualDiskParameters.cs new file mode 100644 index 0000000..14b899c --- /dev/null +++ b/DiscUtils/Core/VirtualDiskParameters.cs @@ -0,0 +1,65 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; + +namespace DiscUtils +{ + /// + /// Common parameters for virtual disks. + /// + /// Not all attributes make sense for all kinds of disks, so some + /// may be null. Modifying instances of this class does not modify the + /// disk itself. + public sealed class VirtualDiskParameters + { + /// + /// Gets or sets the type of disk adapter. + /// + public GenericDiskAdapterType AdapterType { get; set; } + + /// + /// Gets or sets the logical (aka BIOS) geometry of the disk. + /// + public Geometry BiosGeometry { get; set; } + + /// + /// Gets or sets the disk capacity. + /// + public long Capacity { get; set; } + + /// + /// Gets or sets the type of disk (optical, hard disk, etc). + /// + public VirtualDiskClass DiskType { get; set; } + + /// + /// Gets a dictionary of extended parameters, that varies by disk type. + /// + public Dictionary ExtendedParameters { get; } = new Dictionary(); + + /// + /// Gets or sets the physical (aka IDE) geometry of the disk. + /// + public Geometry Geometry { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/VirtualDiskTypeInfo.cs b/DiscUtils/Core/VirtualDiskTypeInfo.cs new file mode 100644 index 0000000..7a7d356 --- /dev/null +++ b/DiscUtils/Core/VirtualDiskTypeInfo.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils +{ + /// + /// Information about a type of virtual disk. + /// + public sealed class VirtualDiskTypeInfo + { + /// + /// Gets or sets the algorithm for determining the geometry for a given disk capacity. + /// + public GeometryCalculation CalcGeometry { get; set; } + + /// + /// Gets or sets a value indicating whether this disk type can represent hard disks. + /// + public bool CanBeHardDisk { get; set; } + + /// + /// Gets or sets a value indicating whether this disk type requires a specific geometry for any given disk capacity. + /// + public bool DeterministicGeometry { get; set; } + + /// + /// Gets or sets the name of the virtual disk type. + /// + public string Name { get; set; } + + /// + /// Gets or sets a value indicating whether this disk type persists the BIOS geometry. + /// + public bool PreservesBiosGeometry { get; set; } + + /// + /// Gets or sets the variant of the virtual disk type. + /// + public string Variant { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/VolumeInfo.cs b/DiscUtils/Core/VolumeInfo.cs new file mode 100644 index 0000000..e0b4562 --- /dev/null +++ b/DiscUtils/Core/VolumeInfo.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +#if !NETSTANDARD +using System; +#endif +using DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// Base class that holds information about a disk volume. + /// + public abstract class VolumeInfo +#if !NETSTANDARD + : MarshalByRefObject +#endif + { + internal VolumeInfo() {} + + /// + /// Gets the one-byte BIOS type for this volume, which indicates the content. + /// + public abstract byte BiosType { get; } + + /// + /// Gets the size of the volume, in bytes. + /// + public abstract long Length { get; } + + /// + /// Gets the stable volume identity. + /// + /// The stability of the identity depends the disk structure. + /// In some cases the identity may include a simple index, when no other information + /// is available. Best practice is to add disks to the Volume Manager in a stable + /// order, if the stability of this identity is paramount. + public abstract string Identity { get; } + + /// + /// Gets the disk geometry of the underlying storage medium, if any (may be null). + /// + public abstract Geometry PhysicalGeometry { get; } + + /// + /// Gets the disk geometry of the underlying storage medium (as used in BIOS calls), may be null. + /// + public abstract Geometry BiosGeometry { get; } + + /// + /// Gets the offset of this volume in the underlying storage medium, if any (may be Zero). + /// + public abstract long PhysicalStartSector { get; } + + /// + /// Opens the volume, providing access to it's contents. + /// + /// Stream that can access the volume's contents. + public abstract SparseStream Open(); + } +} \ No newline at end of file diff --git a/DiscUtils/Core/VolumeManager.cs b/DiscUtils/Core/VolumeManager.cs new file mode 100644 index 0000000..7968e78 --- /dev/null +++ b/DiscUtils/Core/VolumeManager.cs @@ -0,0 +1,343 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using System.Reflection; +using DiscUtils.CoreCompat; +using DiscUtils.Internal; +using DiscUtils.Partitions; +using DiscUtils.Raw; +using DiscUtils.Streams; + +namespace DiscUtils +{ + /// + /// VolumeManager interprets partitions and other on-disk structures (possibly combining multiple disks). + /// + /// + /// Although file systems commonly are placed directly within partitions on a disk, in some + /// cases a logical volume manager / logical disk manager may be used, to combine disk regions in multiple + /// ways for data redundancy or other purposes. + /// + public sealed class VolumeManager +#if !NETSTANDARD + : MarshalByRefObject +#endif + { + private static List s_logicalVolumeFactories; + private readonly List _disks; + private bool _needScan; + + private Dictionary _physicalVolumes; + private Dictionary _logicalVolumes; + private static readonly Assembly _coreAssembly = ReflectionHelper.GetAssembly(typeof(VolumeManager)); + + /// + /// Initializes a new instance of the VolumeManager class. + /// + public VolumeManager() + { + _disks = new List(); + _physicalVolumes = new Dictionary(); + _logicalVolumes = new Dictionary(); + } + + /// + /// Initializes a new instance of the VolumeManager class. + /// + /// The initial disk to add. + public VolumeManager(VirtualDisk initialDisk) + : this() + { + AddDisk(initialDisk); + } + + /// + /// Initializes a new instance of the VolumeManager class. + /// + /// Content of the initial disk to add. + public VolumeManager(Stream initialDiskContent) + : this() + { + AddDisk(initialDiskContent); + } + + private static List LogicalVolumeFactories + { + get + { + if (s_logicalVolumeFactories == null) + { + List factories = new List(); + factories.AddRange(GetLogicalVolumeFactories(_coreAssembly)); + s_logicalVolumeFactories = factories; + } + + return s_logicalVolumeFactories; + } + } + + private static IEnumerable GetLogicalVolumeFactories(Assembly assembly) + { + foreach (Type type in assembly.GetTypes()) + { + foreach (LogicalVolumeFactoryAttribute attr in ReflectionHelper.GetCustomAttributes(type, typeof(LogicalVolumeFactoryAttribute), false)) + { + yield return (LogicalVolumeFactory)Activator.CreateInstance(type); + } + } + } + + /// + /// Register new LogicalVolumeFactories detected in an assembly + /// + /// The assembly to inspect + public static void RegisterLogicalVolumeFactory(Assembly assembly) + { + if (assembly == _coreAssembly) return; + LogicalVolumeFactories.AddRange(GetLogicalVolumeFactories(assembly)); + } + + /// + /// Gets the physical volumes held on a disk. + /// + /// The contents of the disk to inspect. + /// An array of volumes. + /// + /// By preference, use the form of this method that takes a disk parameter. + /// If the disk isn't partitioned, this method returns the entire disk contents + /// as a single volume. + /// + public static PhysicalVolumeInfo[] GetPhysicalVolumes(Stream diskContent) + { + return GetPhysicalVolumes(new Disk(diskContent, Ownership.None)); + } + + /// + /// Gets the physical volumes held on a disk. + /// + /// The disk to inspect. + /// An array of volumes. + /// If the disk isn't partitioned, this method returns the entire disk contents + /// as a single volume. + public static PhysicalVolumeInfo[] GetPhysicalVolumes(VirtualDisk disk) + { + return new VolumeManager(disk).GetPhysicalVolumes(); + } + + /// + /// Adds a disk to the volume manager. + /// + /// The disk to add. + /// The GUID the volume manager will use to identify the disk. + public string AddDisk(VirtualDisk disk) + { + _needScan = true; + int ordinal = _disks.Count; + _disks.Add(disk); + return GetDiskId(ordinal); + } + + /// + /// Adds a disk to the volume manager. + /// + /// The contents of the disk to add. + /// The GUID the volume manager will use to identify the disk. + public string AddDisk(Stream content) + { + return AddDisk(new Disk(content, Ownership.None)); + } + + /// + /// Gets the physical volumes from all disks added to this volume manager. + /// + /// An array of physical volumes. + public PhysicalVolumeInfo[] GetPhysicalVolumes() + { + if (_needScan) + { + Scan(); + } + + return new List(_physicalVolumes.Values).ToArray(); + } + + /// + /// Gets the logical volumes from all disks added to this volume manager. + /// + /// An array of logical volumes. + public LogicalVolumeInfo[] GetLogicalVolumes() + { + if (_needScan) + { + Scan(); + } + + return new List(_logicalVolumes.Values).ToArray(); + } + + /// + /// Gets a particular volume, based on it's identity. + /// + /// The volume's identity. + /// The volume information for the volume, or returns null. + public VolumeInfo GetVolume(string identity) + { + if (_needScan) + { + Scan(); + } + + PhysicalVolumeInfo pvi; + if (_physicalVolumes.TryGetValue(identity, out pvi)) + { + return pvi; + } + + LogicalVolumeInfo lvi; + if (_logicalVolumes.TryGetValue(identity, out lvi)) + { + return lvi; + } + + return null; + } + + private static void MapPhysicalVolumes(IEnumerable physicalVols, Dictionary result) + { + foreach (PhysicalVolumeInfo physicalVol in physicalVols) + { + LogicalVolumeInfo lvi = new LogicalVolumeInfo( + physicalVol.PartitionIdentity, + physicalVol, + physicalVol.Open, + physicalVol.Length, + physicalVol.BiosType, + LogicalVolumeStatus.Healthy); + + result.Add(lvi.Identity, lvi); + } + } + + /// + /// Scans all of the disks for their physical and logical volumes. + /// + private void Scan() + { + Dictionary newPhysicalVolumes = ScanForPhysicalVolumes(); + Dictionary newLogicalVolumes = ScanForLogicalVolumes(newPhysicalVolumes.Values); + + _physicalVolumes = newPhysicalVolumes; + _logicalVolumes = newLogicalVolumes; + + _needScan = false; + } + + private Dictionary ScanForLogicalVolumes(IEnumerable physicalVols) + { + List unhandledPhysical = new List(); + Dictionary result = new Dictionary(); + + foreach (PhysicalVolumeInfo pvi in physicalVols) + { + bool handled = false; + foreach (LogicalVolumeFactory volFactory in LogicalVolumeFactories) + { + if (volFactory.HandlesPhysicalVolume(pvi)) + { + handled = true; + break; + } + } + + if (!handled) + { + unhandledPhysical.Add(pvi); + } + } + + MapPhysicalVolumes(unhandledPhysical, result); + + foreach (LogicalVolumeFactory volFactory in LogicalVolumeFactories) + { + volFactory.MapDisks(_disks, result); + } + + return result; + } + + private Dictionary ScanForPhysicalVolumes() + { + Dictionary result = new Dictionary(); + + // First scan physical volumes + for (int i = 0; i < _disks.Count; ++i) + { + VirtualDisk disk = _disks[i]; + string diskId = GetDiskId(i); + + if (PartitionTable.IsPartitioned(disk.Content)) + { + foreach (PartitionTable table in PartitionTable.GetPartitionTables(disk)) + { + foreach (PartitionInfo part in table.Partitions) + { + PhysicalVolumeInfo pvi = new PhysicalVolumeInfo(diskId, disk, part); + result.Add(pvi.Identity, pvi); + } + } + } + else + { + PhysicalVolumeInfo pvi = new PhysicalVolumeInfo(diskId, disk); + result.Add(pvi.Identity, pvi); + } + } + + return result; + } + + private string GetDiskId(int ordinal) + { + VirtualDisk disk = _disks[ordinal]; + if (disk.IsPartitioned) + { + Guid guid = disk.Partitions.DiskGuid; + if (guid != Guid.Empty) + { + return "DG" + guid.ToString("B"); + } + } + + int sig = disk.Signature; + if (sig != 0) + { + return "DS" + sig.ToString("X8", CultureInfo.InvariantCulture); + } + + return "DO" + ordinal; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Core/WindowsFileInformation.cs b/DiscUtils/Core/WindowsFileInformation.cs new file mode 100644 index 0000000..564d72f --- /dev/null +++ b/DiscUtils/Core/WindowsFileInformation.cs @@ -0,0 +1,58 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils +{ + /// + /// Common information for Windows files. + /// + public class WindowsFileInformation + { + /// + /// Gets or sets the last time the file was changed. + /// + public DateTime ChangeTime { get; set; } + + /// + /// Gets or sets the creation time of the file. + /// + public DateTime CreationTime { get; set; } + + /// + /// Gets or sets the file attributes. + /// + public FileAttributes FileAttributes { get; set; } + + /// + /// Gets or sets the last access time of the file. + /// + public DateTime LastAccessTime { get; set; } + + /// + /// Gets or sets the modification time of the file. + /// + public DateTime LastWriteTime { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/DiscUtils.csproj b/DiscUtils/DiscUtils.csproj new file mode 100644 index 0000000..132c02c --- /dev/null +++ b/DiscUtils/DiscUtils.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/DiscUtils/Iso9660/BaseVolumeDescriptor.cs b/DiscUtils/Iso9660/BaseVolumeDescriptor.cs new file mode 100644 index 0000000..3aeb519 --- /dev/null +++ b/DiscUtils/Iso9660/BaseVolumeDescriptor.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660 +{ + internal class BaseVolumeDescriptor + { + public const string Iso9660StandardIdentifier = "CD001"; + + public readonly string StandardIdentifier; + public readonly VolumeDescriptorType VolumeDescriptorType; + public readonly byte VolumeDescriptorVersion; + private int SectorSize; + + public BaseVolumeDescriptor(VolumeDescriptorType type, byte version, int sectorSize) + { + VolumeDescriptorType = type; + StandardIdentifier = "CD001"; + VolumeDescriptorVersion = version; + SectorSize = sectorSize; + } + + public BaseVolumeDescriptor(byte[] src, int offset) + { + VolumeDescriptorType = (VolumeDescriptorType)src[offset + 0]; + StandardIdentifier = Encoding.ASCII.GetString(src, offset + 1, 5); + VolumeDescriptorVersion = src[offset + 6]; + } + + internal virtual void WriteTo(byte[] buffer, int offset) + { + Array.Clear(buffer, offset, SectorSize); + buffer[offset] = (byte)VolumeDescriptorType; + IsoUtilities.WriteAChars(buffer, offset + 1, 5, StandardIdentifier); + buffer[offset + 6] = VolumeDescriptorVersion; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/BootDeviceEmulation.cs b/DiscUtils/Iso9660/BootDeviceEmulation.cs new file mode 100644 index 0000000..7f4354d --- /dev/null +++ b/DiscUtils/Iso9660/BootDeviceEmulation.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + /// + /// Enumeration of boot device emulation modes. + /// + public enum BootDeviceEmulation : byte + { + /// + /// No emulation, the boot image is just loaded and executed. + /// + NoEmulation = 0x0, + + /// + /// Emulates 1.2MB diskette image as drive A. + /// + Diskette1200KiB = 0x1, + + /// + /// Emulates 1.44MB diskette image as drive A. + /// + Diskette1440KiB = 0x2, + + /// + /// Emulates 2.88MB diskette image as drive A. + /// + Diskette2880KiB = 0x3, + + /// + /// Emulates hard disk image as drive C. + /// + HardDisk = 0x4 + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/BootInitialEntry.cs b/DiscUtils/Iso9660/BootInitialEntry.cs new file mode 100644 index 0000000..b71ddfc --- /dev/null +++ b/DiscUtils/Iso9660/BootInitialEntry.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal class BootInitialEntry + { + public byte BootIndicator; + public BootDeviceEmulation BootMediaType; + public uint ImageStart; + public ushort LoadSegment; + public ushort SectorCount; + public byte SystemType; + + public BootInitialEntry() {} + + public BootInitialEntry(byte[] buffer, int offset) + { + BootIndicator = buffer[offset + 0x00]; + BootMediaType = (BootDeviceEmulation)buffer[offset + 0x01]; + LoadSegment = EndianUtilities.ToUInt16LittleEndian(buffer, offset + 0x02); + SystemType = buffer[offset + 0x04]; + SectorCount = EndianUtilities.ToUInt16LittleEndian(buffer, offset + 0x06); + ImageStart = EndianUtilities.ToUInt32LittleEndian(buffer, offset + 0x08); + } + + internal void WriteTo(byte[] buffer, int offset) + { + Array.Clear(buffer, offset, 0x20); + buffer[offset + 0x00] = BootIndicator; + buffer[offset + 0x01] = (byte)BootMediaType; + EndianUtilities.WriteBytesLittleEndian(LoadSegment, buffer, offset + 0x02); + buffer[offset + 0x04] = SystemType; + EndianUtilities.WriteBytesLittleEndian(SectorCount, buffer, offset + 0x06); + EndianUtilities.WriteBytesLittleEndian(ImageStart, buffer, offset + 0x08); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/BootValidationEntry.cs b/DiscUtils/Iso9660/BootValidationEntry.cs new file mode 100644 index 0000000..9440355 --- /dev/null +++ b/DiscUtils/Iso9660/BootValidationEntry.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal class BootValidationEntry + { + private readonly byte[] _data; + public byte HeaderId; + public string ManfId; + public byte PlatformId; + + public BootValidationEntry() + { + HeaderId = 1; + PlatformId = 0; + ManfId = ".Net DiscUtils"; + } + + public BootValidationEntry(byte[] src, int offset) + { + _data = new byte[32]; + Array.Copy(src, offset, _data, 0, 32); + + HeaderId = _data[0]; + PlatformId = _data[1]; + ManfId = EndianUtilities.BytesToString(_data, 4, 24).TrimEnd('\0').TrimEnd(' '); + } + + public bool ChecksumValid + { + get + { + ushort total = 0; + for (int i = 0; i < 16; ++i) + { + total += EndianUtilities.ToUInt16LittleEndian(_data, i * 2); + } + + return total == 0; + } + } + + internal void WriteTo(byte[] buffer, int offset) + { + Array.Clear(buffer, offset, 0x20); + buffer[offset + 0x00] = HeaderId; + buffer[offset + 0x01] = PlatformId; + EndianUtilities.StringToBytes(ManfId, buffer, offset + 0x04, 24); + buffer[offset + 0x1E] = 0x55; + buffer[offset + 0x1F] = 0xAA; + EndianUtilities.WriteBytesLittleEndian(CalcChecksum(buffer, offset), buffer, offset + 0x1C); + } + + private static ushort CalcChecksum(byte[] buffer, int offset) + { + ushort total = 0; + for (int i = 0; i < 16; ++i) + { + total += EndianUtilities.ToUInt16LittleEndian(buffer, offset + i * 2); + } + + return (ushort)(0 - total); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/BootVolumeDescriptor.cs b/DiscUtils/Iso9660/BootVolumeDescriptor.cs new file mode 100644 index 0000000..d32908a --- /dev/null +++ b/DiscUtils/Iso9660/BootVolumeDescriptor.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal class BootVolumeDescriptor : BaseVolumeDescriptor + { + public const string ElToritoSystemIdentifier = "EL TORITO SPECIFICATION"; + + public BootVolumeDescriptor(uint catalogSector, int sectorSize) + : base(VolumeDescriptorType.Boot, 1, sectorSize) + { + CatalogSector = catalogSector; + } + + public BootVolumeDescriptor(byte[] src, int offset) + : base(src, offset) + { + SystemId = EndianUtilities.BytesToString(src, offset + 0x7, 0x20).TrimEnd('\0'); + CatalogSector = EndianUtilities.ToUInt32LittleEndian(src, offset + 0x47); + } + + public uint CatalogSector { get; } + + public string SystemId { get; } + + internal override void WriteTo(byte[] buffer, int offset) + { + base.WriteTo(buffer, offset); + + EndianUtilities.StringToBytes(ElToritoSystemIdentifier, buffer, offset + 7, 0x20); + EndianUtilities.WriteBytesLittleEndian(CatalogSector, buffer, offset + 0x47); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/BootVolumeDescriptorRegion.cs b/DiscUtils/Iso9660/BootVolumeDescriptorRegion.cs new file mode 100644 index 0000000..8e73eb0 --- /dev/null +++ b/DiscUtils/Iso9660/BootVolumeDescriptorRegion.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal class BootVolumeDescriptorRegion : VolumeDescriptorDiskRegion + { + private readonly BootVolumeDescriptor _descriptor; + + public BootVolumeDescriptorRegion(BootVolumeDescriptor descriptor, long start, int sectorSize) + : base(start, sectorSize) + { + _descriptor = descriptor; + } + + protected override byte[] GetBlockData() + { + byte[] buffer = new byte[Length]; + _descriptor.WriteTo(buffer, 0); + return buffer; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/BuildDirectoryInfo.cs b/DiscUtils/Iso9660/BuildDirectoryInfo.cs new file mode 100644 index 0000000..3212654 --- /dev/null +++ b/DiscUtils/Iso9660/BuildDirectoryInfo.cs @@ -0,0 +1,219 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.Text; +using DiscUtils.CoreCompat; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + /// + /// Represents a directory that will be built into the ISO image. + /// + public sealed class BuildDirectoryInfo : BuildDirectoryMember + { + internal static readonly Comparer PathTableSortComparison = new PathTableComparison(); + private readonly Dictionary _members; + + private readonly BuildDirectoryInfo _parent; + private List _sortedMembers; + private int _sectorSize; + + internal BuildDirectoryInfo(string name, BuildDirectoryInfo parent, int sectorSize) + : base(name, MakeShortDirName(name, parent)) + { + _parent = parent == null ? this : parent; + HierarchyDepth = parent == null ? 0 : parent.HierarchyDepth + 1; + _members = new Dictionary(); + _sectorSize = sectorSize; + } + + internal int HierarchyDepth { get; } + + /// + /// The parent directory, or null if none. + /// + public override BuildDirectoryInfo Parent + { + get { return _parent; } + } + + /// + /// Gets the specified child directory or file. + /// + /// The name of the file or directory to get. + /// The member found (or null). + /// true if the specified member was found. + internal bool TryGetMember(string name, out BuildDirectoryMember member) + { + return _members.TryGetValue(name, out member); + } + + internal void Add(BuildDirectoryMember member) + { + _members.Add(member.Name, member); + _sortedMembers = null; + } + + internal override long GetDataSize(Encoding enc) + { + List sorted = GetSortedMembers(); + + long total = 34 * 2; // Two pseudo entries (self & parent) + + foreach (BuildDirectoryMember m in sorted) + { + uint recordSize = m.GetDirectoryRecordSize(enc); + + // If this record would span a sector boundary, then the current sector is + // zero-padded, and the record goes at the start of the next sector. + if (total % _sectorSize + recordSize > _sectorSize) + { + long padLength = _sectorSize - total % _sectorSize; + total += padLength; + } + + total += recordSize; + } + + return MathUtilities.RoundUp(total, _sectorSize); + } + + internal uint GetPathTableEntrySize(Encoding enc) + { + int nameBytes = enc.GetByteCount(PickName(null, enc)); + + return (uint)(8 + nameBytes + ((nameBytes & 0x1) == 1 ? 1 : 0)); + } + + internal int Write(byte[] buffer, int offset, Dictionary locationTable, Encoding enc) + { + int pos = 0; + + List sorted = GetSortedMembers(); + + // Two pseudo entries, effectively '.' and '..' + pos += WriteMember(this, "\0", Encoding.ASCII, buffer, offset + pos, locationTable, enc, _sectorSize); + pos += WriteMember(_parent, "\x01", Encoding.ASCII, buffer, offset + pos, locationTable, enc, _sectorSize); + + foreach (BuildDirectoryMember m in sorted) + { + uint recordSize = m.GetDirectoryRecordSize(enc); + + if (pos % _sectorSize + recordSize > _sectorSize) + { + int padLength = _sectorSize - pos % _sectorSize; + Array.Clear(buffer, offset + pos, padLength); + pos += padLength; + } + + pos += WriteMember(m, null, enc, buffer, offset + pos, locationTable, enc, _sectorSize); + } + + // Ensure final padding data is zero'd + int finalPadLength = MathUtilities.RoundUp(pos, _sectorSize) - pos; + Array.Clear(buffer, offset + pos, finalPadLength); + + return pos + finalPadLength; + } + + private static int WriteMember(BuildDirectoryMember m, string nameOverride, Encoding nameEnc, byte[] buffer, int offset, Dictionary locationTable, Encoding dataEnc, int sectorSize) + { + DirectoryRecord dr = new DirectoryRecord(); + dr.FileIdentifier = m.PickName(nameOverride, nameEnc); + dr.LocationOfExtent = locationTable[m]; + dr.DataLength = (uint)m.GetDataSize(dataEnc); + dr.RecordingDateAndTime = m.CreationTime; + dr.Flags = m is BuildDirectoryInfo ? FileFlags.Directory : FileFlags.None; + return dr.WriteTo(buffer, offset, nameEnc); + } + + private static string MakeShortDirName(string longName, BuildDirectoryInfo dir) + { + if (IsoUtilities.IsValidDirectoryName(longName)) + { + return longName; + } + + char[] shortNameChars = longName.ToUpper(CultureInfo.InvariantCulture).ToCharArray(); + for (int i = 0; i < shortNameChars.Length; ++i) + { + if (!IsoUtilities.IsValidDChar(shortNameChars[i]) && shortNameChars[i] != '.' && shortNameChars[i] != ';') + { + shortNameChars[i] = '_'; + } + } + + return new string(shortNameChars); + } + + private List GetSortedMembers() + { + if (_sortedMembers == null) + { + List sorted = new List(_members.Values); + sorted.Sort(SortedComparison); + _sortedMembers = sorted; + } + + return _sortedMembers; + } + + private class PathTableComparison : Comparer + { + public override int Compare(BuildDirectoryInfo x, BuildDirectoryInfo y) + { + if (x.HierarchyDepth != y.HierarchyDepth) + { + return x.HierarchyDepth - y.HierarchyDepth; + } + + if (x.Parent != y.Parent) + { + return Compare(x.Parent, y.Parent); + } + + return CompareNames(x.Name, y.Name, ' '); + } + + private static int CompareNames(string x, string y, char padChar) + { + int max = Math.Max(x.Length, y.Length); + for (int i = 0; i < max; ++i) + { + char xChar = i < x.Length ? x[i] : padChar; + char yChar = i < y.Length ? y[i] : padChar; + + if (xChar != yChar) + { + return xChar - yChar; + } + } + + return 0; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/BuildDirectoryMember.cs b/DiscUtils/Iso9660/BuildDirectoryMember.cs new file mode 100644 index 0000000..626bf28 --- /dev/null +++ b/DiscUtils/Iso9660/BuildDirectoryMember.cs @@ -0,0 +1,155 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660 +{ + /// + /// Provides the base class for and + /// objects that will be built into an + /// ISO image. + /// + /// Instances of this class have two names, a , + /// which is the full-length Joliet name and a , + /// which is the strictly compliant ISO 9660 name. + public abstract class BuildDirectoryMember + { + internal static readonly Comparer SortedComparison = new DirectorySortedComparison(); + + /// + /// Initializes a new instance of the BuildDirectoryMember class. + /// + /// The Joliet compliant name of the file or directory. + /// The ISO 9660 compliant name of the file or directory. + protected BuildDirectoryMember(string name, string shortName) + { + Name = name; + ShortName = shortName; + CreationTime = DateTime.UtcNow; + } + + /// + /// Gets or sets the creation date for the file or directory, in UTC. + /// + public DateTime CreationTime { get; set; } + + /// + /// Gets the Joliet compliant name of the file or directory. + /// + public string Name { get; } + + /// + /// Gets the parent directory, or null if this is the root directory. + /// + public abstract BuildDirectoryInfo Parent { get; } + + /// + /// Gets the ISO 9660 compliant name of the file or directory. + /// + public string ShortName { get; } + + internal string PickName(string nameOverride, Encoding enc) + { + if (nameOverride != null) + { + return nameOverride; + } + return enc == Encoding.ASCII ? ShortName : Name; + } + + internal abstract long GetDataSize(Encoding enc); + + internal uint GetDirectoryRecordSize(Encoding enc) + { + return DirectoryRecord.CalcLength(PickName(null, enc), enc); + } + + private class DirectorySortedComparison : Comparer + { + public override int Compare(BuildDirectoryMember x, BuildDirectoryMember y) + { + string[] xParts = x.Name.Split('.', ';'); + string[] yParts = y.Name.Split('.', ';'); + + string xPart; + string yPart; + + for (int i = 0; i < 2; ++i) + { + xPart = xParts.Length > i ? xParts[i] : string.Empty; + yPart = yParts.Length > i ? yParts[i] : string.Empty; + int val = ComparePart(xPart, yPart, ' '); + if (val != 0) + { + return val; + } + } + + xPart = xParts.Length > 2 ? xParts[2] : string.Empty; + yPart = yParts.Length > 2 ? yParts[2] : string.Empty; + return ComparePartBackwards(xPart, yPart, '0'); + } + + private static int ComparePart(string x, string y, char padChar) + { + int max = Math.Max(x.Length, y.Length); + for (int i = 0; i < max; ++i) + { + char xChar = i < x.Length ? x[i] : padChar; + char yChar = i < y.Length ? y[i] : padChar; + + if (xChar != yChar) + { + return xChar - yChar; + } + } + + return 0; + } + + private static int ComparePartBackwards(string x, string y, char padChar) + { + int max = Math.Max(x.Length, y.Length); + + int xPad = max - x.Length; + int yPad = max - y.Length; + + for (int i = 0; i < max; ++i) + { + char xChar = i >= xPad ? x[i - xPad] : padChar; + char yChar = i >= yPad ? y[i - yPad] : padChar; + + if (xChar != yChar) + { + // Note: Version numbers are in DESCENDING order! + return yChar - xChar; + } + } + + return 0; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/BuildFileInfo.cs b/DiscUtils/Iso9660/BuildFileInfo.cs new file mode 100644 index 0000000..9c43280 --- /dev/null +++ b/DiscUtils/Iso9660/BuildFileInfo.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using System.Text; +using DiscUtils.CoreCompat; +using DiscUtils.Internal; + +namespace DiscUtils.Iso9660 +{ + /// + /// Represents a file that will be built into the ISO image. + /// + public sealed class BuildFileInfo : BuildDirectoryMember + { + private readonly byte[] _contentData; + private readonly string _contentPath; + private readonly long _contentSize; + private readonly Stream _contentStream; + + internal BuildFileInfo(string name, BuildDirectoryInfo parent, byte[] content) + : base(IsoUtilities.NormalizeFileName(name), MakeShortFileName(name, parent)) + { + Parent = parent; + _contentData = content; + _contentSize = content.Length; + } + + internal BuildFileInfo(string name, BuildDirectoryInfo parent, string content) + : base(IsoUtilities.NormalizeFileName(name), MakeShortFileName(name, parent)) + { + Parent = parent; + _contentPath = content; + _contentSize = new FileInfo(_contentPath).Length; + + CreationTime = new FileInfo(_contentPath).LastWriteTimeUtc; + } + + internal BuildFileInfo(string name, BuildDirectoryInfo parent, Stream source) + : base(IsoUtilities.NormalizeFileName(name), MakeShortFileName(name, parent)) + { + Parent = parent; + _contentStream = source; + _contentSize = _contentStream.Length; + } + + /// + /// The parent directory, or null if none. + /// + public override BuildDirectoryInfo Parent { get; } + + internal override long GetDataSize(Encoding enc) + { + return _contentSize; + } + + internal Stream OpenStream() + { + if (_contentData != null) + { + return new MemoryStream(_contentData, false); + } + if (_contentPath != null) + { + var locator = new LocalFileLocator(string.Empty); + return locator.Open(_contentPath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + return _contentStream; + } + + internal void CloseStream(Stream s) + { + // Close and dispose the stream, unless it's one we were given to stream in + // from (we might need it again). + if (_contentStream != s) + { + s.Dispose(); + } + } + + private static string MakeShortFileName(string longName, BuildDirectoryInfo dir) + { + if (IsoUtilities.IsValidFileName(longName)) + { + return longName; + } + + char[] shortNameChars = longName.ToUpper(CultureInfo.InvariantCulture).ToCharArray(); + for (int i = 0; i < shortNameChars.Length; ++i) + { + if (!IsoUtilities.IsValidDChar(shortNameChars[i]) && shortNameChars[i] != '.' && shortNameChars[i] != ';') + { + shortNameChars[i] = '_'; + } + } + + string[] parts = IsoUtilities.SplitFileName(new string(shortNameChars)); + + if (parts[0].Length + parts[1].Length > 30) + { + parts[1] = parts[1].Substring(0, Math.Min(parts[1].Length, 3)); + } + + if (parts[0].Length + parts[1].Length > 30) + { + parts[0] = parts[0].Substring(0, 30 - parts[1].Length); + } + + string candidate = parts[0] + '.' + parts[1] + ';' + parts[2]; + + // TODO: Make unique + return candidate; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/BuildParameters.cs b/DiscUtils/Iso9660/BuildParameters.cs new file mode 100644 index 0000000..63f7658 --- /dev/null +++ b/DiscUtils/Iso9660/BuildParameters.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal class BuildParameters + { + public BuildParameters() + { + VolumeIdentifier = string.Empty; + UseJoliet = true; + SectorSize = 2048; + } + + public int SectorSize { get; set; } + + public bool UseJoliet { get; set; } + + public string VolumeIdentifier { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/CDBuilder.cs b/DiscUtils/Iso9660/CDBuilder.cs new file mode 100644 index 0000000..0d7a275 --- /dev/null +++ b/DiscUtils/Iso9660/CDBuilder.cs @@ -0,0 +1,518 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + /// + /// Class that creates ISO images. + /// + /// + /// + /// CDBuilder builder = new CDBuilder(); + /// builder.VolumeIdentifier = "MYISO"; + /// builder.UseJoliet = true; + /// builder.AddFile("Hello.txt", Encoding.ASCII.GetBytes("hello world!")); + /// builder.Build(@"C:\TEMP\myiso.iso"); + /// + /// + public sealed class CDBuilder : StreamBuilder + { + private const long DiskStart = 0x8000; + private BootInitialEntry _bootEntry; + private Stream _bootImage; + + private readonly BuildParameters _buildParams; + private readonly List _dirs; + + private readonly List _files; + private readonly BuildDirectoryInfo _rootDirectory; + private readonly int _sectorSize; + + /// + /// Initializes a new instance of the CDBuilder class. + /// + public CDBuilder() + { + _files = new List(); + _dirs = new List(); + _sectorSize = 2048; + _rootDirectory = new BuildDirectoryInfo("\0", null, _sectorSize); + _dirs.Add(_rootDirectory); + + _buildParams = new BuildParameters(); + _buildParams.UseJoliet = true; + } + + + /// + /// Initializes a new instance of the CDBuilder class. + /// + public CDBuilder(int sectorSize) + { + _files = new List(); + _dirs = new List(); + _sectorSize = sectorSize; + _rootDirectory = new BuildDirectoryInfo("\0", null, _sectorSize); + _dirs.Add(_rootDirectory); + + _buildParams = new BuildParameters(); + _buildParams.UseJoliet = true; + } + + /// + /// Gets or sets a value indicating whether to update the ISOLINUX info table at the + /// start of the boot image. Use with ISOLINUX only. + /// + /// + /// ISOLINUX has an 'information table' at the start of the boot loader that verifies + /// the CD has been loaded correctly by the BIOS. This table needs to be updated + /// to match the actual ISO. + /// + public bool UpdateIsolinuxBootTable { get; set; } + + /// + /// Gets or sets a value indicating whether Joliet file-system extensions should be used. + /// + public bool UseJoliet + { + get { return _buildParams.UseJoliet; } + set { _buildParams.UseJoliet = value; } + } + + /// + /// Gets or sets the Volume Identifier for the ISO file. + /// + /// + /// Must be a valid identifier, i.e. max 32 characters in the range A-Z, 0-9 or _. + /// Lower-case characters are not permitted. + /// + public string VolumeIdentifier + { + get { return _buildParams.VolumeIdentifier; } + + set + { + if (value.Length > 32) + { + throw new ArgumentException("Not a valid volume identifier"); + } + _buildParams.VolumeIdentifier = value; + } + } + + /// + /// Sets the boot image for the ISO image. + /// + /// Stream containing the boot image. + /// The type of emulation requested of the BIOS. + /// The memory segment to load the image to (0 for default). + public void SetBootImage(Stream image, BootDeviceEmulation emulation, int loadSegment) + { + if (_bootEntry != null) + { + throw new InvalidOperationException("Boot image already set"); + } + + _bootEntry = new BootInitialEntry(); + _bootEntry.BootIndicator = 0x88; + _bootEntry.BootMediaType = emulation; + _bootEntry.LoadSegment = (ushort)loadSegment; + _bootEntry.SystemType = 0; + _bootImage = image; + } + + /// + /// Adds a directory to the ISO image. + /// + /// The name of the directory on the ISO image. + /// The object representing this directory. + /// + /// The name is the full path to the directory, for example: + /// + /// builder.AddDirectory(@"DIRA\DIRB\DIRC"); + /// + /// + public BuildDirectoryInfo AddDirectory(string name) + { + string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + return GetDirectory(nameElements, nameElements.Length, true); + } + + /// + /// Adds a byte array to the ISO image as a file. + /// + /// The name of the file on the ISO image. + /// The contents of the file. + /// The object representing this file. + /// + /// The name is the full path to the file, for example: + /// + /// builder.AddFile(@"DIRA\DIRB\FILE.TXT;1", new byte[]{0,1,2}); + /// + /// Note the version number at the end of the file name is optional, if not + /// specified the default of 1 will be used. + /// + public BuildFileInfo AddFile(string name, byte[] content) + { + string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + BuildDirectoryInfo dir = GetDirectory(nameElements, nameElements.Length - 1, true); + + BuildDirectoryMember existing; + if (dir.TryGetMember(nameElements[nameElements.Length - 1], out existing)) + { + throw new IOException("File already exists"); + } + BuildFileInfo fi = new BuildFileInfo(nameElements[nameElements.Length - 1], dir, content); + _files.Add(fi); + dir.Add(fi); + return fi; + } + + /// + /// Adds a disk file to the ISO image as a file. + /// + /// The name of the file on the ISO image. + /// The name of the file on disk. + /// The object representing this file. + /// + /// The name is the full path to the file, for example: + /// + /// builder.AddFile(@"DIRA\DIRB\FILE.TXT;1", @"C:\temp\tempfile.bin"); + /// + /// Note the version number at the end of the file name is optional, if not + /// specified the default of 1 will be used. + /// + public BuildFileInfo AddFile(string name, string sourcePath) + { + string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + BuildDirectoryInfo dir = GetDirectory(nameElements, nameElements.Length - 1, true); + + BuildDirectoryMember existing; + if (dir.TryGetMember(nameElements[nameElements.Length - 1], out existing)) + { + throw new IOException("File already exists"); + } + BuildFileInfo fi = new BuildFileInfo(nameElements[nameElements.Length - 1], dir, sourcePath); + _files.Add(fi); + dir.Add(fi); + return fi; + } + + /// + /// Adds a stream to the ISO image as a file. + /// + /// The name of the file on the ISO image. + /// The contents of the file. + /// The object representing this file. + /// + /// The name is the full path to the file, for example: + /// + /// builder.AddFile(@"DIRA\DIRB\FILE.TXT;1", stream); + /// + /// Note the version number at the end of the file name is optional, if not + /// specified the default of 1 will be used. + /// + public BuildFileInfo AddFile(string name, Stream source) + { + if (!source.CanSeek) + { + throw new ArgumentException("source doesn't support seeking", nameof(source)); + } + + string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + BuildDirectoryInfo dir = GetDirectory(nameElements, nameElements.Length - 1, true); + + BuildDirectoryMember existing; + if (dir.TryGetMember(nameElements[nameElements.Length - 1], out existing)) + { + throw new IOException("File already exists"); + } + BuildFileInfo fi = new BuildFileInfo(nameElements[nameElements.Length - 1], dir, source); + _files.Add(fi); + dir.Add(fi); + return fi; + } + + protected override List FixExtents(out long totalLength) + { + List fixedRegions = new List(); + + DateTime buildTime = DateTime.UtcNow; + + Encoding suppEncoding = _buildParams.UseJoliet ? Encoding.BigEndianUnicode : Encoding.ASCII; + + Dictionary primaryLocationTable = new Dictionary(); + Dictionary supplementaryLocationTable = + new Dictionary(); + + long focus = DiskStart + 3 * _buildParams.SectorSize; // Primary, Supplementary, End (fixed at end...) + if (_bootEntry != null) + { + focus += _buildParams.SectorSize; + } + + // #################################################################### + // # 0. Fix boot image location + // #################################################################### + long bootCatalogPos = 0; + if (_bootEntry != null) + { + long bootImagePos = focus; + Stream realBootImage = PatchBootImage(_bootImage, (uint)(DiskStart / _buildParams.SectorSize), + (uint)(bootImagePos / _buildParams.SectorSize)); + BuilderStreamExtent bootImageExtent = new BuilderStreamExtent(focus, realBootImage); + fixedRegions.Add(bootImageExtent); + focus += MathUtilities.RoundUp(bootImageExtent.Length, _buildParams.SectorSize); + + bootCatalogPos = focus; + byte[] bootCatalog = new byte[_buildParams.SectorSize]; + BootValidationEntry bve = new BootValidationEntry(); + bve.WriteTo(bootCatalog, 0x00); + _bootEntry.ImageStart = (uint)MathUtilities.Ceil(bootImagePos, _buildParams.SectorSize); + _bootEntry.SectorCount = (ushort)MathUtilities.Ceil(_bootImage.Length, Sizes.Sector); + _bootEntry.WriteTo(bootCatalog, 0x20); + fixedRegions.Add(new BuilderBufferExtent(bootCatalogPos, bootCatalog)); + focus += _buildParams.SectorSize; + } + + // #################################################################### + // # 1. Fix file locations + // #################################################################### + + // Find end of the file data, fixing the files in place as we go + foreach (BuildFileInfo fi in _files) + { + primaryLocationTable.Add(fi, (uint)(focus / _buildParams.SectorSize)); + supplementaryLocationTable.Add(fi, (uint)(focus / _buildParams.SectorSize)); + FileExtent extent = new FileExtent(fi, focus); + + // Only remember files of non-zero length (otherwise we'll stomp on a valid file) + if (extent.Length != 0) + { + fixedRegions.Add(extent); + } + + focus += MathUtilities.RoundUp(extent.Length, _buildParams.SectorSize); + } + + // #################################################################### + // # 2. Fix directory locations + // #################################################################### + + // There are two directory tables + // 1. Primary (std ISO9660) + // 2. Supplementary (Joliet) + + // Find start of the second set of directory data, fixing ASCII directories in place. + long startOfFirstDirData = focus; + foreach (BuildDirectoryInfo di in _dirs) + { + primaryLocationTable.Add(di, (uint)(focus / _buildParams.SectorSize)); + DirectoryExtent extent = new DirectoryExtent(di, primaryLocationTable, Encoding.ASCII, focus); + fixedRegions.Add(extent); + focus += MathUtilities.RoundUp(extent.Length, _buildParams.SectorSize); + } + + // Find end of the second directory table, fixing supplementary directories in place. + long startOfSecondDirData = focus; + foreach (BuildDirectoryInfo di in _dirs) + { + supplementaryLocationTable.Add(di, (uint)(focus / _buildParams.SectorSize)); + DirectoryExtent extent = new DirectoryExtent(di, supplementaryLocationTable, suppEncoding, focus); + fixedRegions.Add(extent); + focus += MathUtilities.RoundUp(extent.Length, _buildParams.SectorSize); + } + + // #################################################################### + // # 3. Fix path tables + // #################################################################### + + // There are four path tables: + // 1. LE, ASCII + // 2. BE, ASCII + // 3. LE, Supp Encoding (Joliet) + // 4. BE, Supp Encoding (Joliet) + + // Find end of the path table + long startOfFirstPathTable = focus; + PathTable pathTable = new PathTable(false, Encoding.ASCII, _dirs, primaryLocationTable, focus); + fixedRegions.Add(pathTable); + focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + long primaryPathTableLength = pathTable.Length; + + long startOfSecondPathTable = focus; + pathTable = new PathTable(true, Encoding.ASCII, _dirs, primaryLocationTable, focus); + fixedRegions.Add(pathTable); + focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + + long startOfThirdPathTable = focus; + pathTable = new PathTable(false, suppEncoding, _dirs, supplementaryLocationTable, focus); + fixedRegions.Add(pathTable); + focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + long supplementaryPathTableLength = pathTable.Length; + + long startOfFourthPathTable = focus; + pathTable = new PathTable(true, suppEncoding, _dirs, supplementaryLocationTable, focus); + fixedRegions.Add(pathTable); + focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + + // Find the end of the disk + totalLength = focus; + + // #################################################################### + // # 4. Prepare volume descriptors now other structures are fixed + // #################################################################### + int regionIdx = 0; + focus = DiskStart; + PrimaryVolumeDescriptor pvDesc = new PrimaryVolumeDescriptor( + (uint)(totalLength / _buildParams.SectorSize), // VolumeSpaceSize + (uint)primaryPathTableLength, // PathTableSize + (uint)(startOfFirstPathTable / _buildParams.SectorSize), // TypeLPathTableLocation + (uint)(startOfSecondPathTable / _buildParams.SectorSize), // TypeMPathTableLocation + (uint)(startOfFirstDirData / _buildParams.SectorSize), // RootDirectory.LocationOfExtent + (uint)_rootDirectory.GetDataSize(Encoding.ASCII), // RootDirectory.DataLength + buildTime, + _sectorSize); + pvDesc.VolumeIdentifier = _buildParams.VolumeIdentifier; + PrimaryVolumeDescriptorRegion pvdr = new PrimaryVolumeDescriptorRegion(pvDesc, focus, _sectorSize); + fixedRegions.Insert(regionIdx++, pvdr); + focus += _buildParams.SectorSize; + + if (_bootEntry != null) + { + BootVolumeDescriptor bvDesc = new BootVolumeDescriptor( + (uint)(bootCatalogPos / _buildParams.SectorSize), _buildParams.SectorSize); + BootVolumeDescriptorRegion bvdr = new BootVolumeDescriptorRegion(bvDesc, focus, _buildParams.SectorSize); + fixedRegions.Insert(regionIdx++, bvdr); + focus += _buildParams.SectorSize; + } + + SupplementaryVolumeDescriptor svDesc = new SupplementaryVolumeDescriptor( + (uint)(totalLength / _buildParams.SectorSize), // VolumeSpaceSize + (uint)supplementaryPathTableLength, // PathTableSize + (uint)(startOfThirdPathTable / _buildParams.SectorSize), // TypeLPathTableLocation + (uint)(startOfFourthPathTable / _buildParams.SectorSize), // TypeMPathTableLocation + (uint)(startOfSecondDirData / _buildParams.SectorSize), // RootDirectory.LocationOfExtent + (uint)_rootDirectory.GetDataSize(suppEncoding), // RootDirectory.DataLength + buildTime, + suppEncoding, + _sectorSize); + svDesc.VolumeIdentifier = _buildParams.VolumeIdentifier; + SupplementaryVolumeDescriptorRegion svdr = new SupplementaryVolumeDescriptorRegion(svDesc, focus, _sectorSize); + fixedRegions.Insert(regionIdx++, svdr); + focus += _buildParams.SectorSize; + + VolumeDescriptorSetTerminator evDesc = new VolumeDescriptorSetTerminator(_sectorSize); + VolumeDescriptorSetTerminatorRegion evdr = new VolumeDescriptorSetTerminatorRegion(evDesc, focus, _sectorSize); + fixedRegions.Insert(regionIdx++, evdr); + + return fixedRegions; + } + + /// + /// Patches a boot image (esp. for ISOLINUX) before it is written to the disk. + /// + /// The original (master) boot image. + /// The logical block address of the primary volume descriptor. + /// The logical block address of the boot image itself. + /// A stream containing the patched boot image - does not need to be disposed. + private Stream PatchBootImage(Stream bootImage, uint pvdLba, uint bootImageLba) + { + // Early-exit if no patching to do... + if (!UpdateIsolinuxBootTable) + { + return bootImage; + } + + byte[] bootData = StreamUtilities.ReadExact(bootImage, (int)bootImage.Length); + + Array.Clear(bootData, 8, 56); + + uint checkSum = 0; + for (int i = 64; i < bootData.Length; i += 4) + { + checkSum += EndianUtilities.ToUInt32LittleEndian(bootData, i); + } + + EndianUtilities.WriteBytesLittleEndian(pvdLba, bootData, 8); + EndianUtilities.WriteBytesLittleEndian(bootImageLba, bootData, 12); + EndianUtilities.WriteBytesLittleEndian(bootData.Length, bootData, 16); + EndianUtilities.WriteBytesLittleEndian(checkSum, bootData, 20); + + return new MemoryStream(bootData, false); + } + + private BuildDirectoryInfo GetDirectory(string[] path, int pathLength, bool createMissing) + { + BuildDirectoryInfo di = TryGetDirectory(path, pathLength, createMissing); + + if (di == null) + { + throw new DirectoryNotFoundException("Directory not found"); + } + + return di; + } + + private BuildDirectoryInfo TryGetDirectory(string[] path, int pathLength, bool createMissing) + { + BuildDirectoryInfo focus = _rootDirectory; + + for (int i = 0; i < pathLength; ++i) + { + BuildDirectoryMember next; + if (!focus.TryGetMember(path[i], out next)) + { + if (createMissing) + { + // This directory doesn't exist, create it... + BuildDirectoryInfo di = new BuildDirectoryInfo(path[i], focus, _sectorSize); + focus.Add(di); + _dirs.Add(di); + focus = di; + } + else + { + return null; + } + } + else + { + BuildDirectoryInfo nextAsBuildDirectoryInfo = next as BuildDirectoryInfo; + if (nextAsBuildDirectoryInfo == null) + { + throw new IOException("File with conflicting name exists"); + } + focus = nextAsBuildDirectoryInfo; + } + } + + return focus; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/CDReader.cs b/DiscUtils/Iso9660/CDReader.cs new file mode 100644 index 0000000..82bfe41 --- /dev/null +++ b/DiscUtils/Iso9660/CDReader.cs @@ -0,0 +1,229 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Streams; +using DiscUtils.Vfs; + +namespace DiscUtils.Iso9660 +{ + /// + /// Class for reading existing ISO images. + /// + public class CDReader : VfsFileSystemFacade, IClusterBasedFileSystem, IUnixFileSystem + { + /// + /// Initializes a new instance of the CDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + public CDReader(Stream data, bool joliet) + : base(new VfsCDReader(data, joliet, false, 2048)) + { + } + + /// + /// Initializes a new instance of the CDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + /// Hides version numbers (e.g. ";1") from the end of files. + public CDReader(Stream data, bool joliet, bool hideVersions) + : base(new VfsCDReader(data, joliet, hideVersions, 2048)) { } + + + /// + /// Initializes a new instance of the CDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + /// The size of a sector + public CDReader(Stream data, bool joliet, int sectorSize) + : base(new VfsCDReader(data, joliet, false, sectorSize)) { + } + + /// + /// Initializes a new instance of the CDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + /// Hides version numbers (e.g. ";1") from the end of files. + public CDReader(Stream data, bool joliet, bool hideVersions, int sectorSize) + : base(new VfsCDReader(data, joliet, hideVersions, sectorSize)) {} + + /// + /// Gets which of the Iso9660 variants is being used. + /// + public Iso9660Variant ActiveVariant + { + get { return GetRealFileSystem().ActiveVariant; } + } + + /// + /// Gets the emulation requested of BIOS when the image is loaded. + /// + public BootDeviceEmulation BootEmulation + { + get { return GetRealFileSystem().BootEmulation; } + } + + /// + /// Gets the absolute start position (in bytes) of the boot image, or zero if not found. + /// + public long BootImageStart + { + get { return GetRealFileSystem().BootImageStart; } + } + + /// + /// Gets the memory segment the image should be loaded into (0 for default). + /// + public int BootLoadSegment + { + get { return GetRealFileSystem().BootLoadSegment; } + } + + /// + /// Gets a value indicating whether a boot image is present. + /// + public bool HasBootImage + { + get { return GetRealFileSystem().HasBootImage; } + } + + /// + /// Gets the size (in bytes) of each cluster. + /// + public long ClusterSize + { + get { return GetRealFileSystem().ClusterSize; } + } + + /// + /// Gets the total number of clusters managed by the file system. + /// + public long TotalClusters + { + get { return GetRealFileSystem().TotalClusters; } + } + + /// + /// Converts a cluster (index) into an absolute byte position in the underlying stream. + /// + /// The cluster to convert. + /// The corresponding absolute byte position. + public long ClusterToOffset(long cluster) + { + return GetRealFileSystem().ClusterToOffset(cluster); + } + + /// + /// Converts an absolute byte position in the underlying stream to a cluster (index). + /// + /// The byte position to convert. + /// The cluster containing the specified byte. + public long OffsetToCluster(long offset) + { + return GetRealFileSystem().OffsetToCluster(offset); + } + + /// + /// Converts a file name to the list of clusters occupied by the file's data. + /// + /// The path to inspect. + /// The clusters. + /// Note that in some file systems, small files may not have dedicated + /// clusters. Only dedicated clusters will be returned. + public Range[] PathToClusters(string path) + { + return GetRealFileSystem().PathToClusters(path); + } + + /// + /// Converts a file name to the extents containing its data. + /// + /// The path to inspect. + /// The file extents, as absolute byte positions in the underlying stream. + /// Use this method with caution - not all file systems will store all bytes + /// directly in extents. Files may be compressed, sparse or encrypted. This method + /// merely indicates where file data is stored, not what's stored. + public StreamExtent[] PathToExtents(string path) + { + return GetRealFileSystem().PathToExtents(path); + } + + /// + /// Gets an object that can convert between clusters and files. + /// + /// The cluster map. + public ClusterMap BuildClusterMap() + { + return GetRealFileSystem().BuildClusterMap(); + } + + /// + /// Retrieves Unix-specific information about a file or directory. + /// + /// Path to the file or directory. + /// Information about the owner, group, permissions and type of the + /// file or directory. + public UnixFileSystemInfo GetUnixFileInfo(string path) + { + return GetRealFileSystem().GetUnixFileInfo(path); + } + + /// + /// Detects if a stream contains a valid ISO file system. + /// + /// The stream to inspect. + /// true if the stream contains an ISO file system, else false. + public static bool Detect(Stream data, int sectorSize) + { + byte[] buffer = new byte[sectorSize]; + + if (data.Length < 0x8000 + sectorSize) + { + return false; + } + + data.Position = 0x8000; + int numRead = StreamUtilities.ReadMaximum(data, buffer, 0, sectorSize); + if (numRead != sectorSize) + { + return false; + } + + BaseVolumeDescriptor bvd = new BaseVolumeDescriptor(buffer, 0); + + return bvd.StandardIdentifier == BaseVolumeDescriptor.Iso9660StandardIdentifier; + } + + /// + /// Opens a stream containing the boot image. + /// + /// The boot image as a stream. + public Stream OpenBootImage() + { + return GetRealFileSystem().OpenBootImage(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/CommonVolumeDescriptor.cs b/DiscUtils/Iso9660/CommonVolumeDescriptor.cs new file mode 100644 index 0000000..f4a4e0a --- /dev/null +++ b/DiscUtils/Iso9660/CommonVolumeDescriptor.cs @@ -0,0 +1,141 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal class CommonVolumeDescriptor : BaseVolumeDescriptor + { + public string AbstractFileIdentifier; + public string ApplicationIdentifier; + public string BibliographicFileIdentifier; + public Encoding CharacterEncoding; + public string CopyrightFileIdentifier; + public DateTime CreationDateAndTime; + public string DataPreparerIdentifier; + public DateTime EffectiveDateAndTime; + public DateTime ExpirationDateAndTime; + public byte FileStructureVersion; + public ushort LogicalBlockSize; + public DateTime ModificationDateAndTime; + public uint OptionalTypeLPathTableLocation; + public uint OptionalTypeMPathTableLocation; + public uint PathTableSize; + public string PublisherIdentifier; + public DirectoryRecord RootDirectory; + + public string SystemIdentifier; + public uint TypeLPathTableLocation; + public uint TypeMPathTableLocation; + public string VolumeIdentifier; + public ushort VolumeSequenceNumber; + public string VolumeSetIdentifier; + public ushort VolumeSetSize; + public uint VolumeSpaceSize; + + public CommonVolumeDescriptor(byte[] src, int offset, Encoding enc) + : base(src, offset) + { + CharacterEncoding = enc; + + SystemIdentifier = IsoUtilities.ReadChars(src, offset + 8, 32, CharacterEncoding); + VolumeIdentifier = IsoUtilities.ReadChars(src, offset + 40, 32, CharacterEncoding); + VolumeSpaceSize = IsoUtilities.ToUInt32FromBoth(src, offset + 80); + VolumeSetSize = IsoUtilities.ToUInt16FromBoth(src, offset + 120); + VolumeSequenceNumber = IsoUtilities.ToUInt16FromBoth(src, offset + 124); + LogicalBlockSize = IsoUtilities.ToUInt16FromBoth(src, offset + 128); + PathTableSize = IsoUtilities.ToUInt32FromBoth(src, offset + 132); + TypeLPathTableLocation = EndianUtilities.ToUInt32LittleEndian(src, offset + 140); + OptionalTypeLPathTableLocation = EndianUtilities.ToUInt32LittleEndian(src, offset + 144); + TypeMPathTableLocation = Utilities.BitSwap(EndianUtilities.ToUInt32LittleEndian(src, offset + 148)); + OptionalTypeMPathTableLocation = Utilities.BitSwap(EndianUtilities.ToUInt32LittleEndian(src, offset + 152)); + DirectoryRecord.ReadFrom(src, offset + 156, CharacterEncoding, out RootDirectory); + VolumeSetIdentifier = IsoUtilities.ReadChars(src, offset + 190, 318 - 190, CharacterEncoding); + PublisherIdentifier = IsoUtilities.ReadChars(src, offset + 318, 446 - 318, CharacterEncoding); + DataPreparerIdentifier = IsoUtilities.ReadChars(src, offset + 446, 574 - 446, CharacterEncoding); + ApplicationIdentifier = IsoUtilities.ReadChars(src, offset + 574, 702 - 574, CharacterEncoding); + CopyrightFileIdentifier = IsoUtilities.ReadChars(src, offset + 702, 739 - 702, CharacterEncoding); + AbstractFileIdentifier = IsoUtilities.ReadChars(src, offset + 739, 776 - 739, CharacterEncoding); + BibliographicFileIdentifier = IsoUtilities.ReadChars(src, offset + 776, 813 - 776, CharacterEncoding); + CreationDateAndTime = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(src, offset + 813); + ModificationDateAndTime = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(src, offset + 830); + ExpirationDateAndTime = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(src, offset + 847); + EffectiveDateAndTime = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(src, offset + 864); + FileStructureVersion = src[offset + 881]; + } + + public CommonVolumeDescriptor( + VolumeDescriptorType type, + byte version, + uint volumeSpaceSize, + uint pathTableSize, + uint typeLPathTableLocation, + uint typeMPathTableLocation, + uint rootDirExtentLocation, + uint rootDirDataLength, + DateTime buildTime, + Encoding enc, + int sectorSize) + : base(type, version, sectorSize) + { + CharacterEncoding = enc; + + SystemIdentifier = string.Empty; + VolumeIdentifier = string.Empty; + VolumeSpaceSize = volumeSpaceSize; + VolumeSetSize = 1; + VolumeSequenceNumber = 1; + LogicalBlockSize = (ushort)sectorSize; + PathTableSize = pathTableSize; + TypeLPathTableLocation = typeLPathTableLocation; + ////OptionalTypeLPathTableLocation = 0; + TypeMPathTableLocation = typeMPathTableLocation; + ////OptionalTypeMPathTableLocation = 0; + RootDirectory = new DirectoryRecord(); + RootDirectory.ExtendedAttributeRecordLength = 0; + RootDirectory.LocationOfExtent = rootDirExtentLocation; + RootDirectory.DataLength = rootDirDataLength; + RootDirectory.RecordingDateAndTime = buildTime; + RootDirectory.Flags = FileFlags.Directory; + RootDirectory.FileUnitSize = 0; + RootDirectory.InterleaveGapSize = 0; + RootDirectory.VolumeSequenceNumber = 1; + RootDirectory.FileIdentifier = "\0"; + VolumeSetIdentifier = string.Empty; + PublisherIdentifier = string.Empty; + DataPreparerIdentifier = string.Empty; + ApplicationIdentifier = string.Empty; + CopyrightFileIdentifier = string.Empty; + AbstractFileIdentifier = string.Empty; + BibliographicFileIdentifier = string.Empty; + CreationDateAndTime = buildTime; + ModificationDateAndTime = buildTime; + ExpirationDateAndTime = DateTime.MinValue; + EffectiveDateAndTime = buildTime; + FileStructureVersion = 1; // V1 + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/DirectoryExtent.cs b/DiscUtils/Iso9660/DirectoryExtent.cs new file mode 100644 index 0000000..138b6a6 --- /dev/null +++ b/DiscUtils/Iso9660/DirectoryExtent.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal class DirectoryExtent : BuilderExtent + { + private readonly BuildDirectoryInfo _dirInfo; + private readonly Encoding _enc; + private readonly Dictionary _locationTable; + + private byte[] _readCache; + + public DirectoryExtent(BuildDirectoryInfo dirInfo, Dictionary locationTable, + Encoding enc, long start) + : base(start, dirInfo.GetDataSize(enc)) + { + _dirInfo = dirInfo; + _locationTable = locationTable; + _enc = enc; + } + + public override void Dispose() {} + + public override void PrepareForRead() + { + _readCache = new byte[Length]; + _dirInfo.Write(_readCache, 0, _locationTable, _enc); + } + + public override int Read(long diskOffset, byte[] buffer, int offset, int count) + { + long relPos = diskOffset - Start; + + int numRead = (int)Math.Min(count, _readCache.Length - relPos); + + Array.Copy(_readCache, (int)relPos, buffer, offset, numRead); + + return numRead; + } + + public override void DisposeReadState() + { + _readCache = null; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/DirectoryRecord.cs b/DiscUtils/Iso9660/DirectoryRecord.cs new file mode 100644 index 0000000..18fb192 --- /dev/null +++ b/DiscUtils/Iso9660/DirectoryRecord.cs @@ -0,0 +1,114 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660 +{ + internal class DirectoryRecord + { + public uint DataLength; + public byte ExtendedAttributeRecordLength; + public string FileIdentifier; + public byte FileUnitSize; + public FileFlags Flags; + public byte InterleaveGapSize; + public uint LocationOfExtent; + public DateTime RecordingDateAndTime; + public byte[] SystemUseData; + public ushort VolumeSequenceNumber; + + public static int ReadFrom(byte[] src, int offset, Encoding enc, out DirectoryRecord record) + { + int length = src[offset + 0]; + + record = new DirectoryRecord(); + record.ExtendedAttributeRecordLength = src[offset + 1]; + record.LocationOfExtent = IsoUtilities.ToUInt32FromBoth(src, offset + 2); + record.DataLength = IsoUtilities.ToUInt32FromBoth(src, offset + 10); + record.RecordingDateAndTime = IsoUtilities.ToUTCDateTimeFromDirectoryTime(src, offset + 18); + record.Flags = (FileFlags)src[offset + 25]; + record.FileUnitSize = src[offset + 26]; + record.InterleaveGapSize = src[offset + 27]; + record.VolumeSequenceNumber = IsoUtilities.ToUInt16FromBoth(src, offset + 28); + byte lengthOfFileIdentifier = src[offset + 32]; + record.FileIdentifier = IsoUtilities.ReadChars(src, offset + 33, lengthOfFileIdentifier, enc); + + int padding = (lengthOfFileIdentifier & 1) == 0 ? 1 : 0; + int startSystemArea = lengthOfFileIdentifier + padding + 33; + int lenSystemArea = length - startSystemArea; + if (lenSystemArea > 0) + { + record.SystemUseData = new byte[lenSystemArea]; + Array.Copy(src, offset + startSystemArea, record.SystemUseData, 0, lenSystemArea); + } + + return length; + } + + public static uint CalcLength(string name, Encoding enc) + { + int nameBytes; + if (name.Length == 1 && name[0] <= 1) + { + nameBytes = 1; + } + else + { + nameBytes = enc.GetByteCount(name); + } + + return (uint)(33 + nameBytes + ((nameBytes & 0x1) == 0 ? 1 : 0)); + } + + internal int WriteTo(byte[] buffer, int offset, Encoding enc) + { + uint length = CalcLength(FileIdentifier, enc); + buffer[offset] = (byte)length; + buffer[offset + 1] = ExtendedAttributeRecordLength; + IsoUtilities.ToBothFromUInt32(buffer, offset + 2, LocationOfExtent); + IsoUtilities.ToBothFromUInt32(buffer, offset + 10, DataLength); + IsoUtilities.ToDirectoryTimeFromUTC(buffer, offset + 18, RecordingDateAndTime); + buffer[offset + 25] = (byte)Flags; + buffer[offset + 26] = FileUnitSize; + buffer[offset + 27] = InterleaveGapSize; + IsoUtilities.ToBothFromUInt16(buffer, offset + 28, VolumeSequenceNumber); + byte lengthOfFileIdentifier; + + if (FileIdentifier.Length == 1 && FileIdentifier[0] <= 1) + { + buffer[offset + 33] = (byte)FileIdentifier[0]; + lengthOfFileIdentifier = 1; + } + else + { + lengthOfFileIdentifier = + (byte) + IsoUtilities.WriteString(buffer, offset + 33, (int)(length - 33), false, FileIdentifier, enc); + } + + buffer[offset + 32] = lengthOfFileIdentifier; + return (int)length; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/DiscUtils.Iso9660.csproj b/DiscUtils/Iso9660/DiscUtils.Iso9660.csproj new file mode 100644 index 0000000..e9968cf --- /dev/null +++ b/DiscUtils/Iso9660/DiscUtils.Iso9660.csproj @@ -0,0 +1,14 @@ + + + + + DiscUtils Iso9660 + Kenneth Bell;LordMike + DiscUtils;Optical;Iso9660 + + + + + + + diff --git a/DiscUtils/Iso9660/ExtentStream.cs b/DiscUtils/Iso9660/ExtentStream.cs new file mode 100644 index 0000000..13ec3c2 --- /dev/null +++ b/DiscUtils/Iso9660/ExtentStream.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Iso9660 +{ + internal class ExtentStream : Stream + { + private readonly uint _dataLength; + private readonly byte _fileUnitSize; + private readonly byte _interleaveGapSize; + + private readonly Stream _isoStream; + private long _position; + + private readonly uint _startBlock; + private readonly int _sectorSize; + + public ExtentStream(Stream isoStream, uint startBlock, uint dataLength, byte fileUnitSize, + byte interleaveGapSize, int sectorSize) + { + _isoStream = isoStream; + _startBlock = startBlock; + _dataLength = dataLength; + _fileUnitSize = fileUnitSize; + _interleaveGapSize = interleaveGapSize; + _sectorSize = sectorSize; + + if (_fileUnitSize != 0 || _interleaveGapSize != 0) + { + throw new NotSupportedException("Non-contiguous extents not supported"); + } + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _dataLength; } + } + + public override long Position + { + get { return _position; } + set { _position = value; } + } + + public override void Flush() {} + + public override int Read(byte[] buffer, int offset, int count) + { + if (_position > _dataLength) + { + return 0; + } + + int toRead = (int)Math.Min((uint)count, _dataLength - _position); + + _isoStream.Position = _position + _startBlock * (long)_sectorSize; + int numRead = _isoStream.Read(buffer, offset, toRead); + _position += numRead; + return numRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = offset; + if (origin == SeekOrigin.Current) + { + newPos += _position; + } + else if (origin == SeekOrigin.End) + { + newPos += _dataLength; + } + + _position = newPos; + return newPos; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/File.cs b/DiscUtils/Iso9660/File.cs new file mode 100644 index 0000000..81bbad4 --- /dev/null +++ b/DiscUtils/Iso9660/File.cs @@ -0,0 +1,119 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Vfs; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal class File : IVfsFile + { + protected IsoContext _context; + protected ReaderDirEntry _dirEntry; + + public File(IsoContext context, ReaderDirEntry dirEntry) + { + _context = context; + _dirEntry = dirEntry; + } + + public virtual byte[] SystemUseData + { + get { return _dirEntry.Record.SystemUseData; } + } + + public UnixFileSystemInfo UnixFileInfo + { + get + { + if (!_context.SuspDetected || string.IsNullOrEmpty(_context.RockRidgeIdentifier)) + { + throw new InvalidOperationException("No RockRidge file information available"); + } + + SuspRecords suspRecords = new SuspRecords(_context, SystemUseData, 0); + + PosixFileInfoSystemUseEntry pfi = + suspRecords.GetEntry(_context.RockRidgeIdentifier, "PX"); + if (pfi != null) + { + return new UnixFileSystemInfo + { + FileType = (UnixFileType)((pfi.FileMode >> 12) & 0xff), + Permissions = (UnixFilePermissions)(pfi.FileMode & 0xfff), + UserId = (int)pfi.UserId, + GroupId = (int)pfi.GroupId, + Inode = pfi.Inode, + LinkCount = (int)pfi.NumLinks + }; + } + + throw new InvalidOperationException("No RockRidge file information available for this file"); + } + } + + public DateTime LastAccessTimeUtc + { + get { return _dirEntry.LastAccessTimeUtc; } + + set { throw new NotSupportedException(); } + } + + public DateTime LastWriteTimeUtc + { + get { return _dirEntry.LastWriteTimeUtc; } + + set { throw new NotSupportedException(); } + } + + public DateTime CreationTimeUtc + { + get { return _dirEntry.CreationTimeUtc; } + + set { throw new NotSupportedException(); } + } + + public FileAttributes FileAttributes + { + get { return _dirEntry.FileAttributes; } + + set { throw new NotSupportedException(); } + } + + public long FileLength + { + get { return _dirEntry.Record.DataLength; } + } + + public IBuffer FileContent + { + get + { + ExtentStream es = new ExtentStream(_context.DataStream, _dirEntry.Record.LocationOfExtent, + _dirEntry.Record.DataLength, _dirEntry.Record.FileUnitSize, _dirEntry.Record.InterleaveGapSize, _context.SectorSize); + return new StreamBuffer(es, Ownership.Dispose); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/FileExtent.cs b/DiscUtils/Iso9660/FileExtent.cs new file mode 100644 index 0000000..ba0e793 --- /dev/null +++ b/DiscUtils/Iso9660/FileExtent.cs @@ -0,0 +1,85 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal class FileExtent : BuilderExtent + { + private readonly BuildFileInfo _fileInfo; + + private Stream _readStream; + + public FileExtent(BuildFileInfo fileInfo, long start) + : base(start, fileInfo.GetDataSize(Encoding.ASCII)) + { + _fileInfo = fileInfo; + } + + public override void Dispose() + { + if (_readStream != null) + { + _fileInfo.CloseStream(_readStream); + _readStream = null; + } + } + + public override void PrepareForRead() + { + _readStream = _fileInfo.OpenStream(); + } + + public override int Read(long diskOffset, byte[] block, int offset, int count) + { + long relPos = diskOffset - Start; + int totalRead = 0; + + // Don't arbitrarily set position, just in case stream implementation is + // non-seeking, and we're doing sequential reads + if (_readStream.Position != relPos) + { + _readStream.Position = relPos; + } + + // Read up to EOF + int numRead = _readStream.Read(block, offset, count); + totalRead += numRead; + while (numRead > 0 && totalRead < count) + { + numRead = _readStream.Read(block, offset + totalRead, count - totalRead); + totalRead += numRead; + } + + return totalRead; + } + + public override void DisposeReadState() + { + _fileInfo.CloseStream(_readStream); + _readStream = null; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/FileFlags.cs b/DiscUtils/Iso9660/FileFlags.cs new file mode 100644 index 0000000..0ed6fe0 --- /dev/null +++ b/DiscUtils/Iso9660/FileFlags.cs @@ -0,0 +1,16 @@ +using System; + +namespace DiscUtils.Iso9660 +{ + [Flags] + internal enum FileFlags : byte + { + None = 0x00, + Hidden = 0x01, + Directory = 0x02, + AssociatedFile = 0x04, + Record = 0x08, + Protection = 0x10, + MultiExtent = 0x80 + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Iso9660Variant.cs b/DiscUtils/Iso9660/Iso9660Variant.cs new file mode 100644 index 0000000..dbcdb83 --- /dev/null +++ b/DiscUtils/Iso9660/Iso9660Variant.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + /// + /// Enumeration of known file system variants. + /// + /// + /// ISO9660 has a number of significant limitations, and over time + /// multiple schemes have been devised for extending the standard + /// to support the richer file system semantics typical of most modern + /// operating systems. These variants differ functionally and (in the + /// case of RockRidge) may represent a logically different directory + /// hierarchy to that encoded in the vanilla iso9660 standard. + /// Use this enum to control which variants to honour / prefer + /// when accessing an ISO image. + /// + public enum Iso9660Variant + { + /// + /// No known variant. + /// + None, + + /// + /// Vanilla ISO9660. + /// + Iso9660, + + /// + /// Joliet file system (Windows). + /// + Joliet, + + /// + /// Rock Ridge (Unix). + /// + RockRidge + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/IsoContext.cs b/DiscUtils/Iso9660/IsoContext.cs new file mode 100644 index 0000000..5232f62 --- /dev/null +++ b/DiscUtils/Iso9660/IsoContext.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; +using DiscUtils.Vfs; + +namespace DiscUtils.Iso9660 +{ + internal class IsoContext : VfsContext + { + public IsoContext(int sectorSize) + { + SectorSize = sectorSize; + } + + public Stream DataStream { get; set; } + + public string RockRidgeIdentifier { get; set; } + + public bool SuspDetected { get; set; } + + public int SectorSize { get; set; } + + public List SuspExtensions { get; set; } + + public int SuspSkipBytes { get; set; } + public CommonVolumeDescriptor VolumeDescriptor { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/IsoUtilities.cs b/DiscUtils/Iso9660/IsoUtilities.cs new file mode 100644 index 0000000..4327450 --- /dev/null +++ b/DiscUtils/Iso9660/IsoUtilities.cs @@ -0,0 +1,467 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using System.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal static class IsoUtilities + { + //public const int SectorSize = 2048; + + public static uint ToUInt32FromBoth(byte[] data, int offset) + { + return EndianUtilities.ToUInt32LittleEndian(data, offset); + } + + public static ushort ToUInt16FromBoth(byte[] data, int offset) + { + return EndianUtilities.ToUInt16LittleEndian(data, offset); + } + + internal static void ToBothFromUInt32(byte[] buffer, int offset, uint value) + { + EndianUtilities.WriteBytesLittleEndian(value, buffer, offset); + EndianUtilities.WriteBytesBigEndian(value, buffer, offset + 4); + } + + internal static void ToBothFromUInt16(byte[] buffer, int offset, ushort value) + { + EndianUtilities.WriteBytesLittleEndian(value, buffer, offset); + EndianUtilities.WriteBytesBigEndian(value, buffer, offset + 2); + } + + internal static void ToBytesFromUInt32(byte[] buffer, int offset, uint value) + { + EndianUtilities.WriteBytesLittleEndian(value, buffer, offset); + } + + internal static void ToBytesFromUInt16(byte[] buffer, int offset, ushort value) + { + EndianUtilities.WriteBytesLittleEndian(value, buffer, offset); + } + + internal static void WriteAChars(byte[] buffer, int offset, int numBytes, string str) + { + // Validate string + if (!IsValidAString(str)) + { + throw new IOException("Attempt to write string with invalid a-characters"); + } + + ////WriteASCII(buffer, offset, numBytes, true, str); + WriteString(buffer, offset, numBytes, true, str, Encoding.ASCII); + } + + internal static void WriteDChars(byte[] buffer, int offset, int numBytes, string str) + { + // Validate string + if (!IsValidDString(str)) + { + throw new IOException("Attempt to write string with invalid d-characters"); + } + + ////WriteASCII(buffer, offset, numBytes, true, str); + WriteString(buffer, offset, numBytes, true, str, Encoding.ASCII); + } + + internal static void WriteA1Chars(byte[] buffer, int offset, int numBytes, string str, Encoding enc) + { + // Validate string + if (!IsValidAString(str)) + { + throw new IOException("Attempt to write string with invalid a-characters"); + } + + WriteString(buffer, offset, numBytes, true, str, enc); + } + + internal static void WriteD1Chars(byte[] buffer, int offset, int numBytes, string str, Encoding enc) + { + // Validate string + if (!IsValidDString(str)) + { + throw new IOException("Attempt to write string with invalid d-characters"); + } + + WriteString(buffer, offset, numBytes, true, str, enc); + } + + internal static string ReadChars(byte[] buffer, int offset, int numBytes, Encoding enc) + { + char[] chars; + + // Special handling for 'magic' names '\x00' and '\x01', which indicate root and parent, respectively + if (numBytes == 1) + { + chars = new char[1]; + chars[0] = (char)buffer[offset]; + } + else + { + Decoder decoder = enc.GetDecoder(); + chars = new char[decoder.GetCharCount(buffer, offset, numBytes, false)]; + decoder.GetChars(buffer, offset, numBytes, chars, 0, false); + } + + return new string(chars).TrimEnd(' '); + } + +#if false + public static byte WriteFileName(byte[] buffer, int offset, int numBytes, string str, Encoding enc) + { + if (numBytes > 255 || numBytes < 0) + { + throw new ArgumentOutOfRangeException("numBytes", "Attempt to write overlength or underlength file name"); + } + + // Validate string + if (!isValidFileName(str)) + { + throw new IOException("Attempt to write string with invalid file name characters"); + } + + return (byte)WriteString(buffer, offset, numBytes, false, str, enc); + } + + public static byte WriteDirectoryName(byte[] buffer, int offset, int numBytes, string str, Encoding enc) + { + if (numBytes > 255 || numBytes < 0) + { + throw new ArgumentOutOfRangeException("numBytes", "Attempt to write overlength or underlength directory name"); + } + + // Validate string + if (!isValidDirectoryName(str)) + { + throw new IOException("Attempt to write string with invalid directory name characters"); + } + + return (byte)WriteString(buffer, offset, numBytes, false, str, enc); + } +#endif + + internal static int WriteString(byte[] buffer, int offset, int numBytes, bool pad, string str, Encoding enc) + { + return WriteString(buffer, offset, numBytes, pad, str, enc, false); + } + + internal static int WriteString(byte[] buffer, int offset, int numBytes, bool pad, string str, Encoding enc, + bool canTruncate) + { + Encoder encoder = enc.GetEncoder(); + + string paddedString = pad ? str + new string(' ', numBytes) : str; + + // Assumption: never less than one byte per character + + int charsUsed; + int bytesUsed; + bool completed; + encoder.Convert(paddedString.ToCharArray(), 0, paddedString.Length, buffer, offset, numBytes, false, + out charsUsed, out bytesUsed, out completed); + + if (!canTruncate && charsUsed < str.Length) + { + throw new IOException("Failed to write entire string"); + } + + return bytesUsed; + } + + internal static bool IsValidAString(string str) + { + for (int i = 0; i < str.Length; ++i) + { + if (!( + (str[i] >= ' ' && str[i] <= '\"') + || (str[i] >= '%' && str[i] <= '/') + || (str[i] >= ':' && str[i] <= '?') + || (str[i] >= '0' && str[i] <= '9') + || (str[i] >= 'A' && str[i] <= 'Z') + || (str[i] == '_'))) + { + return false; + } + } + + return true; + } + + internal static bool IsValidDString(string str) + { + for (int i = 0; i < str.Length; ++i) + { + if (!IsValidDChar(str[i])) + { + return false; + } + } + + return true; + } + + internal static bool IsValidDChar(char ch) + { + return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch == '_'); + } + + internal static bool IsValidFileName(string str) + { + for (int i = 0; i < str.Length; ++i) + { + if ( + !((str[i] >= '0' && str[i] <= '9') || (str[i] >= 'A' && str[i] <= 'Z') || (str[i] == '_') || + (str[i] == '.') || (str[i] == ';'))) + { + return false; + } + } + + return true; + } + + internal static bool IsValidDirectoryName(string str) + { + if (str.Length == 1 && (str[0] == 0 || str[0] == 1)) + { + return true; + } + return IsValidDString(str); + } + + internal static string NormalizeFileName(string name) + { + string[] parts = SplitFileName(name); + return parts[0] + '.' + parts[1] + ';' + parts[2]; + } + + internal static string[] SplitFileName(string name) + { + string[] parts = { name, string.Empty, "1" }; + + if (name.Contains(".")) + { + int endOfFilePart = name.IndexOf('.'); + parts[0] = name.Substring(0, endOfFilePart); + if (name.Contains(";")) + { + int verSep = name.IndexOf(';', endOfFilePart + 1); + parts[1] = name.Substring(endOfFilePart + 1, verSep - (endOfFilePart + 1)); + parts[2] = name.Substring(verSep + 1); + } + else + { + parts[1] = name.Substring(endOfFilePart + 1); + } + } + else + { + if (name.Contains(";")) + { + int verSep = name.IndexOf(';'); + parts[0] = name.Substring(0, verSep); + parts[2] = name.Substring(verSep + 1); + } + } + + ushort ver; + if (!ushort.TryParse(parts[2], out ver) || ver > 32767 || ver < 1) + { + ver = 1; + } + + parts[2] = string.Format(CultureInfo.InvariantCulture, "{0}", ver); + + return parts; + } + + /// + /// Converts a DirectoryRecord time to UTC. + /// + /// Buffer containing the time data. + /// Offset in buffer of the time data. + /// The time in UTC. + internal static DateTime ToUTCDateTimeFromDirectoryTime(byte[] data, int offset) + { + try + { + DateTime relTime = new DateTime( + 1900 + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + data[offset + 5], + DateTimeKind.Utc); + return relTime - TimeSpan.FromMinutes(15 * (sbyte)data[offset + 6]); + } + catch (ArgumentOutOfRangeException) + { + // In case the ISO has a bad date encoded, we'll just fall back to using a fixed date + return DateTime.MinValue; + } + } + + internal static void ToDirectoryTimeFromUTC(byte[] data, int offset, DateTime dateTime) + { + if (dateTime == DateTime.MinValue) + { + Array.Clear(data, offset, 7); + } + else + { + if (dateTime.Year < 1900) + { + throw new IOException("Year is out of range"); + } + + data[offset] = (byte)(dateTime.Year - 1900); + data[offset + 1] = (byte)dateTime.Month; + data[offset + 2] = (byte)dateTime.Day; + data[offset + 3] = (byte)dateTime.Hour; + data[offset + 4] = (byte)dateTime.Minute; + data[offset + 5] = (byte)dateTime.Second; + data[offset + 6] = 0; + } + } + + internal static DateTime ToDateTimeFromVolumeDescriptorTime(byte[] data, int offset) + { + bool allNull = true; + for (int i = 0; i < 16; ++i) + { + if (data[offset + i] != (byte)'0' && data[offset + i] != 0) + { + allNull = false; + break; + } + } + + if (allNull) + { + return DateTime.MinValue; + } + + string strForm = Encoding.ASCII.GetString(data, offset, 16); + + // Work around bugs in burning software that may use zero bytes (rather than '0' characters) + strForm = strForm.Replace('\0', '0'); + + int year = SafeParseInt(1, 9999, strForm.Substring(0, 4)); + int month = SafeParseInt(1, 12, strForm.Substring(4, 2)); + int day = SafeParseInt(1, 31, strForm.Substring(6, 2)); + int hour = SafeParseInt(0, 23, strForm.Substring(8, 2)); + int min = SafeParseInt(0, 59, strForm.Substring(10, 2)); + int sec = SafeParseInt(0, 59, strForm.Substring(12, 2)); + int hundredths = SafeParseInt(0, 99, strForm.Substring(14, 2)); + + try + { + DateTime time = new DateTime(year, month, day, hour, min, sec, hundredths * 10, DateTimeKind.Utc); + return time - TimeSpan.FromMinutes(15 * (sbyte)data[offset + 16]); + } + catch (ArgumentOutOfRangeException) + { + return DateTime.MinValue; + } + } + + internal static void ToVolumeDescriptorTimeFromUTC(byte[] buffer, int offset, DateTime dateTime) + { + if (dateTime == DateTime.MinValue) + { + for (int i = offset; i < offset + 16; ++i) + { + buffer[i] = (byte)'0'; + } + + buffer[offset + 16] = 0; + return; + } + + string strForm = dateTime.ToString("yyyyMMddHHmmssff", CultureInfo.InvariantCulture); + EndianUtilities.StringToBytes(strForm, buffer, offset, 16); + buffer[offset + 16] = 0; + } + + internal static void EncodingToBytes(Encoding enc, byte[] data, int offset) + { + Array.Clear(data, offset, 32); + if (enc == Encoding.ASCII) + { + // Nothing to do + } + else if (enc == Encoding.BigEndianUnicode) + { + data[offset + 0] = 0x25; + data[offset + 1] = 0x2F; + data[offset + 2] = 0x45; + } + else + { + throw new ArgumentException("Unrecognized character encoding"); + } + } + + internal static Encoding EncodingFromBytes(byte[] data, int offset) + { + Encoding enc = Encoding.ASCII; + if (data[offset + 0] == 0x25 && data[offset + 1] == 0x2F + && (data[offset + 2] == 0x40 || data[offset + 2] == 0x43 || data[offset + 2] == 0x45)) + { + // I.e. this is a joliet disc! + enc = Encoding.BigEndianUnicode; + } + + return enc; + } + + internal static bool IsSpecialDirectory(DirectoryRecord r) + { + return r.FileIdentifier == "\0" || r.FileIdentifier == "\x01"; + } + + private static int SafeParseInt(int minVal, int maxVal, string str) + { + int val; + if (!int.TryParse(str, out val)) + { + return minVal; + } + + if (val < minVal) + { + return minVal; + } + if (val > maxVal) + { + return maxVal; + } + return val; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/PathTable.cs b/DiscUtils/Iso9660/PathTable.cs new file mode 100644 index 0000000..479b80b --- /dev/null +++ b/DiscUtils/Iso9660/PathTable.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal class PathTable : BuilderExtent + { + private readonly bool _byteSwap; + private readonly List _dirs; + private readonly Encoding _enc; + private readonly Dictionary _locations; + + private byte[] _readCache; + + public PathTable(bool byteSwap, Encoding enc, List dirs, + Dictionary locations, long start) + : base(start, CalcLength(enc, dirs)) + { + _byteSwap = byteSwap; + _enc = enc; + _dirs = dirs; + _locations = locations; + } + + public override void Dispose() {} + + public override void PrepareForRead() + { + _readCache = new byte[Length]; + int pos = 0; + + List sortedList = new List(_dirs); + sortedList.Sort(BuildDirectoryInfo.PathTableSortComparison); + + Dictionary dirNumbers = new Dictionary(_dirs.Count); + ushort i = 1; + foreach (BuildDirectoryInfo di in sortedList) + { + dirNumbers[di] = i++; + PathTableRecord ptr = new PathTableRecord(); + ptr.DirectoryIdentifier = di.PickName(null, _enc); + ptr.LocationOfExtent = _locations[di]; + ptr.ParentDirectoryNumber = dirNumbers[di.Parent]; + + pos += ptr.Write(_byteSwap, _enc, _readCache, pos); + } + } + + public override int Read(long diskOffset, byte[] buffer, int offset, int count) + { + long relPos = diskOffset - Start; + + int numRead = (int)Math.Min(count, _readCache.Length - relPos); + + Array.Copy(_readCache, (int)relPos, buffer, offset, numRead); + + return numRead; + } + + public override void DisposeReadState() + { + _readCache = null; + } + + private static uint CalcLength(Encoding enc, List dirs) + { + uint length = 0; + foreach (BuildDirectoryInfo di in dirs) + { + length += di.GetPathTableEntrySize(enc); + } + + return length; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/PathTableRecord.cs b/DiscUtils/Iso9660/PathTableRecord.cs new file mode 100644 index 0000000..5b279b9 --- /dev/null +++ b/DiscUtils/Iso9660/PathTableRecord.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Internal; + +namespace DiscUtils.Iso9660 +{ + internal struct PathTableRecord + { + ////public byte ExtendedAttributeRecordLength; + public uint LocationOfExtent; + public ushort ParentDirectoryNumber; + public string DirectoryIdentifier; + + ////public static int ReadFrom(byte[] src, int offset, bool byteSwap, Encoding enc, out PathTableRecord record) + ////{ + //// byte directoryIdentifierLength = src[offset + 0]; + //// record.ExtendedAttributeRecordLength = src[offset + 1]; + //// record.LocationOfExtent = EndianUtilities.ToUInt32LittleEndian(src, offset + 2); + //// record.ParentDirectoryNumber = EndianUtilities.ToUInt16LittleEndian(src, offset + 6); + //// record.DirectoryIdentifier = IsoUtilities.ReadChars(src, offset + 8, directoryIdentifierLength, enc); + //// + //// if (byteSwap) + //// { + //// record.LocationOfExtent = Utilities.BitSwap(record.LocationOfExtent); + //// record.ParentDirectoryNumber = Utilities.BitSwap(record.ParentDirectoryNumber); + //// } + //// + //// return directoryIdentifierLength + 8 + (((directoryIdentifierLength & 1) == 1) ? 1 : 0); + ////} + + internal int Write(bool byteSwap, Encoding enc, byte[] buffer, int offset) + { + int nameBytes = enc.GetByteCount(DirectoryIdentifier); + + buffer[offset + 0] = (byte)nameBytes; + buffer[offset + 1] = 0; // ExtendedAttributeRecordLength; + IsoUtilities.ToBytesFromUInt32(buffer, offset + 2, + byteSwap ? Utilities.BitSwap(LocationOfExtent) : LocationOfExtent); + IsoUtilities.ToBytesFromUInt16(buffer, offset + 6, + byteSwap ? Utilities.BitSwap(ParentDirectoryNumber) : ParentDirectoryNumber); + IsoUtilities.WriteString(buffer, offset + 8, nameBytes, false, DirectoryIdentifier, enc); + if ((nameBytes & 1) == 1) + { + buffer[offset + 8 + nameBytes] = 0; + } + + return 8 + nameBytes + ((nameBytes & 0x1) == 1 ? 1 : 0); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs b/DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs new file mode 100644 index 0000000..565c2ea --- /dev/null +++ b/DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Internal; + +namespace DiscUtils.Iso9660 +{ + internal class PrimaryVolumeDescriptor : CommonVolumeDescriptor + { + public PrimaryVolumeDescriptor(byte[] src, int offset) + : base(src, offset, Encoding.ASCII) {} + + public PrimaryVolumeDescriptor( + uint volumeSpaceSize, + uint pathTableSize, + uint typeLPathTableLocation, + uint typeMPathTableLocation, + uint rootDirExtentLocation, + uint rootDirDataLength, + DateTime buildTime, + int sectorSize) + : base( + VolumeDescriptorType.Primary, 1, volumeSpaceSize, pathTableSize, typeLPathTableLocation, + typeMPathTableLocation, rootDirExtentLocation, rootDirDataLength, buildTime, Encoding.ASCII, sectorSize) {} + + internal override void WriteTo(byte[] buffer, int offset) + { + base.WriteTo(buffer, offset); + IsoUtilities.WriteAChars(buffer, offset + 8, 32, SystemIdentifier); + IsoUtilities.WriteString(buffer, offset + 40, 32, true, VolumeIdentifier, Encoding.ASCII, true); + IsoUtilities.ToBothFromUInt32(buffer, offset + 80, VolumeSpaceSize); + IsoUtilities.ToBothFromUInt16(buffer, offset + 120, VolumeSetSize); + IsoUtilities.ToBothFromUInt16(buffer, offset + 124, VolumeSequenceNumber); + IsoUtilities.ToBothFromUInt16(buffer, offset + 128, LogicalBlockSize); + IsoUtilities.ToBothFromUInt32(buffer, offset + 132, PathTableSize); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 140, TypeLPathTableLocation); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 144, OptionalTypeLPathTableLocation); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 148, Utilities.BitSwap(TypeMPathTableLocation)); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 152, Utilities.BitSwap(OptionalTypeMPathTableLocation)); + RootDirectory.WriteTo(buffer, offset + 156, Encoding.ASCII); + IsoUtilities.WriteDChars(buffer, offset + 190, 129, VolumeSetIdentifier); + IsoUtilities.WriteAChars(buffer, offset + 318, 129, PublisherIdentifier); + IsoUtilities.WriteAChars(buffer, offset + 446, 129, DataPreparerIdentifier); + IsoUtilities.WriteAChars(buffer, offset + 574, 129, ApplicationIdentifier); + IsoUtilities.WriteDChars(buffer, offset + 702, 37, CopyrightFileIdentifier); // FIXME!! + IsoUtilities.WriteDChars(buffer, offset + 739, 37, AbstractFileIdentifier); // FIXME!! + IsoUtilities.WriteDChars(buffer, offset + 776, 37, BibliographicFileIdentifier); // FIXME!! + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 813, CreationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 830, ModificationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 847, ExpirationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 864, EffectiveDateAndTime); + buffer[offset + 881] = FileStructureVersion; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/PrimaryVolumeDescriptorRegion.cs b/DiscUtils/Iso9660/PrimaryVolumeDescriptorRegion.cs new file mode 100644 index 0000000..dcc2125 --- /dev/null +++ b/DiscUtils/Iso9660/PrimaryVolumeDescriptorRegion.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal class PrimaryVolumeDescriptorRegion : VolumeDescriptorDiskRegion + { + private readonly PrimaryVolumeDescriptor _descriptor; + + public PrimaryVolumeDescriptorRegion(PrimaryVolumeDescriptor descriptor, long start, int sectorSize) + : base(start, sectorSize) + { + _descriptor = descriptor; + } + + protected override byte[] GetBlockData() + { + byte[] buffer = new byte[Length]; + _descriptor.WriteTo(buffer, 0); + return buffer; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/ReaderDirEntry.cs b/DiscUtils/Iso9660/ReaderDirEntry.cs new file mode 100644 index 0000000..49a813a --- /dev/null +++ b/DiscUtils/Iso9660/ReaderDirEntry.cs @@ -0,0 +1,195 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Text; +using DiscUtils.Internal; +using DiscUtils.Streams; +using DiscUtils.Vfs; + +namespace DiscUtils.Iso9660 +{ + internal sealed class ReaderDirEntry : VfsDirEntry + { + private readonly IsoContext _context; + private readonly string _fileName; + private readonly DirectoryRecord _record; + + public ReaderDirEntry(IsoContext context, DirectoryRecord dirRecord) + { + _context = context; + _record = dirRecord; + _fileName = _record.FileIdentifier; + + bool rockRidge = !string.IsNullOrEmpty(_context.RockRidgeIdentifier); + + if (context.SuspDetected && _record.SystemUseData != null) + { + SuspRecords = new SuspRecords(_context, _record.SystemUseData, 0); + } + + if (rockRidge && SuspRecords != null) + { + // The full name is taken from this record, even if it's a child-link record + List nameEntries = SuspRecords.GetEntries(_context.RockRidgeIdentifier, "NM"); + StringBuilder rrName = new StringBuilder(); + if (nameEntries != null && nameEntries.Count > 0) + { + foreach (PosixNameSystemUseEntry nameEntry in nameEntries) + { + rrName.Append(nameEntry.NameData); + } + + _fileName = rrName.ToString(); + } + + // If this is a Rock Ridge child link, replace the dir record with that from the 'self' record + // in the child directory. + ChildLinkSystemUseEntry clEntry = + SuspRecords.GetEntry(_context.RockRidgeIdentifier, "CL"); + if (clEntry != null) + { + _context.DataStream.Position = clEntry.ChildDirLocation * _context.VolumeDescriptor.LogicalBlockSize; + byte[] firstSector = StreamUtilities.ReadExact(_context.DataStream, + _context.VolumeDescriptor.LogicalBlockSize); + + DirectoryRecord.ReadFrom(firstSector, 0, _context.VolumeDescriptor.CharacterEncoding, out _record); + if (_record.SystemUseData != null) + { + SuspRecords = new SuspRecords(_context, _record.SystemUseData, 0); + } + } + } + + LastAccessTimeUtc = _record.RecordingDateAndTime; + LastWriteTimeUtc = _record.RecordingDateAndTime; + CreationTimeUtc = _record.RecordingDateAndTime; + + if (rockRidge && SuspRecords != null) + { + FileTimeSystemUseEntry tfEntry = + SuspRecords.GetEntry(_context.RockRidgeIdentifier, "TF"); + + if (tfEntry != null) + { + if ((tfEntry.TimestampsPresent & FileTimeSystemUseEntry.Timestamps.Access) != 0) + { + LastAccessTimeUtc = tfEntry.AccessTime; + } + + if ((tfEntry.TimestampsPresent & FileTimeSystemUseEntry.Timestamps.Modify) != 0) + { + LastWriteTimeUtc = tfEntry.ModifyTime; + } + + if ((tfEntry.TimestampsPresent & FileTimeSystemUseEntry.Timestamps.Creation) != 0) + { + CreationTimeUtc = tfEntry.CreationTime; + } + } + } + } + + public override DateTime CreationTimeUtc { get; } + + public override FileAttributes FileAttributes + { + get + { + FileAttributes attrs = 0; + + if (!string.IsNullOrEmpty(_context.RockRidgeIdentifier)) + { + // If Rock Ridge PX info is present, derive the attributes from the RR info. + PosixFileInfoSystemUseEntry pfi = + SuspRecords.GetEntry(_context.RockRidgeIdentifier, "PX"); + if (pfi != null) + { + attrs = Utilities.FileAttributesFromUnixFileType((UnixFileType)((pfi.FileMode >> 12) & 0xF)); + } + + if (_fileName.StartsWith(".", StringComparison.Ordinal)) + { + attrs |= FileAttributes.Hidden; + } + } + + attrs |= FileAttributes.ReadOnly; + + if ((_record.Flags & FileFlags.Directory) != 0) + { + attrs |= FileAttributes.Directory; + } + + if ((_record.Flags & FileFlags.Hidden) != 0) + { + attrs |= FileAttributes.Hidden; + } + + return attrs; + } + } + + public override string FileName + { + get { return _fileName; } + } + + public override bool HasVfsFileAttributes + { + get { return true; } + } + + public override bool HasVfsTimeInfo + { + get { return true; } + } + + public override bool IsDirectory + { + get { return (_record.Flags & FileFlags.Directory) != 0; } + } + + public override bool IsSymlink + { + get { return false; } + } + + public override DateTime LastAccessTimeUtc { get; } + + public override DateTime LastWriteTimeUtc { get; } + + public DirectoryRecord Record + { + get { return _record; } + } + + public SuspRecords SuspRecords { get; } + + public override long UniqueCacheId + { + get { return ((long)_record.LocationOfExtent << 32) | _record.DataLength; } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/ReaderDirectory.cs b/DiscUtils/Iso9660/ReaderDirectory.cs new file mode 100644 index 0000000..003436b --- /dev/null +++ b/DiscUtils/Iso9660/ReaderDirectory.cs @@ -0,0 +1,130 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.CoreCompat; +using DiscUtils.Streams; +using DiscUtils.Vfs; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace DiscUtils.Iso9660 +{ + internal class ReaderDirectory : File, IVfsDirectory + { + private readonly List _records; + + public ReaderDirectory(IsoContext context, ReaderDirEntry dirEntry) + : base(context, dirEntry) + { + byte[] buffer = new byte[context.SectorSize]; + Stream extent = new ExtentStream(_context.DataStream, dirEntry.Record.LocationOfExtent, uint.MaxValue, 0, 0, context.SectorSize); + + _records = new List(); + + uint totalLength = dirEntry.Record.DataLength; + uint totalRead = 0; + while (totalRead < totalLength) + { + int bytesRead = (int)Math.Min(buffer.Length, totalLength - totalRead); + + extent.Seek(24, SeekOrigin.Current); + + StreamUtilities.ReadExact(extent, buffer, 0, bytesRead); + totalRead += (uint)bytesRead; + + uint pos = 0; + while (pos < bytesRead && buffer[pos] != 0) + { + DirectoryRecord dr; + uint length = (uint)DirectoryRecord.ReadFrom(buffer, (int)pos, context.VolumeDescriptor.CharacterEncoding, out dr); + + if (!IsoUtilities.IsSpecialDirectory(dr)) + { + ReaderDirEntry childDirEntry = new ReaderDirEntry(_context, dr); + + if (context.SuspDetected && !string.IsNullOrEmpty(context.RockRidgeIdentifier)) + { + if (childDirEntry.SuspRecords == null || !childDirEntry.SuspRecords.HasEntry(context.RockRidgeIdentifier, "RE")) + { + _records.Add(childDirEntry); + } + } + else + { + _records.Add(childDirEntry); + } + } + else if (dr.FileIdentifier == "\0") + { + Self = new ReaderDirEntry(_context, dr); + } + + pos += length; + } + } + } + + public override byte[] SystemUseData + { + get { return Self.Record.SystemUseData; } + } + + public ICollection AllEntries + { + get { return _records; } + } + + public ReaderDirEntry Self { get; } + + public ReaderDirEntry GetEntryByName(string name) + { + bool anyVerMatch = name.IndexOf(';') < 0; + string normName = IsoUtilities.NormalizeFileName(name).ToUpper(CultureInfo.InvariantCulture); + if (anyVerMatch) + { + normName = normName.Substring(0, normName.LastIndexOf(';') + 1); + } + + foreach (ReaderDirEntry r in _records) + { + string toComp = IsoUtilities.NormalizeFileName(r.FileName).ToUpper(CultureInfo.InvariantCulture); + if (!anyVerMatch && toComp == normName) + { + return r; + } + if (anyVerMatch && toComp.StartsWith(normName, StringComparison.Ordinal)) + { + return r; + } + } + + return null; + } + + public ReaderDirEntry CreateNewFile(string name) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/RockRidge/ChildLinkSystemUseEntry.cs b/DiscUtils/Iso9660/RockRidge/ChildLinkSystemUseEntry.cs new file mode 100644 index 0000000..0f8c31a --- /dev/null +++ b/DiscUtils/Iso9660/RockRidge/ChildLinkSystemUseEntry.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal sealed class ChildLinkSystemUseEntry : SystemUseEntry + { + public uint ChildDirLocation; + + public ChildLinkSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 12, 1); + + ChildDirLocation = IsoUtilities.ToUInt32FromBoth(data, offset + 4); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/RockRidge/FileTimeSystemUseEntry.cs b/DiscUtils/Iso9660/RockRidge/FileTimeSystemUseEntry.cs new file mode 100644 index 0000000..742dc6f --- /dev/null +++ b/DiscUtils/Iso9660/RockRidge/FileTimeSystemUseEntry.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Iso9660 +{ + internal sealed class FileTimeSystemUseEntry : SystemUseEntry + { + [Flags] + public enum Timestamps : byte + { + None = 0x00, + Creation = 0x01, + Modify = 0x02, + Access = 0x04, + Attributes = 0x08, + Backup = 0x10, + Expiration = 0x20, + Effective = 0x40 + } + + public DateTime AccessTime; + public DateTime AttributesTime; + public DateTime BackupTime; + public DateTime CreationTime; + public DateTime EffectiveTime; + public DateTime ExpirationTime; + public DateTime ModifyTime; + public Timestamps TimestampsPresent = Timestamps.None; + + public FileTimeSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 5, 1); + + byte flags = data[offset + 4]; + + bool longForm = (flags & 0x80) != 0; + int fieldLen = longForm ? 17 : 7; + + TimestampsPresent = (Timestamps)(flags & 0x7F); + + int pos = offset + 5; + + CreationTime = ReadTimestamp(Timestamps.Creation, data, longForm, ref pos); + ModifyTime = ReadTimestamp(Timestamps.Modify, data, longForm, ref pos); + AccessTime = ReadTimestamp(Timestamps.Access, data, longForm, ref pos); + AttributesTime = ReadTimestamp(Timestamps.Attributes, data, longForm, ref pos); + BackupTime = ReadTimestamp(Timestamps.Backup, data, longForm, ref pos); + ExpirationTime = ReadTimestamp(Timestamps.Expiration, data, longForm, ref pos); + EffectiveTime = ReadTimestamp(Timestamps.Effective, data, longForm, ref pos); + } + + private DateTime ReadTimestamp(Timestamps timestamp, byte[] data, bool longForm, ref int pos) + { + DateTime result = DateTime.MinValue; + + if ((TimestampsPresent & timestamp) != 0) + { + if (longForm) + { + result = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(data, pos); + pos += 17; + } + else + { + result = IsoUtilities.ToUTCDateTimeFromDirectoryTime(data, pos); + pos += 7; + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/RockRidge/PosixFileInfoSystemUseEntry.cs b/DiscUtils/Iso9660/RockRidge/PosixFileInfoSystemUseEntry.cs new file mode 100644 index 0000000..d2c97b1 --- /dev/null +++ b/DiscUtils/Iso9660/RockRidge/PosixFileInfoSystemUseEntry.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal sealed class PosixFileInfoSystemUseEntry : SystemUseEntry + { + public uint FileMode; + public uint GroupId; + public uint Inode; + public uint NumLinks; + public uint UserId; + + public PosixFileInfoSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 36, 1); + + FileMode = IsoUtilities.ToUInt32FromBoth(data, offset + 4); + NumLinks = IsoUtilities.ToUInt32FromBoth(data, offset + 12); + UserId = IsoUtilities.ToUInt32FromBoth(data, offset + 20); + GroupId = IsoUtilities.ToUInt32FromBoth(data, offset + 28); + Inode = 0; + if (length >= 44) + { + Inode = IsoUtilities.ToUInt32FromBoth(data, offset + 36); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/RockRidge/PosixNameSystemUseEntry.cs b/DiscUtils/Iso9660/RockRidge/PosixNameSystemUseEntry.cs new file mode 100644 index 0000000..15db6d7 --- /dev/null +++ b/DiscUtils/Iso9660/RockRidge/PosixNameSystemUseEntry.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal sealed class PosixNameSystemUseEntry : SystemUseEntry + { + public byte Flags; + public string NameData; + + public PosixNameSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 5, 1); + + Flags = data[offset + 4]; + NameData = EndianUtilities.BytesToString(data, offset + 5, length - 5); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/RockRidge/RockRidgeExtension.cs b/DiscUtils/Iso9660/RockRidge/RockRidgeExtension.cs new file mode 100644 index 0000000..3a573df --- /dev/null +++ b/DiscUtils/Iso9660/RockRidge/RockRidgeExtension.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660 +{ + internal sealed class RockRidgeExtension : SuspExtension + { + public RockRidgeExtension(string identifier) + { + Identifier = identifier; + } + + public override string Identifier { get; } + + public override SystemUseEntry Parse(string name, byte length, byte version, byte[] data, int offset, Encoding encoding) + { + switch (name) + { + case "PX": + return new PosixFileInfoSystemUseEntry(name, length, version, data, offset); + + case "NM": + return new PosixNameSystemUseEntry(name, length, version, data, offset); + + case "CL": + return new ChildLinkSystemUseEntry(name, length, version, data, offset); + + case "TF": + return new FileTimeSystemUseEntry(name, length, version, data, offset); + + default: + return new GenericSystemUseEntry(name, length, version, data, offset); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/SupplementaryVolumeDescriptor.cs b/DiscUtils/Iso9660/SupplementaryVolumeDescriptor.cs new file mode 100644 index 0000000..bb7c6ba --- /dev/null +++ b/DiscUtils/Iso9660/SupplementaryVolumeDescriptor.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Internal; + +namespace DiscUtils.Iso9660 +{ + internal class SupplementaryVolumeDescriptor : CommonVolumeDescriptor + { + public SupplementaryVolumeDescriptor(byte[] src, int offset) + : base(src, offset, IsoUtilities.EncodingFromBytes(src, offset + 88)) {} + + public SupplementaryVolumeDescriptor( + uint volumeSpaceSize, + uint pathTableSize, + uint typeLPathTableLocation, + uint typeMPathTableLocation, + uint rootDirExtentLocation, + uint rootDirDataLength, + DateTime buildTime, + Encoding enc, + int sectorSize) + : base( + VolumeDescriptorType.Supplementary, 1, volumeSpaceSize, pathTableSize, typeLPathTableLocation, + typeMPathTableLocation, rootDirExtentLocation, rootDirDataLength, buildTime, enc, sectorSize) {} + + internal override void WriteTo(byte[] buffer, int offset) + { + base.WriteTo(buffer, offset); + IsoUtilities.WriteA1Chars(buffer, offset + 8, 32, SystemIdentifier, CharacterEncoding); + IsoUtilities.WriteString(buffer, offset + 40, 32, true, VolumeIdentifier, CharacterEncoding, true); + IsoUtilities.ToBothFromUInt32(buffer, offset + 80, VolumeSpaceSize); + IsoUtilities.EncodingToBytes(CharacterEncoding, buffer, offset + 88); + IsoUtilities.ToBothFromUInt16(buffer, offset + 120, VolumeSetSize); + IsoUtilities.ToBothFromUInt16(buffer, offset + 124, VolumeSequenceNumber); + IsoUtilities.ToBothFromUInt16(buffer, offset + 128, LogicalBlockSize); + IsoUtilities.ToBothFromUInt32(buffer, offset + 132, PathTableSize); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 140, TypeLPathTableLocation); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 144, OptionalTypeLPathTableLocation); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 148, Utilities.BitSwap(TypeMPathTableLocation)); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 152, Utilities.BitSwap(OptionalTypeMPathTableLocation)); + RootDirectory.WriteTo(buffer, offset + 156, CharacterEncoding); + IsoUtilities.WriteD1Chars(buffer, offset + 190, 129, VolumeSetIdentifier, CharacterEncoding); + IsoUtilities.WriteA1Chars(buffer, offset + 318, 129, PublisherIdentifier, CharacterEncoding); + IsoUtilities.WriteA1Chars(buffer, offset + 446, 129, DataPreparerIdentifier, CharacterEncoding); + IsoUtilities.WriteA1Chars(buffer, offset + 574, 129, ApplicationIdentifier, CharacterEncoding); + IsoUtilities.WriteD1Chars(buffer, offset + 702, 37, CopyrightFileIdentifier, CharacterEncoding); // FIXME!! + IsoUtilities.WriteD1Chars(buffer, offset + 739, 37, AbstractFileIdentifier, CharacterEncoding); // FIXME!! + IsoUtilities.WriteD1Chars(buffer, offset + 776, 37, BibliographicFileIdentifier, CharacterEncoding); + + // FIXME!! + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 813, CreationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 830, ModificationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 847, ExpirationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 864, EffectiveDateAndTime); + buffer[offset + 881] = FileStructureVersion; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/SupplementaryVolumeDescriptorRegion.cs b/DiscUtils/Iso9660/SupplementaryVolumeDescriptorRegion.cs new file mode 100644 index 0000000..07e756a --- /dev/null +++ b/DiscUtils/Iso9660/SupplementaryVolumeDescriptorRegion.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal class SupplementaryVolumeDescriptorRegion : VolumeDescriptorDiskRegion + { + private readonly SupplementaryVolumeDescriptor _descriptor; + + public SupplementaryVolumeDescriptorRegion(SupplementaryVolumeDescriptor descriptor, long start, int sectorSize) + : base(start, sectorSize) + { + _descriptor = descriptor; + } + + protected override byte[] GetBlockData() + { + byte[] buffer = new byte[Length]; + _descriptor.WriteTo(buffer, 0); + return buffer; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/ContinuationSystemUseEntry.cs b/DiscUtils/Iso9660/Susp/ContinuationSystemUseEntry.cs new file mode 100644 index 0000000..294fdb2 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/ContinuationSystemUseEntry.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal sealed class ContinuationSystemUseEntry : SystemUseEntry + { + public uint Block; + public uint BlockOffset; + public uint Length; + + public ContinuationSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 28, 1); + + Block = IsoUtilities.ToUInt32FromBoth(data, offset + 4); + BlockOffset = IsoUtilities.ToUInt32FromBoth(data, offset + 12); + Length = IsoUtilities.ToUInt32FromBoth(data, offset + 20); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/ExtensionSelectSystemUseEntry.cs b/DiscUtils/Iso9660/Susp/ExtensionSelectSystemUseEntry.cs new file mode 100644 index 0000000..2055bf3 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/ExtensionSelectSystemUseEntry.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal sealed class ExtensionSelectSystemUseEntry : SystemUseEntry + { + public byte SelectedExtension; + + public ExtensionSelectSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 5, 1); + + SelectedExtension = data[offset + 4]; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/ExtensionSystemUseEntry.cs b/DiscUtils/Iso9660/Susp/ExtensionSystemUseEntry.cs new file mode 100644 index 0000000..ce4b526 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/ExtensionSystemUseEntry.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Text; + +namespace DiscUtils.Iso9660 +{ + internal sealed class ExtensionSystemUseEntry : SystemUseEntry + { + public string ExtensionDescriptor; + public string ExtensionIdentifier; + public string ExtensionSource; + public byte ExtensionVersion; + + public ExtensionSystemUseEntry(string name, byte length, byte version, byte[] data, int offset, Encoding encoding) + { + CheckAndSetCommonProperties(name, length, version, 8, 1); + + int lenId = data[offset + 4]; + int lenDescriptor = data[offset + 5]; + int lenSource = data[offset + 6]; + + ExtensionVersion = data[offset + 7]; + + if (length < 8 + lenId + lenDescriptor + lenSource) + { + throw new InvalidDataException("Invalid SUSP ER entry - too short, only " + length + " bytes - expected: " + + (8 + lenId + lenDescriptor + lenSource)); + } + + ExtensionIdentifier = IsoUtilities.ReadChars(data, offset + 8, lenId, encoding); + ExtensionDescriptor = IsoUtilities.ReadChars(data, offset + 8 + lenId, lenDescriptor, encoding); + ExtensionSource = IsoUtilities.ReadChars(data, offset + 8 + lenId + lenDescriptor, lenSource, encoding); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/GenericSuspExtension.cs b/DiscUtils/Iso9660/Susp/GenericSuspExtension.cs new file mode 100644 index 0000000..a814b53 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/GenericSuspExtension.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660 +{ + internal sealed class GenericSuspExtension : SuspExtension + { + public GenericSuspExtension(string identifier) + { + Identifier = identifier; + } + + public override string Identifier { get; } + + public override SystemUseEntry Parse(string name, byte length, byte version, byte[] data, int offset, Encoding encoding) + { + return new GenericSystemUseEntry(name, length, version, data, offset); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/GenericSystemUseEntry.cs b/DiscUtils/Iso9660/Susp/GenericSystemUseEntry.cs new file mode 100644 index 0000000..00b3d02 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/GenericSystemUseEntry.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Iso9660 +{ + internal sealed class GenericSystemUseEntry : SystemUseEntry + { + public byte[] Data; + + public GenericSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 4, 0xFF); + + Data = new byte[length - 4]; + Array.Copy(data, offset + 4, Data, 0, length - 4); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/PaddingSystemUseEntry.cs b/DiscUtils/Iso9660/Susp/PaddingSystemUseEntry.cs new file mode 100644 index 0000000..10d8b78 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/PaddingSystemUseEntry.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal sealed class PaddingSystemUseEntry : SystemUseEntry + { + public PaddingSystemUseEntry(string name, byte length, byte version) + { + CheckAndSetCommonProperties(name, length, version, 4, 1); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/SharingProtocolSystemUseEntry.cs b/DiscUtils/Iso9660/Susp/SharingProtocolSystemUseEntry.cs new file mode 100644 index 0000000..38b96c0 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/SharingProtocolSystemUseEntry.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Iso9660 +{ + internal sealed class SharingProtocolSystemUseEntry : SystemUseEntry + { + public byte SystemAreaSkip; + + public SharingProtocolSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 7, 1); + + if (data[offset + 4] != 0xBE || data[offset + 5] != 0xEF) + { + throw new InvalidDataException("Invalid SUSP SP entry - invalid checksum bytes"); + } + + SystemAreaSkip = data[offset + 6]; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/SuspExtension.cs b/DiscUtils/Iso9660/Susp/SuspExtension.cs new file mode 100644 index 0000000..7b242c3 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/SuspExtension.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660 +{ + internal abstract class SuspExtension + { + public abstract string Identifier { get; } + + public abstract SystemUseEntry Parse(string name, byte length, byte version, byte[] data, int offset, Encoding encoding); + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/SuspRecords.cs b/DiscUtils/Iso9660/Susp/SuspRecords.cs new file mode 100644 index 0000000..34f0613 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/SuspRecords.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal sealed class SuspRecords + { + private readonly Dictionary>> _records; + + public SuspRecords(IsoContext context, byte[] data, int offset) + { + _records = new Dictionary>>(); + + ContinuationSystemUseEntry contEntry = Parse(context, data, offset + context.SuspSkipBytes); + while (contEntry != null) + { + context.DataStream.Position = contEntry.Block * (long)context.VolumeDescriptor.LogicalBlockSize + + contEntry.BlockOffset; + byte[] contData = StreamUtilities.ReadExact(context.DataStream, (int)contEntry.Length); + + contEntry = Parse(context, contData, 0); + } + } + + public static bool DetectSharingProtocol(byte[] data, int offset) + { + if (data == null || data.Length - offset < 7) + { + return false; + } + + return data[offset] == 83 + && data[offset + 1] == 80 + && data[offset + 2] == 7 + && data[offset + 3] == 1 + && data[offset + 4] == 0xBE + && data[offset + 5] == 0xEF; + } + + public List GetEntries(string extension, string name) + { + if (string.IsNullOrEmpty(extension)) + { + extension = string.Empty; + } + + Dictionary> extensionData; + if (!_records.TryGetValue(extension, out extensionData)) + { + return null; + } + + List result; + if (extensionData.TryGetValue(name, out result)) + { + return result; + } + + return null; + } + + public T GetEntry(string extension, string name) + where T : SystemUseEntry + { + List entries = GetEntries(extension, name); + if (entries == null) + { + return null; + } + + foreach (T entry in entries) + { + return entry; + } + + return null; + } + + public bool HasEntry(string extension, string name) + { + List entries = GetEntries(extension, name); + return entries != null && entries.Count != 0; + } + + private ContinuationSystemUseEntry Parse(IsoContext context, byte[] data, int offset) + { + ContinuationSystemUseEntry contEntry = null; + SuspExtension extension = null; + + if (context.SuspExtensions != null && context.SuspExtensions.Count > 0) + { + extension = context.SuspExtensions[0]; + } + + int pos = offset; + while (data.Length - pos > 4) + { + byte len; + SystemUseEntry entry = SystemUseEntry.Parse(data, pos, context.VolumeDescriptor.CharacterEncoding, + extension, out len); + pos += len; + + if (entry == null) + { + // A null entry indicates SUSP parsing must terminate. + // This will occur if a termination record is found, + // or if there is a problem with the SUSP data. + return contEntry; + } + + switch (entry.Name) + { + case "CE": + contEntry = (ContinuationSystemUseEntry)entry; + break; + + case "ES": + ExtensionSelectSystemUseEntry esEntry = (ExtensionSelectSystemUseEntry)entry; + extension = context.SuspExtensions[esEntry.SelectedExtension]; + break; + + case "PD": + break; + + case "SP": + case "ER": + StoreEntry(null, entry); + break; + + default: + StoreEntry(extension, entry); + break; + } + } + + return contEntry; + } + + private void StoreEntry(SuspExtension extension, SystemUseEntry entry) + { + string extensionId = extension == null ? string.Empty : extension.Identifier; + + Dictionary> extensionEntries; + if (!_records.TryGetValue(extensionId, out extensionEntries)) + { + extensionEntries = new Dictionary>(); + _records.Add(extensionId, extensionEntries); + } + + List entries; + if (!extensionEntries.TryGetValue(entry.Name, out entries)) + { + entries = new List(); + extensionEntries.Add(entry.Name, entries); + } + + entries.Add(entry); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/Susp/SystemUseEntry.cs b/DiscUtils/Iso9660/Susp/SystemUseEntry.cs new file mode 100644 index 0000000..5d72392 --- /dev/null +++ b/DiscUtils/Iso9660/Susp/SystemUseEntry.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal abstract class SystemUseEntry + { + public string Name; + public byte Version; + + public static SystemUseEntry Parse(byte[] data, int offset, Encoding encoding, SuspExtension extension, + out byte length) + { + if (data[offset] == 0) + { + // A zero-byte here is invalid and indicates an incorrectly written SUSP field. + // Return null to indicate to the caller that SUSP parsing is terminated. + length = 0; + + return null; + } + + string name = EndianUtilities.BytesToString(data, offset, 2); + length = data[offset + 2]; + byte version = data[offset + 3]; + + switch (name) + { + case "CE": + return new ContinuationSystemUseEntry(name, length, version, data, offset); + + case "PD": + return new PaddingSystemUseEntry(name, length, version); + + case "SP": + return new SharingProtocolSystemUseEntry(name, length, version, data, offset); + + case "ST": + // Termination entry. There's no point in storing or validating this one. + // Return null to indicate to the caller that SUSP parsing is terminated. + return null; + + case "ER": + return new ExtensionSystemUseEntry(name, length, version, data, offset, encoding); + + case "ES": + return new ExtensionSelectSystemUseEntry(name, length, version, data, offset); + + case "AA": + case "AB": + case "AS": + // Placeholder support for Apple and Amiga extension records. + return new GenericSystemUseEntry(name, length, version, data, offset); + + default: + if (extension == null) + { + return new GenericSystemUseEntry(name, length, version, data, offset); + } + + return extension.Parse(name, length, version, data, offset, encoding); + } + } + + protected void CheckAndSetCommonProperties(string name, byte length, byte version, byte minLength, byte maxVersion) + { + if (length < minLength) + { + throw new InvalidDataException("Invalid SUSP " + Name + " entry - too short, only " + length + " bytes"); + } + + if (version > maxVersion || version == 0) + { + throw new NotSupportedException("Unknown SUSP " + Name + " entry version: " + version); + } + + Name = name; + Version = version; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/VfsCDReader.cs b/DiscUtils/Iso9660/VfsCDReader.cs new file mode 100644 index 0000000..f9207ed --- /dev/null +++ b/DiscUtils/Iso9660/VfsCDReader.cs @@ -0,0 +1,530 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Streams; +using DiscUtils.Vfs; + +namespace DiscUtils.Iso9660 +{ + internal class VfsCDReader : VfsReadOnlyFileSystem, + IClusterBasedFileSystem, IUnixFileSystem + { + private static readonly Iso9660Variant[] DefaultVariantsNoJoliet = { Iso9660Variant.RockRidge, Iso9660Variant.Iso9660 }; + + private static readonly Iso9660Variant[] DefaultVariantsWithJoliet = { Iso9660Variant.Joliet, Iso9660Variant.RockRidge, Iso9660Variant.Iso9660 }; + + private byte[] _bootCatalog; + private readonly BootVolumeDescriptor _bootVolDesc; + + private readonly Stream _data; + private readonly bool _hideVersions; + protected readonly int _sectorSize; + + /// + /// Initializes a new instance of the VfsCDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + /// Hides version numbers (e.g. ";1") from the end of files. + public VfsCDReader(Stream data, bool joliet, bool hideVersions, int sectorSize) + : this(data, joliet ? DefaultVariantsWithJoliet : DefaultVariantsNoJoliet, hideVersions, sectorSize) { + } + + /// + /// Initializes a new instance of the VfsCDReader class. + /// + /// The stream to read the ISO image from. + /// Which possible file system variants to use, and with which priority. + /// Hides version numbers (e.g. ";1") from the end of files. + /// + /// + /// The implementation considers each of the file system variants in variantProperties and selects + /// the first which is determined to be present. In this example Joliet, then Rock Ridge, then vanilla + /// Iso9660 will be considered: + /// + /// + /// VfsCDReader(stream, new Iso9660Variant[] {Joliet, RockRidge, Iso9660}, true); + /// + /// The Iso9660 variant should normally be specified as the final entry in the list. Placing it earlier + /// in the list will effectively mask later items and not including it may prevent some ISOs from being read. + /// + public VfsCDReader(Stream data, Iso9660Variant[] variantPriorities, bool hideVersions, int sectorSize) + : base(new DiscFileSystemOptions()) + { + _data = data; + _sectorSize = sectorSize; + _hideVersions = hideVersions; + + long vdpos = _sectorSize * 16; // Skip lead-in + + byte[] buffer = new byte[_sectorSize]; + + long pvdPos = 0; + long svdPos = 0; + + BaseVolumeDescriptor bvd; + do + { + data.Position = vdpos; + int numRead = data.Read(buffer, 0, _sectorSize); + if (numRead != _sectorSize) + { + break; + } + + var offset = 24; + + bvd = new BaseVolumeDescriptor(buffer, offset); + + if (bvd.StandardIdentifier != BaseVolumeDescriptor.Iso9660StandardIdentifier) + { + throw new InvalidFileSystemException("Volume is not ISO-9660"); + } + + switch (bvd.VolumeDescriptorType) + { + case VolumeDescriptorType.Boot: + _bootVolDesc = new BootVolumeDescriptor(buffer, offset); + if (_bootVolDesc.SystemId != BootVolumeDescriptor.ElToritoSystemIdentifier) + { + _bootVolDesc = null; + } + + break; + + case VolumeDescriptorType.Primary: // Primary Vol Descriptor + pvdPos = vdpos; + break; + + case VolumeDescriptorType.Supplementary: // Supplementary Vol Descriptor + svdPos = vdpos; + break; + + case VolumeDescriptorType.Partition: // Volume Partition Descriptor + break; + case VolumeDescriptorType.SetTerminator: // Volume Descriptor Set Terminator + break; + } + + vdpos += _sectorSize; + } while (bvd.VolumeDescriptorType != VolumeDescriptorType.SetTerminator); + + ActiveVariant = Iso9660Variant.None; + foreach (Iso9660Variant variant in variantPriorities) + { + switch (variant) + { + case Iso9660Variant.Joliet: + if (svdPos != 0) + { + data.Position = svdPos; + data.Read(buffer, 0, _sectorSize); + SupplementaryVolumeDescriptor volDesc = new SupplementaryVolumeDescriptor(buffer, 0); + + Context = new IsoContext(_sectorSize) { VolumeDescriptor = volDesc, DataStream = _data }; + RootDirectory = new ReaderDirectory(Context, + new ReaderDirEntry(Context, volDesc.RootDirectory)); + ActiveVariant = Iso9660Variant.Iso9660; + } + + break; + + case Iso9660Variant.RockRidge: + case Iso9660Variant.Iso9660: + if (pvdPos != 0) + { + data.Position = pvdPos + 24; + data.Read(buffer, 0, _sectorSize); + PrimaryVolumeDescriptor volDesc = new PrimaryVolumeDescriptor(buffer, 0); + + volDesc.LogicalBlockSize = 2352; + + IsoContext context = new IsoContext(_sectorSize) { VolumeDescriptor = volDesc, DataStream = _data }; + DirectoryRecord rootSelfRecord = ReadRootSelfRecord(context); + + InitializeSusp(context, rootSelfRecord); + + if (variant == Iso9660Variant.Iso9660 + || + (variant == Iso9660Variant.RockRidge && + !string.IsNullOrEmpty(context.RockRidgeIdentifier))) + { + Context = context; + RootDirectory = new ReaderDirectory(context, new ReaderDirEntry(context, rootSelfRecord)); + ActiveVariant = variant; + } + } + + break; + } + + if (ActiveVariant != Iso9660Variant.None) + { + break; + } + } + + if (ActiveVariant == Iso9660Variant.None) + { + throw new IOException("None of the permitted ISO9660 file system variants was detected"); + } + } + + public Iso9660Variant ActiveVariant { get; } + + public BootDeviceEmulation BootEmulation + { + get + { + BootInitialEntry initialEntry = GetBootInitialEntry(); + if (initialEntry != null) + { + return initialEntry.BootMediaType; + } + + return BootDeviceEmulation.NoEmulation; + } + } + + public long BootImageStart + { + get + { + BootInitialEntry initialEntry = GetBootInitialEntry(); + if (initialEntry != null) + { + return initialEntry.ImageStart * _sectorSize; + } + return 0; + } + } + + public int BootLoadSegment + { + get + { + BootInitialEntry initialEntry = GetBootInitialEntry(); + if (initialEntry != null) + { + return initialEntry.LoadSegment; + } + + return 0; + } + } + + /// + /// Provides the friendly name for the CD filesystem. + /// + public override string FriendlyName + { + get { return "ISO 9660 (CD-ROM)"; } + } + + public bool HasBootImage + { + get + { + if (_bootVolDesc == null) + { + return false; + } + + byte[] bootCatalog = GetBootCatalog(); + if (bootCatalog == null) + { + return false; + } + + BootValidationEntry entry = new BootValidationEntry(bootCatalog, 0); + return entry.ChecksumValid; + } + } + + /// + /// Gets the Volume Identifier. + /// + public override string VolumeLabel + { + get { return Context.VolumeDescriptor.VolumeIdentifier; } + } + + public long ClusterSize + { + get { return _sectorSize; } + } + + public long TotalClusters + { + get { return Context.VolumeDescriptor.VolumeSpaceSize; } + } + + public long ClusterToOffset(long cluster) + { + return cluster * ClusterSize; + } + + public long OffsetToCluster(long offset) + { + return offset / ClusterSize; + } + + /// + /// Size of the Filesystem in bytes + /// + public override long Size + { + get { throw new NotSupportedException("Filesystem size is not (yet) supported"); } + } + + /// + /// Used space of the Filesystem in bytes + /// + public override long UsedSpace + { + get { throw new NotSupportedException("Filesystem size is not (yet) supported"); } + } + + /// + /// Available space of the Filesystem in bytes + /// + public override long AvailableSpace + { + get { throw new NotSupportedException("Filesystem size is not (yet) supported"); } + } + + public Range[] PathToClusters(string path) + { + ReaderDirEntry entry = GetDirectoryEntry(path); + if (entry == null) + { + throw new FileNotFoundException("File not found", path); + } + + if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + { + throw new NotSupportedException("Non-contiguous extents not supported"); + } + + return new[] + { + new Range(entry.Record.LocationOfExtent, + MathUtilities.Ceil(entry.Record.DataLength, _sectorSize)) + }; + } + + public StreamExtent[] PathToExtents(string path) + { + ReaderDirEntry entry = GetDirectoryEntry(path); + if (entry == null) + { + throw new FileNotFoundException("File not found", path); + } + + if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + { + throw new NotSupportedException("Non-contiguous extents not supported"); + } + + return new[] + { new StreamExtent(entry.Record.LocationOfExtent * _sectorSize, entry.Record.DataLength) }; + } + + public ClusterMap BuildClusterMap() + { + long totalClusters = TotalClusters; + ClusterRoles[] clusterToRole = new ClusterRoles[totalClusters]; + object[] clusterToFileId = new object[totalClusters]; + Dictionary fileIdToPaths = new Dictionary(); + + ForAllDirEntries( + string.Empty, + (path, entry) => + { + string[] paths = null; + if (fileIdToPaths.ContainsKey(entry.UniqueCacheId)) + { + paths = fileIdToPaths[entry.UniqueCacheId]; + } + + if (paths == null) + { + fileIdToPaths[entry.UniqueCacheId] = new[] { path }; + } + else + { + string[] newPaths = new string[paths.Length + 1]; + Array.Copy(paths, newPaths, paths.Length); + newPaths[paths.Length] = path; + fileIdToPaths[entry.UniqueCacheId] = newPaths; + } + + if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + { + throw new NotSupportedException("Non-contiguous extents not supported"); + } + + long clusters = MathUtilities.Ceil(entry.Record.DataLength, _sectorSize); + for (long i = 0; i < clusters; ++i) + { + clusterToRole[i + entry.Record.LocationOfExtent] = ClusterRoles.DataFile; + clusterToFileId[i + entry.Record.LocationOfExtent] = entry.UniqueCacheId; + } + }); + + return new ClusterMap(clusterToRole, clusterToFileId, fileIdToPaths); + } + + public UnixFileSystemInfo GetUnixFileInfo(string path) + { + File file = GetFile(path); + return file.UnixFileInfo; + } + + public Stream OpenBootImage() + { + BootInitialEntry initialEntry = GetBootInitialEntry(); + if (initialEntry != null) + { + return new SubStream(_data, initialEntry.ImageStart * _sectorSize, + initialEntry.SectorCount * Sizes.Sector); + } + throw new InvalidOperationException("No valid boot image"); + } + + protected override File ConvertDirEntryToFile(ReaderDirEntry dirEntry) + { + if (dirEntry.IsDirectory) + { + return new ReaderDirectory(Context, dirEntry); + } + return new File(Context, dirEntry); + } + + protected override string FormatFileName(string name) + { + if (_hideVersions) + { + int pos = name.LastIndexOf(';'); + if (pos > 0) + { + return name.Substring(0, pos); + } + } + + return name; + } + + private static void InitializeSusp(IsoContext context, DirectoryRecord rootSelfRecord) + { + // Stage 1 - SUSP present? + List extensions = new List(); + if (!SuspRecords.DetectSharingProtocol(rootSelfRecord.SystemUseData, 0)) + { + context.SuspExtensions = new List(); + context.SuspDetected = false; + return; + } + context.SuspDetected = true; + + SuspRecords suspRecords = new SuspRecords(context, rootSelfRecord.SystemUseData, 0); + + // Stage 2 - Init general SUSP params + SharingProtocolSystemUseEntry spEntry = + (SharingProtocolSystemUseEntry)suspRecords.GetEntries(null, "SP")[0]; + context.SuspSkipBytes = spEntry.SystemAreaSkip; + + // Stage 3 - Init extensions + List extensionEntries = suspRecords.GetEntries(null, "ER"); + if (extensionEntries != null) + { + foreach (ExtensionSystemUseEntry extension in extensionEntries) + { + switch (extension.ExtensionIdentifier) + { + case "RRIP_1991A": + case "IEEE_P1282": + case "IEEE_1282": + extensions.Add(new RockRidgeExtension(extension.ExtensionIdentifier)); + context.RockRidgeIdentifier = extension.ExtensionIdentifier; + break; + + default: + extensions.Add(new GenericSuspExtension(extension.ExtensionIdentifier)); + break; + } + } + } + else if (suspRecords.GetEntries(null, "RR") != null) + { + // Some ISO creators don't add the 'ER' record for RockRidge, but write the (legacy) + // RR record anyway + extensions.Add(new RockRidgeExtension("RRIP_1991A")); + context.RockRidgeIdentifier = "RRIP_1991A"; + } + + context.SuspExtensions = extensions; + } + + private static DirectoryRecord ReadRootSelfRecord(IsoContext context) + { + context.DataStream.Position = context.VolumeDescriptor.RootDirectory.LocationOfExtent * + context.VolumeDescriptor.LogicalBlockSize + 24; + byte[] firstSector = StreamUtilities.ReadExact(context.DataStream, context.VolumeDescriptor.LogicalBlockSize); + + DirectoryRecord rootSelfRecord; + DirectoryRecord.ReadFrom(firstSector, 0, context.VolumeDescriptor.CharacterEncoding, out rootSelfRecord); + return rootSelfRecord; + } + + private BootInitialEntry GetBootInitialEntry() + { + byte[] bootCatalog = GetBootCatalog(); + if (bootCatalog == null) + { + return null; + } + + BootValidationEntry validationEntry = new BootValidationEntry(bootCatalog, 0); + if (!validationEntry.ChecksumValid) + { + return null; + } + + return new BootInitialEntry(bootCatalog, 0x20); + } + + private byte[] GetBootCatalog() + { + if (_bootCatalog == null && _bootVolDesc != null) + { + _data.Position = _bootVolDesc.CatalogSector * _sectorSize; + _bootCatalog = StreamUtilities.ReadExact(_data, _sectorSize); + } + + return _bootCatalog; + } + } +} diff --git a/DiscUtils/Iso9660/VolumeDescriptorDiskRegion.cs b/DiscUtils/Iso9660/VolumeDescriptorDiskRegion.cs new file mode 100644 index 0000000..ad89652 --- /dev/null +++ b/DiscUtils/Iso9660/VolumeDescriptorDiskRegion.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660 +{ + internal abstract class VolumeDescriptorDiskRegion : BuilderExtent + { + private byte[] _readCache; + + public VolumeDescriptorDiskRegion(long start, int sectorSize) + : base(start, sectorSize) {} + + public override void Dispose() {} + + public override void PrepareForRead() + { + _readCache = GetBlockData(); + } + + public override int Read(long diskOffset, byte[] buffer, int offset, int count) + { + long relPos = diskOffset - Start; + + int numRead = (int)Math.Min(count, _readCache.Length - relPos); + + Array.Copy(_readCache, (int)relPos, buffer, offset, numRead); + + return numRead; + } + + public override void DisposeReadState() + { + _readCache = null; + } + + protected abstract byte[] GetBlockData(); + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/VolumeDescriptorSetTerminator.cs b/DiscUtils/Iso9660/VolumeDescriptorSetTerminator.cs new file mode 100644 index 0000000..fb247d2 --- /dev/null +++ b/DiscUtils/Iso9660/VolumeDescriptorSetTerminator.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal class VolumeDescriptorSetTerminator : BaseVolumeDescriptor + { + public VolumeDescriptorSetTerminator(int sectorSize) + : base(VolumeDescriptorType.SetTerminator, 1, sectorSize) {} + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/VolumeDescriptorSetTerminatorRegion.cs b/DiscUtils/Iso9660/VolumeDescriptorSetTerminatorRegion.cs new file mode 100644 index 0000000..743bfc4 --- /dev/null +++ b/DiscUtils/Iso9660/VolumeDescriptorSetTerminatorRegion.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660 +{ + internal class VolumeDescriptorSetTerminatorRegion : VolumeDescriptorDiskRegion + { + private readonly VolumeDescriptorSetTerminator _descriptor; + + public VolumeDescriptorSetTerminatorRegion(VolumeDescriptorSetTerminator descriptor, long start, int sectorSize) + : base(start, sectorSize) + { + _descriptor = descriptor; + } + + protected override byte[] GetBlockData() + { + byte[] buffer = new byte[Length]; + _descriptor.WriteTo(buffer, 0); + return buffer; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660/VolumeDescriptorType.cs b/DiscUtils/Iso9660/VolumeDescriptorType.cs new file mode 100644 index 0000000..8798d89 --- /dev/null +++ b/DiscUtils/Iso9660/VolumeDescriptorType.cs @@ -0,0 +1,11 @@ +namespace DiscUtils.Iso9660 +{ + internal enum VolumeDescriptorType : byte + { + Boot = 0, + Primary = 1, + Supplementary = 2, + Partition = 3, + SetTerminator = 255 + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/AligningStream.cs b/DiscUtils/Streams/AligningStream.cs new file mode 100644 index 0000000..e3a37f6 --- /dev/null +++ b/DiscUtils/Streams/AligningStream.cs @@ -0,0 +1,180 @@ +// +// Copyright (c) 2008-2012, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Aligns I/O to a given block size. + /// + /// Uses the read-modify-write pattern to align I/O. + public sealed class AligningStream : WrappingMappedStream + { + private readonly byte[] _alignmentBuffer; + private readonly int _blockSize; + private long _position; + + public AligningStream(SparseStream toWrap, Ownership ownership, int blockSize) + : base(toWrap, ownership, null) + { + _blockSize = blockSize; + _alignmentBuffer = new byte[blockSize]; + } + + public override long Position + { + get { return _position; } + set { _position = value; } + } + + public override int Read(byte[] buffer, int offset, int count) + { + int startOffset = (int)(_position % _blockSize); + if (startOffset == 0 && (count % _blockSize == 0 || _position + count == Length)) + { + // Aligned read - pass through to underlying stream. + WrappedStream.Position = _position; + int numRead = WrappedStream.Read(buffer, offset, count); + _position += numRead; + return numRead; + } + + long startPos = MathUtilities.RoundDown(_position, _blockSize); + long endPos = MathUtilities.RoundUp(_position + count, _blockSize); + + if (endPos - startPos > int.MaxValue) + { + throw new IOException("Oversized read, after alignment"); + } + + byte[] tempBuffer = new byte[endPos - startPos]; + + WrappedStream.Position = startPos; + int read = WrappedStream.Read(tempBuffer, 0, tempBuffer.Length); + int available = Math.Min(count, read - startOffset); + + Array.Copy(tempBuffer, startOffset, buffer, offset, available); + + _position += available; + return available; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += Length; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of stream"); + } + _position = effectiveOffset; + return _position; + } + + public override void Clear(int count) + { + DoOperation( + (s, opOffset, opCount) => { s.Clear(opCount); }, + (buffer, offset, opOffset, opCount) => { Array.Clear(buffer, offset, opCount); }, + count); + } + + public override void Write(byte[] buffer, int offset, int count) + { + DoOperation( + (s, opOffset, opCount) => { s.Write(buffer, offset + opOffset, opCount); }, + (tempBuffer, tempOffset, opOffset, opCount) => { Array.Copy(buffer, offset + opOffset, tempBuffer, tempOffset, opCount); }, + count); + } + + private void DoOperation(ModifyStream modifyStream, ModifyBuffer modifyBuffer, int count) + { + int startOffset = (int)(_position % _blockSize); + if (startOffset == 0 && (count % _blockSize == 0 || _position + count == Length)) + { + WrappedStream.Position = _position; + modifyStream(WrappedStream, 0, count); + _position += count; + return; + } + + long unalignedEnd = _position + count; + long alignedPos = MathUtilities.RoundDown(_position, _blockSize); + + if (startOffset != 0) + { + WrappedStream.Position = alignedPos; + WrappedStream.Read(_alignmentBuffer, 0, _blockSize); + + modifyBuffer(_alignmentBuffer, startOffset, 0, Math.Min(count, _blockSize - startOffset)); + + WrappedStream.Position = alignedPos; + WrappedStream.Write(_alignmentBuffer, 0, _blockSize); + } + + alignedPos = MathUtilities.RoundUp(_position, _blockSize); + if (alignedPos >= unalignedEnd) + { + _position = unalignedEnd; + return; + } + + int passthroughLength = (int)MathUtilities.RoundDown(_position + count - alignedPos, _blockSize); + if (passthroughLength > 0) + { + WrappedStream.Position = alignedPos; + modifyStream(WrappedStream, (int)(alignedPos - _position), passthroughLength); + } + + alignedPos += passthroughLength; + if (alignedPos >= unalignedEnd) + { + _position = unalignedEnd; + return; + } + + WrappedStream.Position = alignedPos; + WrappedStream.Read(_alignmentBuffer, 0, _blockSize); + + modifyBuffer(_alignmentBuffer, 0, (int)(alignedPos - _position), (int)Math.Min(count - (alignedPos - _position), unalignedEnd - alignedPos)); + + WrappedStream.Position = alignedPos; + WrappedStream.Write(_alignmentBuffer, 0, _blockSize); + + _position = unalignedEnd; + } + + private delegate void ModifyStream(SparseStream stream, int opOffset, int count); + + private delegate void ModifyBuffer(byte[] buffer, int offset, int opOffset, int count); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Block/Block.cs b/DiscUtils/Streams/Block/Block.cs new file mode 100644 index 0000000..df2383b --- /dev/null +++ b/DiscUtils/Streams/Block/Block.cs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Streams +{ + public class Block + { + public int Available { get; set; } + + public byte[] Data { get; set; } + + public long Position { get; set; } + + public bool Equals(Block other) + { + return Position == other.Position; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Block/BlockCache.cs b/DiscUtils/Streams/Block/BlockCache.cs new file mode 100644 index 0000000..596def4 --- /dev/null +++ b/DiscUtils/Streams/Block/BlockCache.cs @@ -0,0 +1,132 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; + +namespace DiscUtils.Streams +{ + public class BlockCache + where T : Block, new() + { + private readonly Dictionary _blocks; + private int _blocksCreated; + private readonly int _blockSize; + + private readonly List _freeBlocks; + private readonly LinkedList _lru; + private readonly int _totalBlocks; + + public BlockCache(int blockSize, int blockCount) + { + _blockSize = blockSize; + _totalBlocks = blockCount; + + _blocks = new Dictionary(); + _lru = new LinkedList(); + _freeBlocks = new List(_totalBlocks); + + FreeBlockCount = _totalBlocks; + } + + public int FreeBlockCount { get; private set; } + + public bool ContainsBlock(long position) + { + return _blocks.ContainsKey(position); + } + + public bool TryGetBlock(long position, out T block) + { + if (_blocks.TryGetValue(position, out block)) + { + _lru.Remove(block); + _lru.AddFirst(block); + return true; + } + + return false; + } + + public T GetBlock(long position) + { + T result; + + if (TryGetBlock(position, out result)) + { + return result; + } + + result = GetFreeBlock(); + result.Position = position; + result.Available = -1; + StoreBlock(result); + + return result; + } + + public void ReleaseBlock(long position) + { + T block; + if (_blocks.TryGetValue(position, out block)) + { + _blocks.Remove(position); + _lru.Remove(block); + _freeBlocks.Add(block); + FreeBlockCount++; + } + } + + private void StoreBlock(T block) + { + _blocks[block.Position] = block; + _lru.AddFirst(block); + } + + private T GetFreeBlock() + { + T block; + + if (_freeBlocks.Count > 0) + { + int idx = _freeBlocks.Count - 1; + block = _freeBlocks[idx]; + _freeBlocks.RemoveAt(idx); + FreeBlockCount--; + } + else if (_blocksCreated < _totalBlocks) + { + block = new T(); + block.Data = new byte[_blockSize]; + _blocksCreated++; + FreeBlockCount--; + } + else + { + block = _lru.Last.Value; + _lru.RemoveLast(); + _blocks.Remove(block.Position); + } + + return block; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Block/BlockCacheSettings.cs b/DiscUtils/Streams/Block/BlockCacheSettings.cs new file mode 100644 index 0000000..7c444ef --- /dev/null +++ b/DiscUtils/Streams/Block/BlockCacheSettings.cs @@ -0,0 +1,77 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Streams +{ + /// + /// Settings controlling BlockCache instances. + /// + public sealed class BlockCacheSettings + { + /// + /// Initializes a new instance of the BlockCacheSettings class. + /// + public BlockCacheSettings() + { + BlockSize = (int)(4 * Sizes.OneKiB); + ReadCacheSize = 4 * Sizes.OneMiB; + LargeReadSize = 64 * Sizes.OneKiB; + OptimumReadSize = (int)(64 * Sizes.OneKiB); + } + + /// + /// Initializes a new instance of the BlockCacheSettings class. + /// + /// The cache settings. + internal BlockCacheSettings(BlockCacheSettings settings) + { + BlockSize = settings.BlockSize; + ReadCacheSize = settings.ReadCacheSize; + LargeReadSize = settings.LargeReadSize; + OptimumReadSize = settings.OptimumReadSize; + } + + /// + /// Gets or sets the size (in bytes) of each cached block. + /// + public int BlockSize { get; set; } + + /// + /// Gets or sets the maximum read size that will be cached. + /// + /// Large reads are not cached, on the assumption they will not + /// be repeated. This setting controls what is considered 'large'. + /// Any read that is more than this many bytes will not be cached. + public long LargeReadSize { get; set; } + + /// + /// Gets or sets the optimum size of a read to the wrapped stream. + /// + /// This value must be a multiple of BlockSize. + public int OptimumReadSize { get; set; } + + /// + /// Gets or sets the size (in bytes) of the read cache. + /// + public long ReadCacheSize { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Block/BlockCacheStatistics.cs b/DiscUtils/Streams/Block/BlockCacheStatistics.cs new file mode 100644 index 0000000..7d460a2 --- /dev/null +++ b/DiscUtils/Streams/Block/BlockCacheStatistics.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Streams +{ + /// + /// Statistical information about the effectiveness of a BlockCache instance. + /// + public sealed class BlockCacheStatistics + { + /// + /// Gets the number of free blocks in the read cache. + /// + public int FreeReadBlocks { get; internal set; } + + /// + /// Gets the number of requested 'large' reads, as defined by the LargeReadSize setting. + /// + public long LargeReadsIn { get; internal set; } + + /// + /// Gets the number of times a read request was serviced (in part or whole) from the cache. + /// + public long ReadCacheHits { get; internal set; } + + /// + /// Gets the number of time a read request was serviced (in part or whole) from the wrapped stream. + /// + public long ReadCacheMisses { get; internal set; } + + /// + /// Gets the total number of requested reads. + /// + public long TotalReadsIn { get; internal set; } + + /// + /// Gets the total number of reads passed on by the cache. + /// + public long TotalReadsOut { get; internal set; } + + /// + /// Gets the total number of requested writes. + /// + public long TotalWritesIn { get; internal set; } + + /// + /// Gets the number of requested unaligned reads. + /// + /// Unaligned reads are reads where the read doesn't start on a multiple of + /// the block size. + public long UnalignedReadsIn { get; internal set; } + + /// + /// Gets the number of requested unaligned writes. + /// + /// Unaligned writes are writes where the write doesn't start on a multiple of + /// the block size. + public long UnalignedWritesIn { get; internal set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Block/BlockCacheStream.cs b/DiscUtils/Streams/Block/BlockCacheStream.cs new file mode 100644 index 0000000..15c3005 --- /dev/null +++ b/DiscUtils/Streams/Block/BlockCacheStream.cs @@ -0,0 +1,461 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + /// + /// A stream implementing a block-oriented read cache. + /// + public sealed class BlockCacheStream : SparseStream + { + private bool _atEof; + private readonly int _blocksInReadBuffer; + + private readonly BlockCache _cache; + private readonly Ownership _ownWrapped; + + private long _position; + private readonly byte[] _readBuffer; + private readonly BlockCacheSettings _settings; + private readonly BlockCacheStatistics _stats; + private SparseStream _wrappedStream; + + /// + /// Initializes a new instance of the BlockCacheStream class. + /// + /// The stream to wrap. + /// Whether to assume ownership of toWrap. + public BlockCacheStream(SparseStream toWrap, Ownership ownership) + : this(toWrap, ownership, new BlockCacheSettings()) {} + + /// + /// Initializes a new instance of the BlockCacheStream class. + /// + /// The stream to wrap. + /// Whether to assume ownership of toWrap. + /// The cache settings. + public BlockCacheStream(SparseStream toWrap, Ownership ownership, BlockCacheSettings settings) + { + if (!toWrap.CanRead) + { + throw new ArgumentException("The wrapped stream does not support reading", nameof(toWrap)); + } + + if (!toWrap.CanSeek) + { + throw new ArgumentException("The wrapped stream does not support seeking", nameof(toWrap)); + } + + _wrappedStream = toWrap; + _ownWrapped = ownership; + _settings = new BlockCacheSettings(settings); + + if (_settings.OptimumReadSize % _settings.BlockSize != 0) + { + throw new ArgumentException("Invalid settings, OptimumReadSize must be a multiple of BlockSize", + nameof(settings)); + } + + _readBuffer = new byte[_settings.OptimumReadSize]; + _blocksInReadBuffer = _settings.OptimumReadSize / _settings.BlockSize; + + int totalBlocks = (int)(_settings.ReadCacheSize / _settings.BlockSize); + + _cache = new BlockCache(_settings.BlockSize, totalBlocks); + _stats = new BlockCacheStatistics(); + _stats.FreeReadBlocks = totalBlocks; + } + + /// + /// Gets an indication as to whether the stream can be read. + /// + public override bool CanRead + { + get { return true; } + } + + /// + /// Gets an indication as to whether the stream position can be changed. + /// + public override bool CanSeek + { + get { return true; } + } + + /// + /// Gets an indication as to whether the stream can be written to. + /// + public override bool CanWrite + { + get { return _wrappedStream.CanWrite; } + } + + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public override IEnumerable Extents + { + get + { + CheckDisposed(); + return _wrappedStream.Extents; + } + } + + /// + /// Gets the length of the stream. + /// + public override long Length + { + get + { + CheckDisposed(); + return _wrappedStream.Length; + } + } + + /// + /// Gets and sets the current stream position. + /// + public override long Position + { + get + { + CheckDisposed(); + return _position; + } + + set + { + CheckDisposed(); + _position = value; + } + } + + /// + /// Gets the performance statistics for this instance. + /// + public BlockCacheStatistics Statistics + { + get + { + _stats.FreeReadBlocks = _cache.FreeBlockCount; + return _stats; + } + } + + /// + /// Gets the parts of a stream that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + CheckDisposed(); + return _wrappedStream.GetExtentsInRange(start, count); + } + + /// + /// Reads data from the stream. + /// + /// The buffer to fill. + /// The buffer offset to start from. + /// The number of bytes to read. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + CheckDisposed(); + + if (_position >= Length) + { + if (_atEof) + { + throw new IOException("Attempt to read beyond end of stream"); + } + _atEof = true; + return 0; + } + + _stats.TotalReadsIn++; + + if (count > _settings.LargeReadSize) + { + _stats.LargeReadsIn++; + _stats.TotalReadsOut++; + _wrappedStream.Position = _position; + int numRead = _wrappedStream.Read(buffer, offset, count); + _position = _wrappedStream.Position; + + if (_position >= Length) + { + _atEof = true; + } + + return numRead; + } + + int totalBytesRead = 0; + bool servicedFromCache = false; + bool servicedOutsideCache = false; + int blockSize = _settings.BlockSize; + + long firstBlock = _position / blockSize; + int offsetInNextBlock = (int)(_position % blockSize); + long endBlock = MathUtilities.Ceil(Math.Min(_position + count, Length), blockSize); + int numBlocks = (int)(endBlock - firstBlock); + + if (offsetInNextBlock != 0) + { + _stats.UnalignedReadsIn++; + } + + int blocksRead = 0; + while (blocksRead < numBlocks) + { + Block block; + + // Read from the cache as much as possible + while (blocksRead < numBlocks && _cache.TryGetBlock(firstBlock + blocksRead, out block)) + { + int bytesToRead = Math.Min(count - totalBytesRead, block.Available - offsetInNextBlock); + + Array.Copy(block.Data, offsetInNextBlock, buffer, offset + totalBytesRead, bytesToRead); + offsetInNextBlock = 0; + totalBytesRead += bytesToRead; + _position += bytesToRead; + blocksRead++; + + servicedFromCache = true; + } + + // Now handle a sequence of (one or more) blocks that are not cached + if (blocksRead < numBlocks && !_cache.ContainsBlock(firstBlock + blocksRead)) + { + servicedOutsideCache = true; + + // Figure out how many blocks to read from the wrapped stream + int blocksToRead = 0; + while (blocksRead + blocksToRead < numBlocks + && blocksToRead < _blocksInReadBuffer + && !_cache.ContainsBlock(firstBlock + blocksRead + blocksToRead)) + { + ++blocksToRead; + } + + // Allow for the end of the stream not being block-aligned + long readPosition = (firstBlock + blocksRead) * blockSize; + int bytesRead = (int)Math.Min(blocksToRead * (long)blockSize, Length - readPosition); + + // Do the read + _stats.TotalReadsOut++; + _wrappedStream.Position = readPosition; + StreamUtilities.ReadExact(_wrappedStream, _readBuffer, 0, bytesRead); + + // Cache the read blocks + for (int i = 0; i < blocksToRead; ++i) + { + int copyBytes = Math.Min(blockSize, bytesRead - i * blockSize); + block = _cache.GetBlock(firstBlock + blocksRead + i); + Array.Copy(_readBuffer, i * blockSize, block.Data, 0, copyBytes); + block.Available = copyBytes; + + if (copyBytes < blockSize) + { + Array.Clear(_readBuffer, i * blockSize + copyBytes, blockSize - copyBytes); + } + } + + blocksRead += blocksToRead; + + // Propogate the data onto the caller + int bytesToCopy = Math.Min(count - totalBytesRead, bytesRead - offsetInNextBlock); + Array.Copy(_readBuffer, offsetInNextBlock, buffer, offset + totalBytesRead, bytesToCopy); + totalBytesRead += bytesToCopy; + _position += bytesToCopy; + offsetInNextBlock = 0; + } + } + + if (_position >= Length && totalBytesRead == 0) + { + _atEof = true; + } + + if (servicedFromCache) + { + _stats.ReadCacheHits++; + } + + if (servicedOutsideCache) + { + _stats.ReadCacheMisses++; + } + + return totalBytesRead; + } + + /// + /// Flushes the stream. + /// + public override void Flush() + { + CheckDisposed(); + _wrappedStream.Flush(); + } + + /// + /// Moves the stream position. + /// + /// The origin-relative location. + /// The base location. + /// The new absolute stream position. + public override long Seek(long offset, SeekOrigin origin) + { + CheckDisposed(); + + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += Length; + } + + _atEof = false; + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of disk"); + } + _position = effectiveOffset; + return _position; + } + + /// + /// Sets the length of the stream. + /// + /// The new length. + public override void SetLength(long value) + { + CheckDisposed(); + _wrappedStream.SetLength(value); + } + + /// + /// Writes data to the stream at the current location. + /// + /// The data to write. + /// The first byte to write from buffer. + /// The number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + CheckDisposed(); + + _stats.TotalWritesIn++; + + int blockSize = _settings.BlockSize; + long firstBlock = _position / blockSize; + long endBlock = MathUtilities.Ceil(Math.Min(_position + count, Length), blockSize); + int numBlocks = (int)(endBlock - firstBlock); + + try + { + _wrappedStream.Position = _position; + _wrappedStream.Write(buffer, offset, count); + } + catch + { + InvalidateBlocks(firstBlock, numBlocks); + throw; + } + + int offsetInNextBlock = (int)(_position % blockSize); + if (offsetInNextBlock != 0) + { + _stats.UnalignedWritesIn++; + } + + // For each block touched, if it's cached, update it + int bytesProcessed = 0; + for (int i = 0; i < numBlocks; ++i) + { + int bufferPos = offset + bytesProcessed; + int bytesThisBlock = Math.Min(count - bytesProcessed, blockSize - offsetInNextBlock); + + Block block; + if (_cache.TryGetBlock(firstBlock + i, out block)) + { + Array.Copy(buffer, bufferPos, block.Data, offsetInNextBlock, bytesThisBlock); + block.Available = Math.Max(block.Available, offsetInNextBlock + bytesThisBlock); + } + + offsetInNextBlock = 0; + bytesProcessed += bytesThisBlock; + } + + _position += count; + } + + /// + /// Disposes of this instance, freeing up associated resources. + /// + /// true if invoked from Dispose, else false. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_wrappedStream != null && _ownWrapped == Ownership.Dispose) + { + _wrappedStream.Dispose(); + } + + _wrappedStream = null; + } + + base.Dispose(disposing); + } + + private void CheckDisposed() + { + if (_wrappedStream == null) + { + throw new ObjectDisposedException("BlockCacheStream"); + } + } + + private void InvalidateBlocks(long firstBlock, int numBlocks) + { + for (long i = firstBlock; i < firstBlock + numBlocks; ++i) + { + _cache.ReleaseBlock(i); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Buffer/Buffer.cs b/DiscUtils/Streams/Buffer/Buffer.cs new file mode 100644 index 0000000..e2c5375 --- /dev/null +++ b/DiscUtils/Streams/Buffer/Buffer.cs @@ -0,0 +1,121 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +#if !NETSTANDARD +using System; +#endif + +namespace DiscUtils.Streams +{ + /// + /// Abstract base class for implementations of IBuffer. + /// + public abstract class Buffer : +#if !NETSTANDARD + MarshalByRefObject, +#endif + IBuffer + { + /// + /// Gets a value indicating whether this buffer can be read. + /// + public abstract bool CanRead { get; } + + /// + /// Gets a value indicating whether this buffer can be modified. + /// + public abstract bool CanWrite { get; } + + /// + /// Gets the current capacity of the buffer, in bytes. + /// + public abstract long Capacity { get; } + + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public virtual IEnumerable Extents + { + get { return GetExtentsInRange(0, Capacity); } + } + + /// + /// Reads from the buffer into a byte array. + /// + /// The offset within the buffer to start reading. + /// The destination byte array. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The actual number of bytes read. + public abstract int Read(long pos, byte[] buffer, int offset, int count); + + /// + /// Writes a byte array into the buffer. + /// + /// The start offset within the buffer. + /// The source byte array. + /// The start offset within the source byte array. + /// The number of bytes to write. + public abstract void Write(long pos, byte[] buffer, int offset, int count); + + /// + /// Clears bytes from the buffer. + /// + /// The start offset within the buffer. + /// The number of bytes to clear. + /// + /// Logically equivalent to writing count null/zero bytes to the buffer, some + /// implementations determine that some (or all) of the range indicated is not actually + /// stored. There is no direct, automatic, correspondence to clearing bytes and them + /// not being represented as an 'extent' - for example, the implementation of the underlying + /// stream may not permit fine-grained extent storage. + /// It is always safe to call this method to 'zero-out' a section of a buffer, regardless of + /// the underlying buffer implementation. + /// + public virtual void Clear(long pos, int count) + { + Write(pos, new byte[count], 0, count); + } + + /// + /// Flushes all data to the underlying storage. + /// + /// The default behaviour, implemented by this class, is to take no action. + public virtual void Flush() {} + + /// + /// Sets the capacity of the buffer, truncating if appropriate. + /// + /// The desired capacity of the buffer. + public abstract void SetCapacity(long value); + + /// + /// Gets the parts of a buffer that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public abstract IEnumerable GetExtentsInRange(long start, long count); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Buffer/BufferStream.cs b/DiscUtils/Streams/Buffer/BufferStream.cs new file mode 100644 index 0000000..57aa4f5 --- /dev/null +++ b/DiscUtils/Streams/Buffer/BufferStream.cs @@ -0,0 +1,213 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; + +namespace DiscUtils.Streams +{ + /// + /// Converts a Buffer into a Stream. + /// + public class BufferStream : SparseStream + { + private readonly FileAccess _access; + private readonly IBuffer _buffer; + + private long _position; + + /// + /// Initializes a new instance of the BufferStream class. + /// + /// The buffer to use. + /// The access permitted to clients. + public BufferStream(IBuffer buffer, FileAccess access) + { + _buffer = buffer; + _access = access; + } + + /// + /// Gets an indication of whether read access is permitted. + /// + public override bool CanRead + { + get { return _access != FileAccess.Write; } + } + + /// + /// Gets an indication of whether seeking is permitted. + /// + public override bool CanSeek + { + get { return true; } + } + + /// + /// Gets an indication of whether write access is permitted. + /// + public override bool CanWrite + { + get { return _access != FileAccess.Read; } + } + + /// + /// Gets the stored extents within the sparse stream. + /// + public override IEnumerable Extents + { + get { return _buffer.Extents; } + } + + /// + /// Gets the length of the stream (the capacity of the underlying buffer). + /// + public override long Length + { + get { return _buffer.Capacity; } + } + + /// + /// Gets and sets the current position within the stream. + /// + public override long Position + { + get { return _position; } + set { _position = value; } + } + + /// + /// Flushes all data to the underlying storage. + /// + public override void Flush() {} + + /// + /// Reads a number of bytes from the stream. + /// + /// The destination buffer. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + if (!CanRead) + { + throw new IOException("Attempt to read from write-only stream"); + } + + StreamUtilities.AssertBufferParameters(buffer, offset, count); + + int numRead = _buffer.Read(_position, buffer, offset, count); + _position += numRead; + return numRead; + } + + /// + /// Changes the current stream position. + /// + /// The origin-relative stream position. + /// The origin for the stream position. + /// The new stream position. + public override long Seek(long offset, SeekOrigin origin) + { + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += _buffer.Capacity; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of disk"); + } + _position = effectiveOffset; + return _position; + } + + /// + /// Sets the length of the stream (the underlying buffer's capacity). + /// + /// The new length of the stream. + public override void SetLength(long value) + { + _buffer.SetCapacity(value); + } + + /// + /// Writes a buffer to the stream. + /// + /// The buffer to write. + /// The starting offset within buffer. + /// The number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + if (!CanWrite) + { + throw new IOException("Attempt to write to read-only stream"); + } + + StreamUtilities.AssertBufferParameters(buffer, offset, count); + + _buffer.Write(_position, buffer, offset, count); + _position += count; + } + + /// + /// Clears bytes from the stream. + /// + /// The number of bytes (from the current position) to clear. + /// + /// Logically equivalent to writing count null/zero bytes to the stream, some + /// implementations determine that some (or all) of the range indicated is not actually + /// stored. There is no direct, automatic, correspondence to clearing bytes and them + /// not being represented as an 'extent' - for example, the implementation of the underlying + /// stream may not permit fine-grained extent storage. + /// It is always safe to call this method to 'zero-out' a section of a stream, regardless of + /// the underlying stream implementation. + /// + public override void Clear(int count) + { + if (!CanWrite) + { + throw new IOException("Attempt to erase bytes in a read-only stream"); + } + + _buffer.Clear(_position, count); + _position += count; + } + + /// + /// Gets the parts of a stream that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + return _buffer.GetExtentsInRange(start, count); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Buffer/IBuffer.cs b/DiscUtils/Streams/Buffer/IBuffer.cs new file mode 100644 index 0000000..7b9ad11 --- /dev/null +++ b/DiscUtils/Streams/Buffer/IBuffer.cs @@ -0,0 +1,112 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; + +namespace DiscUtils.Streams +{ + /// + /// Interface shared by all buffers. + /// + /// + /// Buffers are very similar to streams, except the buffer has no notion of + /// 'current position'. All I/O operations instead specify the position, as + /// needed. Buffers also support sparse behaviour. + /// + public interface IBuffer + { + /// + /// Gets a value indicating whether this buffer can be read. + /// + bool CanRead { get; } + + /// + /// Gets a value indicating whether this buffer can be modified. + /// + bool CanWrite { get; } + + /// + /// Gets the current capacity of the buffer, in bytes. + /// + long Capacity { get; } + + /// + /// Gets the parts of the buffer that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + IEnumerable Extents { get; } + + /// + /// Reads from the buffer into a byte array. + /// + /// The offset within the buffer to start reading. + /// The destination byte array. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The actual number of bytes read. + int Read(long pos, byte[] buffer, int offset, int count); + + /// + /// Writes a byte array into the buffer. + /// + /// The start offset within the buffer. + /// The source byte array. + /// The start offset within the source byte array. + /// The number of bytes to write. + void Write(long pos, byte[] buffer, int offset, int count); + + /// + /// Clears bytes from the buffer. + /// + /// The start offset within the buffer. + /// The number of bytes to clear. + /// + /// Logically equivalent to writing count null/zero bytes to the buffer, some + /// implementations determine that some (or all) of the range indicated is not actually + /// stored. There is no direct, automatic, correspondence to clearing bytes and them + /// not being represented as an 'extent' - for example, the implementation of the underlying + /// stream may not permit fine-grained extent storage. + /// It is always safe to call this method to 'zero-out' a section of a buffer, regardless of + /// the underlying buffer implementation. + /// + void Clear(long pos, int count); + + /// + /// Flushes all data to the underlying storage. + /// + void Flush(); + + /// + /// Sets the capacity of the buffer, truncating if appropriate. + /// + /// The desired capacity of the buffer. + void SetCapacity(long value); + + /// + /// Gets the parts of a buffer that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + IEnumerable GetExtentsInRange(long start, long count); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Buffer/IMappedBuffer.cs b/DiscUtils/Streams/Buffer/IMappedBuffer.cs new file mode 100644 index 0000000..9ef95d9 --- /dev/null +++ b/DiscUtils/Streams/Buffer/IMappedBuffer.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Streams +{ + public interface IMappedBuffer : IBuffer + { + long MapPosition(long position); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Buffer/SubBuffer.cs b/DiscUtils/Streams/Buffer/SubBuffer.cs new file mode 100644 index 0000000..a1631de --- /dev/null +++ b/DiscUtils/Streams/Buffer/SubBuffer.cs @@ -0,0 +1,173 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Class representing a portion of an existing buffer. + /// + public class SubBuffer : Buffer + { + private readonly long _first; + private readonly long _length; + + private readonly IBuffer _parent; + + /// + /// Initializes a new instance of the SubBuffer class. + /// + /// The parent buffer. + /// The first byte in represented by this sub-buffer. + /// The number of bytes of represented by this sub-buffer. + public SubBuffer(IBuffer parent, long first, long length) + { + _parent = parent; + _first = first; + _length = length; + + if (_first + _length > _parent.Capacity) + { + throw new ArgumentException("Substream extends beyond end of parent stream"); + } + } + + /// + /// Can this buffer be read. + /// + public override bool CanRead + { + get { return _parent.CanRead; } + } + + /// + /// Can this buffer be modified. + /// + public override bool CanWrite + { + get { return _parent.CanWrite; } + } + + /// + /// Gets the current capacity of the buffer, in bytes. + /// + public override long Capacity + { + get { return _length; } + } + + /// + /// Gets the parts of the buffer that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public override IEnumerable Extents + { + get { return OffsetExtents(_parent.GetExtentsInRange(_first, _length)); } + } + + /// + /// Flushes all data to the underlying storage. + /// + public override void Flush() + { + _parent.Flush(); + } + + /// + /// Reads from the buffer into a byte array. + /// + /// The offset within the buffer to start reading. + /// The destination byte array. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The actual number of bytes read. + public override int Read(long pos, byte[] buffer, int offset, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Attempt to read negative bytes"); + } + + if (pos >= _length) + { + return 0; + } + + return _parent.Read(pos + _first, buffer, offset, + (int)Math.Min(count, Math.Min(_length - pos, int.MaxValue))); + } + + /// + /// Writes a byte array into the buffer. + /// + /// The start offset within the buffer. + /// The source byte array. + /// The start offset within the source byte array. + /// The number of bytes to write. + public override void Write(long pos, byte[] buffer, int offset, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Attempt to write negative bytes"); + } + + if (pos + count > _length) + { + throw new ArgumentOutOfRangeException(nameof(count), "Attempt to write beyond end of substream"); + } + + _parent.Write(pos + _first, buffer, offset, count); + } + + /// + /// Sets the capacity of the buffer, truncating if appropriate. + /// + /// The desired capacity of the buffer. + public override void SetCapacity(long value) + { + throw new NotSupportedException("Attempt to change length of a subbuffer"); + } + + /// + /// Gets the parts of a buffer that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + long absStart = _first + start; + long absEnd = Math.Min(absStart + count, _first + _length); + return OffsetExtents(_parent.GetExtentsInRange(absStart, absEnd - absStart)); + } + + private IEnumerable OffsetExtents(IEnumerable src) + { + foreach (StreamExtent e in src) + { + yield return new StreamExtent(e.Start - _first, e.Length); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/BuilderBufferExtent.cs b/DiscUtils/Streams/Builder/BuilderBufferExtent.cs new file mode 100644 index 0000000..8f5dac2 --- /dev/null +++ b/DiscUtils/Streams/Builder/BuilderBufferExtent.cs @@ -0,0 +1,73 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + public class BuilderBufferExtent : BuilderExtent + { + private byte[] _buffer; + private readonly bool _fixedBuffer; + + public BuilderBufferExtent(long start, long length) + : base(start, length) {} + + public BuilderBufferExtent(long start, byte[] buffer) + : base(start, buffer.Length) + { + _fixedBuffer = true; + _buffer = buffer; + } + + public override void Dispose() {} + + public override void PrepareForRead() + { + if (!_fixedBuffer) + { + _buffer = GetBuffer(); + } + } + + public override int Read(long diskOffset, byte[] block, int offset, int count) + { + int startOffset = (int)(diskOffset - Start); + int numBytes = (int)Math.Min(Length - startOffset, count); + Array.Copy(_buffer, startOffset, block, offset, numBytes); + return numBytes; + } + + public override void DisposeReadState() + { + if (!_fixedBuffer) + { + _buffer = null; + } + } + + protected virtual byte[] GetBuffer() + { + throw new NotSupportedException("Derived class should implement"); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/BuilderBufferExtentSource.cs b/DiscUtils/Streams/Builder/BuilderBufferExtentSource.cs new file mode 100644 index 0000000..de6a7a1 --- /dev/null +++ b/DiscUtils/Streams/Builder/BuilderBufferExtentSource.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Streams +{ + public class BuilderBufferExtentSource : BuilderExtentSource + { + private readonly byte[] _buffer; + + public BuilderBufferExtentSource(byte[] buffer) + { + _buffer = buffer; + } + + public override BuilderExtent Fix(long pos) + { + return new BuilderBufferExtent(pos, _buffer); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/BuilderBytesExtent.cs b/DiscUtils/Streams/Builder/BuilderBytesExtent.cs new file mode 100644 index 0000000..8b36659 --- /dev/null +++ b/DiscUtils/Streams/Builder/BuilderBytesExtent.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + public class BuilderBytesExtent : BuilderExtent + { + protected byte[] _data; + + public BuilderBytesExtent(long start, byte[] data) + : base(start, data.Length) + { + _data = data; + } + + protected BuilderBytesExtent(long start, long length) + : base(start, length) {} + + public override void Dispose() {} + + public override void PrepareForRead() {} + + public override int Read(long diskOffset, byte[] block, int offset, int count) + { + int start = (int)Math.Min(diskOffset - Start, _data.Length); + int numRead = Math.Min(count, _data.Length - start); + + Array.Copy(_data, start, block, offset, numRead); + + return numRead; + } + + public override void DisposeReadState() {} + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/BuilderExtent.cs b/DiscUtils/Streams/Builder/BuilderExtent.cs new file mode 100644 index 0000000..d93a62d --- /dev/null +++ b/DiscUtils/Streams/Builder/BuilderExtent.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + public abstract class BuilderExtent : IDisposable + { + public BuilderExtent(long start, long length) + { + Start = start; + Length = length; + } + + public long Length { get; } + + public long Start { get; } + + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public virtual IEnumerable StreamExtents + { + get { return new[] { new StreamExtent(Start, Length) }; } + } + + public abstract void Dispose(); + + public abstract void PrepareForRead(); + + public abstract int Read(long diskOffset, byte[] block, int offset, int count); + + public abstract void DisposeReadState(); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/BuilderExtentSource.cs b/DiscUtils/Streams/Builder/BuilderExtentSource.cs new file mode 100644 index 0000000..58e11bc --- /dev/null +++ b/DiscUtils/Streams/Builder/BuilderExtentSource.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Streams +{ + public abstract class BuilderExtentSource + { + public abstract BuilderExtent Fix(long pos); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/BuilderSparseStreamExtent.cs b/DiscUtils/Streams/Builder/BuilderSparseStreamExtent.cs new file mode 100644 index 0000000..ff5dc52 --- /dev/null +++ b/DiscUtils/Streams/Builder/BuilderSparseStreamExtent.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; + +namespace DiscUtils.Streams +{ + public class BuilderSparseStreamExtent : BuilderExtent + { + private readonly Ownership _ownership; + private SparseStream _stream; + + public BuilderSparseStreamExtent(long start, SparseStream stream) + : this(start, stream, Ownership.None) {} + + public BuilderSparseStreamExtent(long start, SparseStream stream, Ownership ownership) + : base(start, stream.Length) + { + _stream = stream; + _ownership = ownership; + } + + public override IEnumerable StreamExtents + { + get { return StreamExtent.Offset(_stream.Extents, Start); } + } + + public override void Dispose() + { + if (_stream != null && _ownership == Ownership.Dispose) + { + _stream.Dispose(); + _stream = null; + } + } + + public override void PrepareForRead() {} + + public override int Read(long diskOffset, byte[] block, int offset, int count) + { + _stream.Position = diskOffset - Start; + return _stream.Read(block, offset, count); + } + + public override void DisposeReadState() {} + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/BuilderStreamExtent.cs b/DiscUtils/Streams/Builder/BuilderStreamExtent.cs new file mode 100644 index 0000000..0f53c1f --- /dev/null +++ b/DiscUtils/Streams/Builder/BuilderStreamExtent.cs @@ -0,0 +1,61 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + public class BuilderStreamExtent : BuilderExtent + { + private readonly Ownership _ownership; + private Stream _source; + + public BuilderStreamExtent(long start, Stream source) + : this(start, source, Ownership.None) {} + + public BuilderStreamExtent(long start, Stream source, Ownership ownership) + : base(start, source.Length) + { + _source = source; + _ownership = ownership; + } + + public override void Dispose() + { + if (_source != null && _ownership == Ownership.Dispose) + { + _source.Dispose(); + _source = null; + } + } + + public override void PrepareForRead() {} + + public override int Read(long diskOffset, byte[] block, int offset, int count) + { + _source.Position = diskOffset - Start; + return _source.Read(block, offset, count); + } + + public override void DisposeReadState() {} + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/BuilderStreamExtentSource.cs b/DiscUtils/Streams/Builder/BuilderStreamExtentSource.cs new file mode 100644 index 0000000..27c481d --- /dev/null +++ b/DiscUtils/Streams/Builder/BuilderStreamExtentSource.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + public class BuilderStreamExtentSource : BuilderExtentSource + { + private readonly Stream _stream; + + public BuilderStreamExtentSource(Stream stream) + { + _stream = stream; + } + + public override BuilderExtent Fix(long pos) + { + return new BuilderStreamExtent(pos, _stream); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/PassthroughStreamBuilder.cs b/DiscUtils/Streams/Builder/PassthroughStreamBuilder.cs new file mode 100644 index 0000000..241b774 --- /dev/null +++ b/DiscUtils/Streams/Builder/PassthroughStreamBuilder.cs @@ -0,0 +1,46 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; + +namespace DiscUtils.Streams +{ + public class PassthroughStreamBuilder : StreamBuilder + { + private readonly Stream _stream; + + public PassthroughStreamBuilder(Stream stream) + { + _stream = stream; + } + + protected override List FixExtents(out long totalLength) + { + _stream.Position = 0; + List result = new List(); + result.Add(new BuilderStreamExtent(0, _stream)); + totalLength = _stream.Length; + return result; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Builder/StreamBuilder.cs b/DiscUtils/Streams/Builder/StreamBuilder.cs new file mode 100644 index 0000000..38c1469 --- /dev/null +++ b/DiscUtils/Streams/Builder/StreamBuilder.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; + +namespace DiscUtils.Streams +{ + /// + /// Base class for objects that can dynamically construct a stream. + /// + public abstract class StreamBuilder + { + /// + /// Builds a new stream. + /// + /// The stream created by the StreamBuilder instance. + public virtual SparseStream Build() + { + long totalLength; + List extents = FixExtents(out totalLength); + return new BuiltStream(totalLength, extents); + } + + /// + /// Writes the stream contents to an existing stream. + /// + /// The stream to write to. + public void Build(Stream output) + { + using (Stream src = Build()) + { + byte[] buffer = new byte[64 * 1024]; + int numRead = src.Read(buffer, 0, buffer.Length); + while (numRead != 0) + { + output.Write(buffer, 0, numRead); + numRead = src.Read(buffer, 0, buffer.Length); + } + } + } + + /// + /// Writes the stream contents to a file. + /// + /// The file to write to. + public void Build(string outputFile) + { + using (FileStream destStream = new FileStream(outputFile, FileMode.Create, FileAccess.Write)) + { + Build(destStream); + } + } + + protected abstract List FixExtents(out long totalLength); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/BuiltStream.cs b/DiscUtils/Streams/BuiltStream.cs new file mode 100644 index 0000000..19d01a0 --- /dev/null +++ b/DiscUtils/Streams/BuiltStream.cs @@ -0,0 +1,325 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + public class BuiltStream : SparseStream + { + private Stream _baseStream; + + private BuilderExtent _currentExtent; + private readonly List _extents; + private readonly long _length; + private long _position; + + public BuiltStream(long length, List extents) + { + _baseStream = new ZeroStream(length); + _length = length; + _extents = extents; + + // Make sure the extents are sorted, so binary searches will work. + _extents.Sort(new ExtentStartComparer()); + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override IEnumerable Extents + { + get + { + foreach (BuilderExtent extent in _extents) + { + foreach (StreamExtent streamExtent in extent.StreamExtents) + { + yield return streamExtent; + } + } + } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + set { _position = value; } + } + + public override void Flush() {} + + public override int Read(byte[] buffer, int offset, int count) + { + if (_position >= _length) + { + return 0; + } + if (_position + count > _length) + { + count = (int)(_length - _position); + } + + int totalRead = 0; + while (totalRead < count && _position < _length) + { + // If current region is outside the area of interest, clean it up + if (_currentExtent != null && + (_position < _currentExtent.Start || _position >= _currentExtent.Start + _currentExtent.Length)) + { + _currentExtent.DisposeReadState(); + _currentExtent = null; + } + + // If we need to find a new region, look for it + if (_currentExtent == null) + { + using (SearchExtent searchExtent = new SearchExtent(_position)) + { + int idx = _extents.BinarySearch(searchExtent, new ExtentRangeComparer()); + if (idx >= 0) + { + BuilderExtent extent = _extents[idx]; + extent.PrepareForRead(); + _currentExtent = extent; + } + } + } + + int numRead = 0; + + // If the block is outside any known extent, defer to base stream. + if (_currentExtent == null) + { + _baseStream.Position = _position; + BuilderExtent nextExtent = FindNext(_position); + if (nextExtent != null) + { + numRead = _baseStream.Read(buffer, offset + totalRead, + (int)Math.Min(count - totalRead, nextExtent.Start - _position)); + } + else + { + numRead = _baseStream.Read(buffer, offset + totalRead, count - totalRead); + } + } + else + { + numRead = _currentExtent.Read(_position, buffer, offset + totalRead, count - totalRead); + } + + _position += numRead; + totalRead += numRead; + if (numRead == 0) + break; + } + + return totalRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = offset; + if (origin == SeekOrigin.Current) + { + newPos += _position; + } + else if (origin == SeekOrigin.End) + { + newPos += _length; + } + + _position = newPos; + return newPos; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_currentExtent != null) + { + _currentExtent.DisposeReadState(); + _currentExtent = null; + } + + if (_baseStream != null) + { + _baseStream.Dispose(); + _baseStream = null; + } + } + } + finally + { + base.Dispose(disposing); + } + } + + private BuilderExtent FindNext(long pos) + { + int min = 0; + int max = _extents.Count - 1; + + if (_extents.Count == 0 || _extents[_extents.Count - 1].Start + _extents[_extents.Count - 1].Length <= pos) + { + return null; + } + + while (true) + { + if (min >= max) + { + return _extents[min]; + } + + int mid = (max + min) / 2; + if (_extents[mid].Start < pos) + { + min = mid + 1; + } + else if (_extents[mid].Start > pos) + { + max = mid; + } + else + { + return _extents[mid]; + } + } + } + + private class SearchExtent : BuilderExtent + { + public SearchExtent(long pos) + : base(pos, 1) {} + + public override void Dispose() {} + + public override void PrepareForRead() + { + // Not valid to use this 'dummy' extent for actual construction + throw new NotSupportedException(); + } + + public override int Read(long diskOffset, byte[] block, int offset, int count) + { + // Not valid to use this 'dummy' extent for actual construction + throw new NotSupportedException(); + } + + public override void DisposeReadState() + { + // Not valid to use this 'dummy' extent for actual construction + throw new NotSupportedException(); + } + } + + private class ExtentRangeComparer : IComparer + { + public int Compare(BuilderExtent x, BuilderExtent y) + { + if (x == null) + { + throw new ArgumentNullException(nameof(x)); + } + + if (y == null) + { + throw new ArgumentNullException(nameof(y)); + } + + if (x.Start + x.Length <= y.Start) + { + // x < y, with no intersection + return -1; + } + if (x.Start >= y.Start + y.Length) + { + // x > y, with no intersection + return 1; + } + + // x intersects y + return 0; + } + } + + private class ExtentStartComparer : IComparer + { + public int Compare(BuilderExtent x, BuilderExtent y) + { + if (x == null) + { + throw new ArgumentNullException(nameof(x)); + } + + if (y == null) + { + throw new ArgumentNullException(nameof(y)); + } + + long val = x.Start - y.Start; + if (val < 0) + { + return -1; + } + if (val > 0) + { + return 1; + } + return 0; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/CircularStream.cs b/DiscUtils/Streams/CircularStream.cs new file mode 100644 index 0000000..c222e9a --- /dev/null +++ b/DiscUtils/Streams/CircularStream.cs @@ -0,0 +1,74 @@ +// +// Copyright (c) 2008-2013, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Represents a stream that is circular, so reads and writes off the end of the stream wrap. + /// + public sealed class CircularStream : WrappingStream + { + public CircularStream(SparseStream toWrap, Ownership ownership) + : base(toWrap, ownership) {} + + public override int Read(byte[] buffer, int offset, int count) + { + WrapPosition(); + + int read = base.Read(buffer, offset, (int)Math.Min(Length - Position, count)); + + WrapPosition(); + + return read; + } + + public override void Write(byte[] buffer, int offset, int count) + { + WrapPosition(); + + int totalWritten = 0; + while (totalWritten < count) + { + int toWrite = (int)Math.Min(count - totalWritten, Length - Position); + + base.Write(buffer, offset + totalWritten, toWrite); + + WrapPosition(); + + totalWritten += toWrite; + } + } + + private void WrapPosition() + { + long pos = Position; + long length = Length; + + if (pos >= length) + { + Position = pos % length; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/ConcatStream.cs b/DiscUtils/Streams/ConcatStream.cs new file mode 100644 index 0000000..38341bb --- /dev/null +++ b/DiscUtils/Streams/ConcatStream.cs @@ -0,0 +1,286 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; + +namespace DiscUtils.Streams +{ + /// + /// The concatenation of multiple streams (read-only, for now). + /// + public class ConcatStream : SparseStream + { + private readonly bool _canWrite; + private readonly Ownership _ownsStreams; + + private long _position; + private SparseStream[] _streams; + + public ConcatStream(Ownership ownsStreams, params SparseStream[] streams) + { + _ownsStreams = ownsStreams; + _streams = streams; + + // Only allow writes if all streams can be written + _canWrite = true; + foreach (SparseStream stream in streams) + { + if (!stream.CanWrite) + { + _canWrite = false; + } + } + } + + public override bool CanRead + { + get + { + CheckDisposed(); + return true; + } + } + + public override bool CanSeek + { + get + { + CheckDisposed(); + return true; + } + } + + public override bool CanWrite + { + get + { + CheckDisposed(); + return _canWrite; + } + } + + public override IEnumerable Extents + { + get + { + CheckDisposed(); + List extents = new List(); + + long pos = 0; + for (int i = 0; i < _streams.Length; ++i) + { + foreach (StreamExtent extent in _streams[i].Extents) + { + extents.Add(new StreamExtent(extent.Start + pos, extent.Length)); + } + + pos += _streams[i].Length; + } + + return extents; + } + } + + public override long Length + { + get + { + CheckDisposed(); + long length = 0; + for (int i = 0; i < _streams.Length; ++i) + { + length += _streams[i].Length; + } + + return length; + } + } + + public override long Position + { + get + { + CheckDisposed(); + return _position; + } + + set + { + CheckDisposed(); + _position = value; + } + } + + public override void Flush() + { + CheckDisposed(); + for (int i = 0; i < _streams.Length; ++i) + { + _streams[i].Flush(); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + CheckDisposed(); + + int totalRead = 0; + int numRead = 0; + + do + { + long activeStreamStartPos; + int activeStream = GetActiveStream(out activeStreamStartPos); + + _streams[activeStream].Position = _position - activeStreamStartPos; + + numRead = _streams[activeStream].Read(buffer, offset + totalRead, count - totalRead); + + totalRead += numRead; + _position += numRead; + } while (numRead != 0); + + return totalRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + CheckDisposed(); + + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += Length; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of disk"); + } + Position = effectiveOffset; + return Position; + } + + public override void SetLength(long value) + { + CheckDisposed(); + + long lastStreamOffset; + int lastStream = GetStream(Length, out lastStreamOffset); + if (value < lastStreamOffset) + { + throw new IOException(string.Format(CultureInfo.InvariantCulture, + "Unable to reduce stream length to less than {0}", lastStreamOffset)); + } + + _streams[lastStream].SetLength(value - lastStreamOffset); + } + + public override void Write(byte[] buffer, int offset, int count) + { + CheckDisposed(); + + int totalWritten = 0; + while (totalWritten != count) + { + // Offset of the stream = streamOffset + long streamOffset; + int streamIdx = GetActiveStream(out streamOffset); + + // Offset within the stream = streamPos + long streamPos = _position - streamOffset; + _streams[streamIdx].Position = streamPos; + + // Write (limited to the stream's length), except for final stream - that may be + // extendable + int numToWrite; + if (streamIdx == _streams.Length - 1) + { + numToWrite = count - totalWritten; + } + else + { + numToWrite = (int)Math.Min(count - totalWritten, _streams[streamIdx].Length - streamPos); + } + + _streams[streamIdx].Write(buffer, offset + totalWritten, numToWrite); + + totalWritten += numToWrite; + _position += numToWrite; + } + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsStreams == Ownership.Dispose && _streams != null) + { + foreach (SparseStream stream in _streams) + { + stream.Dispose(); + } + + _streams = null; + } + } + finally + { + base.Dispose(disposing); + } + } + + private int GetActiveStream(out long startPos) + { + return GetStream(_position, out startPos); + } + + private int GetStream(long targetPos, out long streamStartPos) + { + // Find the stream that _position is within + streamStartPos = 0; + int focusStream = 0; + while (focusStream < _streams.Length - 1 && streamStartPos + _streams[focusStream].Length <= targetPos) + { + streamStartPos += _streams[focusStream].Length; + focusStream++; + } + + return focusStream; + } + + private void CheckDisposed() + { + if (_streams == null) + { + throw new ObjectDisposedException("ConcatStream"); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/DiscUtils.Streams.csproj b/DiscUtils/Streams/DiscUtils.Streams.csproj new file mode 100644 index 0000000..0083bf2 --- /dev/null +++ b/DiscUtils/Streams/DiscUtils.Streams.csproj @@ -0,0 +1,10 @@ + + + + + DiscUtils Streams + Kenneth Bell;LordMike;Bianco Veigel + DiscUtils;Streams + + + diff --git a/DiscUtils/Streams/IByteArraySerializable.cs b/DiscUtils/Streams/IByteArraySerializable.cs new file mode 100644 index 0000000..91f7976 --- /dev/null +++ b/DiscUtils/Streams/IByteArraySerializable.cs @@ -0,0 +1,50 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Streams +{ + /// + /// Common interface for reading structures to/from byte arrays. + /// + public interface IByteArraySerializable + { + /// + /// Gets the total number of bytes the structure occupies. + /// + int Size { get; } + + /// + /// Reads the structure from a byte array. + /// + /// The buffer to read from. + /// The buffer offset to start reading from. + /// The number of bytes read. + int ReadFrom(byte[] buffer, int offset); + + /// + /// Writes a structure to a byte array. + /// + /// The buffer to write to. + /// The buffer offset to start writing at. + void WriteTo(byte[] buffer, int offset); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/LengthWrappingStream.cs b/DiscUtils/Streams/LengthWrappingStream.cs new file mode 100644 index 0000000..c9cccf8 --- /dev/null +++ b/DiscUtils/Streams/LengthWrappingStream.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2017, Bianco Veigel +// +// 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. +// + +namespace DiscUtils.Streams +{ + /// + /// Represents a stream with a specified length + /// + /// + /// since the wrapped stream may not support + /// there is no validation of the specified length + /// + public class LengthWrappingStream : WrappingStream + { + private readonly long _length; + + public LengthWrappingStream(SparseStream toWrap, long length, Ownership ownership) + : base(toWrap, ownership) + { + _length = length; + } + + public override long Length + { + get { return _length; } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/MappedStream.cs b/DiscUtils/Streams/MappedStream.cs new file mode 100644 index 0000000..d4d11ac --- /dev/null +++ b/DiscUtils/Streams/MappedStream.cs @@ -0,0 +1,83 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; + +namespace DiscUtils.Streams +{ + /// + /// Base class for streams that are essentially a mapping onto a parent stream. + /// + /// + /// This class provides access to the mapping underlying the stream, enabling + /// callers to convert a byte range in this stream into one or more ranges in + /// the parent stream. + /// + public abstract class MappedStream : SparseStream + { + /// + /// Converts any stream into a non-linear stream. + /// + /// The stream to convert. + /// true to have the new stream dispose the wrapped + /// stream when it is disposed. + /// A sparse stream. + /// The wrapped stream is assumed to be a linear stream (such that any byte range + /// maps directly onto the parent stream). + public new static MappedStream FromStream(Stream stream, Ownership takeOwnership) + { + return new WrappingMappedStream(stream, takeOwnership, null); + } + + /// + /// Converts any stream into a non-linear stream. + /// + /// The stream to convert. + /// true to have the new stream dispose the wrapped + /// stream when it is disposed. + /// The set of extents actually stored in stream. + /// A sparse stream. + /// The wrapped stream is assumed to be a linear stream (such that any byte range + /// maps directly onto the parent stream). + public new static MappedStream FromStream(Stream stream, Ownership takeOwnership, + IEnumerable extents) + { + return new WrappingMappedStream(stream, takeOwnership, extents); + } + + /// + /// Maps a logical range down to storage locations. + /// + /// The first logical range to map. + /// The length of the range to map. + /// One or more stream extents specifying the storage locations that correspond + /// to the identified logical extent range. + /// + /// As far as possible, the stream extents are returned in logical disk order - + /// however, due to the nature of non-linear streams, not all of the range may actually + /// be stored, or some or all of the range may be compressed - thus reading the + /// returned stream extents is not equivalent to reading the logical disk range. + /// + public abstract IEnumerable MapContent(long start, long length); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/MirrorStream.cs b/DiscUtils/Streams/MirrorStream.cs new file mode 100644 index 0000000..bf98fb4 --- /dev/null +++ b/DiscUtils/Streams/MirrorStream.cs @@ -0,0 +1,170 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + public class MirrorStream : SparseStream + { + private readonly bool _canRead; + private readonly bool _canSeek; + private readonly bool _canWrite; + private readonly long _length; + private readonly Ownership _ownsWrapped; + private List _wrapped; + + public MirrorStream(Ownership ownsWrapped, params SparseStream[] wrapped) + { + _wrapped = new List(wrapped); + _ownsWrapped = ownsWrapped; + + _canRead = _wrapped[0].CanRead; + _canWrite = _wrapped[0].CanWrite; + _canSeek = _wrapped[0].CanSeek; + _length = _wrapped[0].Length; + + foreach (SparseStream stream in _wrapped) + { + if (stream.CanRead != _canRead || stream.CanWrite != _canWrite || stream.CanSeek != _canSeek) + { + throw new ArgumentException("All mirrored streams must have the same read/write/seek permissions", + nameof(wrapped)); + } + + if (stream.Length != _length) + { + throw new ArgumentException("All mirrored streams must have the same length", nameof(wrapped)); + } + } + } + + public override bool CanRead + { + get { return _canRead; } + } + + public override bool CanSeek + { + get { return _canSeek; } + } + + public override bool CanWrite + { + get { return _canWrite; } + } + + public override IEnumerable Extents + { + get { return _wrapped[0].Extents; } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _wrapped[0].Position; } + + set { _wrapped[0].Position = value; } + } + + public override void Flush() + { + _wrapped[0].Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrapped[0].Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _wrapped[0].Seek(offset, origin); + } + + public override void SetLength(long value) + { + if (value != _length) + { + throw new InvalidOperationException("Changing the stream length is not permitted for mirrored streams"); + } + } + + public override void Clear(int count) + { + long pos = _wrapped[0].Position; + + if (pos + count > _length) + { + throw new IOException("Attempt to clear beyond end of mirrored stream"); + } + + foreach (SparseStream stream in _wrapped) + { + stream.Position = pos; + stream.Clear(count); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + long pos = _wrapped[0].Position; + + if (pos + count > _length) + { + throw new IOException("Attempt to write beyond end of mirrored stream"); + } + + foreach (SparseStream stream in _wrapped) + { + stream.Position = pos; + stream.Write(buffer, offset, count); + } + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsWrapped == Ownership.Dispose && _wrapped != null) + { + foreach (SparseStream stream in _wrapped) + { + stream.Dispose(); + } + + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/PositionWrappingStream.cs b/DiscUtils/Streams/PositionWrappingStream.cs new file mode 100644 index 0000000..8bf40a1 --- /dev/null +++ b/DiscUtils/Streams/PositionWrappingStream.cs @@ -0,0 +1,102 @@ +// +// Copyright (c) 2017, Bianco Veigel +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Stream wrapper to allow forward only seeking on not seekable streams + /// + public class PositionWrappingStream : WrappingStream + { + public PositionWrappingStream(SparseStream toWrap, long currentPosition, Ownership ownership) + : base(toWrap, ownership) + { + _position = currentPosition; + } + + private long _position; + public override long Position + { + get { return _position; } + set + { + if (_position == value) + return; + Seek(value, SeekOrigin.Begin); + } + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (base.CanSeek) + { + return base.Seek(offset, SeekOrigin.Current); + } + switch (origin) + { + case SeekOrigin.Begin: + offset = offset - _position; + break; + case SeekOrigin.Current: + offset = offset + _position; + break; + case SeekOrigin.End: + offset = Length - offset; + break; + default: + throw new ArgumentOutOfRangeException(nameof(origin), origin, null); + } + if (offset == 0) + return _position; + if (offset < 0) + throw new NotSupportedException("backward seeking is not supported"); + var buffer = new byte[Sizes.OneKiB]; + while (offset > 0) + { + var read = base.Read(buffer, 0, (int)Math.Min(buffer.Length, offset)); + offset -= read; + } + return _position; + } + + public override bool CanSeek + { + get { return true; } + } + + public override int Read(byte[] buffer, int offset, int count) + { + var read = base.Read(buffer, offset, count); + _position += read; + return read; + } + + public override void Write(byte[] buffer, int offset, int count) + { + base.Write(buffer, offset, count); + _position += count; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/PumpProgressEventArgs.cs b/DiscUtils/Streams/PumpProgressEventArgs.cs new file mode 100644 index 0000000..31eaa75 --- /dev/null +++ b/DiscUtils/Streams/PumpProgressEventArgs.cs @@ -0,0 +1,52 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Event arguments indicating progress on pumping a stream. + /// + public class PumpProgressEventArgs : EventArgs + { + /// + /// Gets or sets the number of bytes read from InputStream. + /// + public long BytesRead { get; set; } + + /// + /// Gets or sets the number of bytes written to OutputStream. + /// + public long BytesWritten { get; set; } + + /// + /// Gets or sets the absolute position in OutputStream. + /// + public long DestinationPosition { get; set; } + + /// + /// Gets or sets the absolute position in InputStream. + /// + public long SourcePosition { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/ReaderWriter/BigEndianDataReader.cs b/DiscUtils/Streams/ReaderWriter/BigEndianDataReader.cs new file mode 100644 index 0000000..177152e --- /dev/null +++ b/DiscUtils/Streams/ReaderWriter/BigEndianDataReader.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + public class BigEndianDataReader : DataReader + { + public BigEndianDataReader(Stream stream) + : base(stream) {} + + public override ushort ReadUInt16() + { + ReadToBuffer(sizeof(UInt16)); + return EndianUtilities.ToUInt16BigEndian(_buffer, 0); + } + + public override int ReadInt32() + { + ReadToBuffer(sizeof(Int32)); + return EndianUtilities.ToInt32BigEndian(_buffer, 0); + } + + public override uint ReadUInt32() + { + ReadToBuffer(sizeof(UInt32)); + return EndianUtilities.ToUInt32BigEndian(_buffer, 0); + } + + public override long ReadInt64() + { + ReadToBuffer(sizeof(Int64)); + return EndianUtilities.ToInt64BigEndian(_buffer, 0); + } + + public override ulong ReadUInt64() + { + ReadToBuffer(sizeof(UInt64)); + return EndianUtilities.ToUInt64BigEndian(_buffer, 0); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/ReaderWriter/BigEndianDataWriter.cs b/DiscUtils/Streams/ReaderWriter/BigEndianDataWriter.cs new file mode 100644 index 0000000..ceae6bc --- /dev/null +++ b/DiscUtils/Streams/ReaderWriter/BigEndianDataWriter.cs @@ -0,0 +1,68 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + public class BigEndianDataWriter : DataWriter + { + public BigEndianDataWriter(Stream stream) + : base(stream) {} + + public override void Write(ushort value) + { + EnsureBuffer(); + EndianUtilities.WriteBytesBigEndian(value, _buffer, 0); + FlushBuffer(sizeof(UInt16)); + } + + public override void Write(int value) + { + EnsureBuffer(); + EndianUtilities.WriteBytesBigEndian(value, _buffer, 0); + FlushBuffer(sizeof(Int32)); + } + + public override void Write(uint value) + { + EnsureBuffer(); + EndianUtilities.WriteBytesBigEndian(value, _buffer, 0); + FlushBuffer(sizeof(UInt32)); + } + + public override void Write(long value) + { + EnsureBuffer(); + EndianUtilities.WriteBytesBigEndian(value, _buffer, 0); + FlushBuffer(sizeof(Int64)); + } + + public override void Write(ulong value) + { + EnsureBuffer(); + EndianUtilities.WriteBytesBigEndian(value, _buffer, 0); + FlushBuffer(sizeof(UInt64)); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/ReaderWriter/DataReader.cs b/DiscUtils/Streams/ReaderWriter/DataReader.cs new file mode 100644 index 0000000..0e2a96c --- /dev/null +++ b/DiscUtils/Streams/ReaderWriter/DataReader.cs @@ -0,0 +1,84 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Base class for reading binary data from a stream. + /// + public abstract class DataReader + { + private const int _bufferSize = sizeof(UInt64); + + protected readonly Stream _stream; + + protected byte[] _buffer; + + public DataReader(Stream stream) + { + _stream = stream; + } + + public long Length + { + get { return _stream.Length; } + } + + public long Position + { + get { return _stream.Position; } + } + + public void Skip(int bytes) + { + ReadBytes(bytes); + } + + public abstract ushort ReadUInt16(); + + public abstract int ReadInt32(); + + public abstract uint ReadUInt32(); + + public abstract long ReadInt64(); + + public abstract ulong ReadUInt64(); + + public virtual byte[] ReadBytes(int count) + { + return StreamUtilities.ReadExact(_stream, count); + } + + protected void ReadToBuffer(int count) + { + if (_buffer == null) + { + _buffer = new byte[_bufferSize]; + } + + StreamUtilities.ReadExact(_stream, _buffer, 0, count); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/ReaderWriter/DataWriter.cs b/DiscUtils/Streams/ReaderWriter/DataWriter.cs new file mode 100644 index 0000000..f2622ac --- /dev/null +++ b/DiscUtils/Streams/ReaderWriter/DataWriter.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + public abstract class DataWriter + { + private const int _bufferSize = sizeof(UInt64); + + protected readonly Stream _stream; + + protected byte[] _buffer; + + public DataWriter(Stream stream) + { + _stream = stream; + } + + public abstract void Write(ushort value); + + public abstract void Write(int value); + + public abstract void Write(uint value); + + public abstract void Write(long value); + + public abstract void Write(ulong value); + + public virtual void WriteBytes(byte[] value, int offset, int count) + { + _stream.Write(value, offset, count); + } + + public virtual void WriteBytes(byte[] value) + { + _stream.Write(value, 0, value.Length); + } + + public virtual void Flush() + { + _stream.Flush(); + } + + protected void EnsureBuffer() + { + if (_buffer == null) + { + _buffer = new byte[_bufferSize]; + } + } + + protected void FlushBuffer(int count) + { + _stream.Write(_buffer, 0, count); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/ReaderWriter/LittleEndianDataReader.cs b/DiscUtils/Streams/ReaderWriter/LittleEndianDataReader.cs new file mode 100644 index 0000000..ad7ee98 --- /dev/null +++ b/DiscUtils/Streams/ReaderWriter/LittleEndianDataReader.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Class for reading little-endian data from a stream. + /// + public class LittleEndianDataReader : DataReader + { + public LittleEndianDataReader(Stream stream) + : base(stream) {} + + public override ushort ReadUInt16() + { + ReadToBuffer(sizeof(UInt16)); + return EndianUtilities.ToUInt16LittleEndian(_buffer, 0); + } + + public override int ReadInt32() + { + ReadToBuffer(sizeof(Int32)); + return EndianUtilities.ToInt32LittleEndian(_buffer, 0); + } + + public override uint ReadUInt32() + { + ReadToBuffer(sizeof(UInt32)); + return EndianUtilities.ToUInt32LittleEndian(_buffer, 0); + } + + public override long ReadInt64() + { + ReadToBuffer(sizeof(Int64)); + return EndianUtilities.ToInt64LittleEndian(_buffer, 0); + } + + public override ulong ReadUInt64() + { + ReadToBuffer(sizeof(UInt64)); + return EndianUtilities.ToUInt64LittleEndian(_buffer, 0); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/SnapshotStream.cs b/DiscUtils/Streams/SnapshotStream.cs new file mode 100644 index 0000000..d27b505 --- /dev/null +++ b/DiscUtils/Streams/SnapshotStream.cs @@ -0,0 +1,410 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + /// + /// A wrapper stream that enables you to take a snapshot, pushing changes into a side buffer. + /// + /// Once a snapshot is taken, you can discard subsequent changes or merge them back + /// into the wrapped stream. + public sealed class SnapshotStream : SparseStream + { + private Stream _baseStream; + + private readonly Ownership _baseStreamOwnership; + + /// + /// Records which byte ranges in diffStream hold changes. + /// + /// Can't use _diffStream's own tracking because that's based on it's + /// internal block size, not on the _actual_ bytes stored. + private List _diffExtents; + + /// + /// Captures changes to the base stream (when enabled). + /// + private SparseMemoryStream _diffStream; + + /// + /// Indicates that no writes should be permitted. + /// + private bool _frozen; + + private long _position; + + /// + /// The saved stream position (if the diffStream is active). + /// + private long _savedPosition; + + /// + /// Initializes a new instance of the SnapshotStream class. + /// + /// The stream to wrap. + /// Indicates if this stream should control the lifetime of baseStream. + public SnapshotStream(Stream baseStream, Ownership owns) + { + _baseStream = baseStream; + _baseStreamOwnership = owns; + _diffExtents = new List(); + } + + /// + /// Gets an indication as to whether the stream can be read. + /// + public override bool CanRead + { + get { return _baseStream.CanRead; } + } + + /// + /// Gets an indication as to whether the stream position can be changed. + /// + public override bool CanSeek + { + get { return _baseStream.CanSeek; } + } + + /// + /// Gets an indication as to whether the stream can be written to. + /// + /// This property is orthogonal to Freezing/Thawing, it's + /// perfectly possible for a stream to be frozen and this method + /// return true. + public override bool CanWrite + { + get { return _diffStream != null ? true : _baseStream.CanWrite; } + } + + /// + /// Returns an enumeration over the parts of the stream that contain real data. + /// + public override IEnumerable Extents + { + get + { + SparseStream sparseBase = _baseStream as SparseStream; + if (sparseBase == null) + { + return new[] { new StreamExtent(0, Length) }; + } + return StreamExtent.Union(sparseBase.Extents, _diffExtents); + } + } + + /// + /// Gets the length of the stream. + /// + public override long Length + { + get + { + if (_diffStream != null) + { + return _diffStream.Length; + } + return _baseStream.Length; + } + } + + /// + /// Gets and sets the current stream position. + /// + public override long Position + { + get { return _position; } + + set { _position = value; } + } + + /// + /// Prevents any write operations to the stream. + /// + /// Useful to prevent changes whilst inspecting the stream. + public void Freeze() + { + _frozen = true; + } + + /// + /// Re-permits write operations to the stream. + /// + public void Thaw() + { + _frozen = false; + } + + /// + /// Takes a snapshot of the current stream contents. + /// + public void Snapshot() + { + if (_diffStream != null) + { + throw new InvalidOperationException("Already have a snapshot"); + } + + _savedPosition = _position; + + _diffExtents = new List(); + _diffStream = new SparseMemoryStream(); + _diffStream.SetLength(_baseStream.Length); + } + + /// + /// Reverts to a previous snapshot, discarding any changes made to the stream. + /// + public void RevertToSnapshot() + { + if (_diffStream == null) + { + throw new InvalidOperationException("No snapshot"); + } + + _diffStream = null; + _diffExtents = null; + + _position = _savedPosition; + } + + /// + /// Discards the snapshot any changes made after the snapshot was taken are kept. + /// + public void ForgetSnapshot() + { + if (_diffStream == null) + { + throw new InvalidOperationException("No snapshot"); + } + + byte[] buffer = new byte[8192]; + + foreach (StreamExtent extent in _diffExtents) + { + _diffStream.Position = extent.Start; + _baseStream.Position = extent.Start; + + int totalRead = 0; + while (totalRead < extent.Length) + { + int toRead = (int)Math.Min(extent.Length - totalRead, buffer.Length); + + int read = _diffStream.Read(buffer, 0, toRead); + _baseStream.Write(buffer, 0, read); + + totalRead += read; + } + } + + _diffStream = null; + _diffExtents = null; + } + + /// + /// Flushes the stream. + /// + public override void Flush() + { + CheckFrozen(); + + _baseStream.Flush(); + } + + /// + /// Reads data from the stream. + /// + /// The buffer to fill. + /// The buffer offset to start from. + /// The number of bytes to read. + /// The number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + int numRead; + + if (_diffStream == null) + { + _baseStream.Position = _position; + numRead = _baseStream.Read(buffer, offset, count); + } + else + { + if (_position > _diffStream.Length) + { + throw new IOException("Attempt to read beyond end of file"); + } + + int toRead = (int)Math.Min(count, _diffStream.Length - _position); + + // If the read is within the base stream's range, then touch it first to get the + // (potentially) stale data. + if (_position < _baseStream.Length) + { + int baseToRead = (int)Math.Min(toRead, _baseStream.Length - _position); + _baseStream.Position = _position; + + int totalBaseRead = 0; + while (totalBaseRead < baseToRead) + { + totalBaseRead += _baseStream.Read(buffer, offset + totalBaseRead, baseToRead - totalBaseRead); + } + } + + // Now overlay any data from the overlay stream (if any) + IEnumerable overlayExtents = StreamExtent.Intersect(_diffExtents, + new StreamExtent(_position, toRead)); + foreach (StreamExtent extent in overlayExtents) + { + _diffStream.Position = extent.Start; + int overlayNumRead = 0; + while (overlayNumRead < extent.Length) + { + overlayNumRead += _diffStream.Read( + buffer, + (int)(offset + (extent.Start - _position) + overlayNumRead), + (int)(extent.Length - overlayNumRead)); + } + } + + numRead = toRead; + } + + _position += numRead; + + return numRead; + } + + /// + /// Moves the stream position. + /// + /// The origin-relative location. + /// The base location. + /// The new absolute stream position. + public override long Seek(long offset, SeekOrigin origin) + { + CheckFrozen(); + + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += Length; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of disk"); + } + _position = effectiveOffset; + return _position; + } + + /// + /// Sets the length of the stream. + /// + /// The new length. + public override void SetLength(long value) + { + CheckFrozen(); + + if (_diffStream != null) + { + _diffStream.SetLength(value); + } + else + { + _baseStream.SetLength(value); + } + } + + /// + /// Writes data to the stream at the current location. + /// + /// The data to write. + /// The first byte to write from buffer. + /// The number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + CheckFrozen(); + + if (_diffStream != null) + { + _diffStream.Position = _position; + _diffStream.Write(buffer, offset, count); + + // Beware of Linq's delayed model - force execution now by placing into a list. + // Without this, large execution chains can build up (v. slow) and potential for stack overflow. + _diffExtents = + new List(StreamExtent.Union(_diffExtents, new StreamExtent(_position, count))); + + _position += count; + } + else + { + _baseStream.Position = _position; + _baseStream.Write(buffer, offset, count); + _position += count; + } + } + + /// + /// Disposes of this instance. + /// + /// true if called from Dispose(), else false. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_baseStreamOwnership == Ownership.Dispose && _baseStream != null) + { + _baseStream.Dispose(); + } + + _baseStream = null; + + if (_diffStream != null) + { + _diffStream.Dispose(); + } + + _diffStream = null; + } + + base.Dispose(disposing); + } + + private void CheckFrozen() + { + if (_frozen) + { + throw new InvalidOperationException("The stream is frozen"); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/SparseMemoryBuffer.cs b/DiscUtils/Streams/SparseMemoryBuffer.cs new file mode 100644 index 0000000..32dd78f --- /dev/null +++ b/DiscUtils/Streams/SparseMemoryBuffer.cs @@ -0,0 +1,252 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// A sparse in-memory buffer. + /// + /// This class is useful for storing large sparse buffers in memory, unused + /// chunks of the buffer are not stored (assumed to be zero). + public sealed class SparseMemoryBuffer : Buffer + { + private readonly Dictionary _buffers; + + private long _capacity; + + /// + /// Initializes a new instance of the SparseMemoryBuffer class. + /// + /// The size of each allocation chunk. + public SparseMemoryBuffer(int chunkSize) + { + ChunkSize = chunkSize; + _buffers = new Dictionary(); + } + + /// + /// Gets the (sorted) list of allocated chunks, as chunk indexes. + /// + /// An enumeration of chunk indexes. + /// This method returns chunks as an index rather than absolute stream position. + /// For example, if ChunkSize is 16KB, and the first 32KB of the buffer is actually stored, + /// this method will return 0 and 1. This indicates the first and second chunks are stored. + public IEnumerable AllocatedChunks + { + get + { + List keys = new List(_buffers.Keys); + keys.Sort(); + return keys; + } + } + + /// + /// Indicates this stream can be read (always true). + /// + public override bool CanRead + { + get { return true; } + } + + /// + /// Indicates this stream can be written (always true). + /// + public override bool CanWrite + { + get { return true; } + } + + /// + /// Gets the current capacity of the sparse buffer (number of logical bytes stored). + /// + public override long Capacity + { + get { return _capacity; } + } + + /// + /// Gets the size of each allocation chunk. + /// + public int ChunkSize { get; } + + /// + /// Accesses this memory buffer as an infinite byte array. + /// + /// The buffer position to read. + /// The byte stored at this position (or Zero if not explicitly stored). + public byte this[long pos] + { + get + { + byte[] buffer = new byte[1]; + if (Read(pos, buffer, 0, 1) != 0) + { + return buffer[0]; + } + return 0; + } + + set + { + byte[] buffer = new byte[1]; + buffer[0] = value; + Write(pos, buffer, 0, 1); + } + } + + /// + /// Reads a section of the sparse buffer into a byte array. + /// + /// The offset within the sparse buffer to start reading. + /// The destination byte array. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The actual number of bytes read. + public override int Read(long pos, byte[] buffer, int offset, int count) + { + int totalRead = 0; + + while (count > 0 && pos < _capacity) + { + int chunk = (int)(pos / ChunkSize); + int chunkOffset = (int)(pos % ChunkSize); + int numToRead = (int)Math.Min(Math.Min(ChunkSize - chunkOffset, _capacity - pos), count); + + if (!_buffers.TryGetValue(chunk, out byte[] chunkBuffer)) + { + Array.Clear(buffer, offset, numToRead); + } + else + { + Array.Copy(chunkBuffer, chunkOffset, buffer, offset, numToRead); + } + + totalRead += numToRead; + offset += numToRead; + count -= numToRead; + pos += numToRead; + } + + return totalRead; + } + + /// + /// Writes a byte array into the sparse buffer. + /// + /// The start offset within the sparse buffer. + /// The source byte array. + /// The start offset within the source byte array. + /// The number of bytes to write. + public override void Write(long pos, byte[] buffer, int offset, int count) + { + while (count > 0) + { + int chunk = (int)(pos / ChunkSize); + int chunkOffset = (int)(pos % ChunkSize); + int numToWrite = Math.Min(ChunkSize - chunkOffset, count); + + if (!_buffers.TryGetValue(chunk, out byte[] chunkBuffer)) + { + chunkBuffer = new byte[ChunkSize]; + _buffers[chunk] = chunkBuffer; + } + + Array.Copy(buffer, offset, chunkBuffer, chunkOffset, numToWrite); + + offset += numToWrite; + count -= numToWrite; + pos += numToWrite; + } + + _capacity = Math.Max(_capacity, pos); + } + + /// + /// Clears bytes from the buffer. + /// + /// The start offset within the buffer. + /// The number of bytes to clear. + public override void Clear(long pos, int count) + { + while (count > 0) + { + int chunk = (int)(pos / ChunkSize); + int chunkOffset = (int)(pos % ChunkSize); + int numToClear = Math.Min(ChunkSize - chunkOffset, count); + + if (_buffers.TryGetValue(chunk, out byte[] chunkBuffer)) + { + if (chunkOffset == 0 && numToClear == ChunkSize) + { + _buffers.Remove(chunk); + } + else + { + Array.Clear(chunkBuffer, chunkOffset, numToClear); + } + } + + count -= numToClear; + pos += numToClear; + } + + _capacity = Math.Max(_capacity, pos); + } + + /// + /// Sets the capacity of the sparse buffer, truncating if appropriate. + /// + /// The desired capacity of the buffer. + /// This method does not allocate any chunks, it merely records the logical + /// capacity of the sparse buffer. Writes beyond the specified capacity will increase + /// the capacity. + public override void SetCapacity(long value) + { + _capacity = value; + } + + /// + /// Gets the parts of a buffer that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + long end = start + count; + foreach (int chunk in AllocatedChunks) + { + long chunkStart = chunk * (long)ChunkSize; + long chunkEnd = chunkStart + ChunkSize; + if (chunkEnd > start && chunkStart < end) + { + long extentStart = Math.Max(start, chunkStart); + yield return new StreamExtent(extentStart, Math.Min(chunkEnd, end) - extentStart); + } + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/SparseMemoryStream.cs b/DiscUtils/Streams/SparseMemoryStream.cs new file mode 100644 index 0000000..2c91446 --- /dev/null +++ b/DiscUtils/Streams/SparseMemoryStream.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + /// + /// Provides a sparse equivalent to MemoryStream. + /// + public sealed class SparseMemoryStream : BufferStream + { + /// + /// Initializes a new instance of the SparseMemoryStream class. + /// + /// The created instance permits read and write access. + public SparseMemoryStream() + : base(new SparseMemoryBuffer(16 * 1024), FileAccess.ReadWrite) {} + + /// + /// Initializes a new instance of the SparseMemoryStream class. + /// + /// The buffer to use. + /// The access permitted to clients. + public SparseMemoryStream(SparseMemoryBuffer buffer, FileAccess access) + : base(buffer, access) {} + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/SparseStream.cs b/DiscUtils/Streams/SparseStream.cs new file mode 100644 index 0000000..b302aef --- /dev/null +++ b/DiscUtils/Streams/SparseStream.cs @@ -0,0 +1,324 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + /// + /// Represents a sparse stream. + /// + /// A sparse stream is a logically contiguous stream where some parts of the stream + /// aren't stored. The unstored parts are implicitly zero-byte ranges. + public abstract class SparseStream : Stream + { + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public abstract IEnumerable Extents { get; } + + /// + /// Converts any stream into a sparse stream. + /// + /// The stream to convert. + /// true to have the new stream dispose the wrapped + /// stream when it is disposed. + /// A sparse stream. + /// The returned stream has the entire wrapped stream as a + /// single extent. + public static SparseStream FromStream(Stream stream, Ownership takeOwnership) + { + return new SparseWrapperStream(stream, takeOwnership, null); + } + + /// + /// Converts any stream into a sparse stream. + /// + /// The stream to convert. + /// true to have the new stream dispose the wrapped + /// stream when it is disposed. + /// The set of extents actually stored in stream. + /// A sparse stream. + /// The returned stream has the entire wrapped stream as a + /// single extent. + public static SparseStream FromStream(Stream stream, Ownership takeOwnership, IEnumerable extents) + { + return new SparseWrapperStream(stream, takeOwnership, extents); + } + + /// + /// Efficiently pumps data from a sparse stream to another stream. + /// + /// The sparse stream to pump from. + /// The stream to pump to. + /// must support seeking. + public static void Pump(Stream inStream, Stream outStream) + { + Pump(inStream, outStream, Sizes.Sector); + } + + /// + /// Efficiently pumps data from a sparse stream to another stream. + /// + /// The stream to pump from. + /// The stream to pump to. + /// The smallest sequence of zero bytes that will be skipped when writing to . + /// must support seeking. + public static void Pump(Stream inStream, Stream outStream, int chunkSize) + { + StreamPump pump = new StreamPump(inStream, outStream, chunkSize); + pump.Run(); + } + + /// + /// Wraps a sparse stream in a read-only wrapper, preventing modification. + /// + /// The stream to make read-only. + /// Whether to transfer responsibility for calling Dispose on toWrap. + /// The read-only stream. + public static SparseStream ReadOnly(SparseStream toWrap, Ownership ownership) + { + return new SparseReadOnlyWrapperStream(toWrap, ownership); + } + + /// + /// Clears bytes from the stream. + /// + /// The number of bytes (from the current position) to clear. + /// + /// Logically equivalent to writing count null/zero bytes to the stream, some + /// implementations determine that some (or all) of the range indicated is not actually + /// stored. There is no direct, automatic, correspondence to clearing bytes and them + /// not being represented as an 'extent' - for example, the implementation of the underlying + /// stream may not permit fine-grained extent storage. + /// It is always safe to call this method to 'zero-out' a section of a stream, regardless of + /// the underlying stream implementation. + /// + public virtual void Clear(int count) + { + Write(new byte[count], 0, count); + } + + /// + /// Gets the parts of a stream that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public virtual IEnumerable GetExtentsInRange(long start, long count) + { + return StreamExtent.Intersect(Extents, new[] { new StreamExtent(start, count) }); + } + + private class SparseReadOnlyWrapperStream : SparseStream + { + private readonly Ownership _ownsWrapped; + private SparseStream _wrapped; + + public SparseReadOnlyWrapperStream(SparseStream wrapped, Ownership ownsWrapped) + { + _wrapped = wrapped; + _ownsWrapped = ownsWrapped; + } + + public override bool CanRead + { + get { return _wrapped.CanRead; } + } + + public override bool CanSeek + { + get { return _wrapped.CanSeek; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override IEnumerable Extents + { + get { return _wrapped.Extents; } + } + + public override long Length + { + get { return _wrapped.Length; } + } + + public override long Position + { + get { return _wrapped.Position; } + + set { _wrapped.Position = value; } + } + + public override void Flush() {} + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrapped.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _wrapped.Seek(offset, origin); + } + + public override void SetLength(long value) + { + throw new InvalidOperationException("Attempt to change length of read-only stream"); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new InvalidOperationException("Attempt to write to read-only stream"); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsWrapped == Ownership.Dispose && _wrapped != null) + { + _wrapped.Dispose(); + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } + + private class SparseWrapperStream : SparseStream + { + private readonly List _extents; + private readonly Ownership _ownsWrapped; + private Stream _wrapped; + + public SparseWrapperStream(Stream wrapped, Ownership ownsWrapped, IEnumerable extents) + { + _wrapped = wrapped; + _ownsWrapped = ownsWrapped; + if (extents != null) + { + _extents = new List(extents); + } + } + + public override bool CanRead + { + get { return _wrapped.CanRead; } + } + + public override bool CanSeek + { + get { return _wrapped.CanSeek; } + } + + public override bool CanWrite + { + get { return _wrapped.CanWrite; } + } + + public override IEnumerable Extents + { + get + { + if (_extents != null) + { + return _extents; + } + SparseStream wrappedAsSparse = _wrapped as SparseStream; + if (wrappedAsSparse != null) + { + return wrappedAsSparse.Extents; + } + return new[] { new StreamExtent(0, _wrapped.Length) }; + } + } + + public override long Length + { + get { return _wrapped.Length; } + } + + public override long Position + { + get { return _wrapped.Position; } + + set { _wrapped.Position = value; } + } + + public override void Flush() + { + _wrapped.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrapped.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _wrapped.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _wrapped.SetLength(value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (_extents != null) + { + throw new InvalidOperationException("Attempt to write to stream with explicit extents"); + } + + _wrapped.Write(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsWrapped == Ownership.Dispose && _wrapped != null) + { + _wrapped.Dispose(); + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/SparseStreamOpenDelegate.cs b/DiscUtils/Streams/SparseStreamOpenDelegate.cs new file mode 100644 index 0000000..1406258 --- /dev/null +++ b/DiscUtils/Streams/SparseStreamOpenDelegate.cs @@ -0,0 +1,4 @@ +namespace DiscUtils.Streams +{ + public delegate SparseStream SparseStreamOpenDelegate(); +} \ No newline at end of file diff --git a/DiscUtils/Streams/StreamBuffer.cs b/DiscUtils/Streams/StreamBuffer.cs new file mode 100644 index 0000000..d65685f --- /dev/null +++ b/DiscUtils/Streams/StreamBuffer.cs @@ -0,0 +1,164 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + /// + /// Converts a Stream into an IBuffer instance. + /// + public sealed class StreamBuffer : Buffer, IDisposable + { + private readonly Ownership _ownership; + private SparseStream _stream; + + /// + /// Initializes a new instance of the StreamBuffer class. + /// + /// The stream to wrap. + /// Whether to dispose stream, when this object is disposed. + public StreamBuffer(Stream stream, Ownership ownership) + { + if (stream == null) + { + throw new ArgumentNullException(nameof(stream)); + } + + _stream = stream as SparseStream; + if (_stream == null) + { + _stream = SparseStream.FromStream(stream, ownership); + _ownership = Ownership.Dispose; + } + else + { + _ownership = ownership; + } + } + + /// + /// Can this buffer be read. + /// + public override bool CanRead + { + get { return _stream.CanRead; } + } + + /// + /// Can this buffer be written. + /// + public override bool CanWrite + { + get { return _stream.CanWrite; } + } + + /// + /// Gets the current capacity of the buffer, in bytes. + /// + public override long Capacity + { + get { return _stream.Length; } + } + + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public override IEnumerable Extents + { + get { return _stream.Extents; } + } + + /// + /// Disposes of this instance. + /// + public void Dispose() + { + if (_ownership == Ownership.Dispose) + { + if (_stream != null) + { + _stream.Dispose(); + _stream = null; + } + } + } + + /// + /// Reads from the buffer into a byte array. + /// + /// The offset within the buffer to start reading. + /// The destination byte array. + /// The start offset within the destination buffer. + /// The number of bytes to read. + /// The actual number of bytes read. + public override int Read(long pos, byte[] buffer, int offset, int count) + { + _stream.Position = pos; + return _stream.Read(buffer, offset, count); + } + + /// + /// Writes a byte array into the buffer. + /// + /// The start offset within the buffer. + /// The source byte array. + /// The start offset within the source byte array. + /// The number of bytes to write. + public override void Write(long pos, byte[] buffer, int offset, int count) + { + _stream.Position = pos; + _stream.Write(buffer, offset, count); + } + + /// + /// Flushes all data to the underlying storage. + /// + public override void Flush() + { + _stream.Flush(); + } + + /// + /// Sets the capacity of the buffer, truncating if appropriate. + /// + /// The desired capacity of the buffer. + public override void SetCapacity(long value) + { + _stream.SetLength(value); + } + + /// + /// Gets the parts of a buffer that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + return _stream.GetExtentsInRange(start, count); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/StreamExtent.cs b/DiscUtils/Streams/StreamExtent.cs new file mode 100644 index 0000000..a00de5e --- /dev/null +++ b/DiscUtils/Streams/StreamExtent.cs @@ -0,0 +1,493 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Represents a range of bytes in a stream. + /// + /// This is normally used to represent regions of a SparseStream that + /// are actually stored in the underlying storage medium (rather than implied + /// zero bytes). Extents are stored as a zero-based byte offset (from the + /// beginning of the stream), and a byte length. + public sealed class StreamExtent : IEquatable, IComparable + { + /// + /// Initializes a new instance of the StreamExtent class. + /// + /// The start of the extent. + /// The length of the extent. + public StreamExtent(long start, long length) + { + Start = start; + Length = length; + } + + /// + /// Gets the start of the extent (in bytes). + /// + public long Length { get; } + + /// + /// Gets the start of the extent (in bytes). + /// + public long Start { get; } + + /// + /// Compares this stream extent to another. + /// + /// The extent to compare. + /// Value greater than zero if this extent starts after + /// other, zero if they start at the same position, else + /// a value less than zero. + public int CompareTo(StreamExtent other) + { + if (Start > other.Start) + { + return 1; + } + if (Start == other.Start) + { + return 0; + } + return -1; + } + + /// + /// Indicates if this StreamExtent is equal to another. + /// + /// The extent to compare. + /// true if the extents are equal, else false. + public bool Equals(StreamExtent other) + { + if (other == null) + { + return false; + } + return Start == other.Start && Length == other.Length; + } + + /// + /// Calculates the union of a list of extents with another extent. + /// + /// The list of extents. + /// The other extent. + /// The union of the extents. + public static IEnumerable Union(IEnumerable extents, StreamExtent other) + { + List otherList = new List(); + otherList.Add(other); + return Union(extents, otherList); + } + + /// + /// Calculates the union of the extents of multiple streams. + /// + /// The stream extents. + /// The union of the extents from multiple streams. + /// A typical use of this method is to calculate the combined set of + /// stored extents from a number of overlayed sparse streams. + public static IEnumerable Union(params IEnumerable[] streams) + { + long extentStart = long.MaxValue; + long extentEnd = 0; + + // Initialize enumerations and find first stored byte position + IEnumerator[] enums = new IEnumerator[streams.Length]; + bool[] streamsValid = new bool[streams.Length]; + int validStreamsRemaining = 0; + for (int i = 0; i < streams.Length; ++i) + { + enums[i] = streams[i].GetEnumerator(); + streamsValid[i] = enums[i].MoveNext(); + if (streamsValid[i]) + { + ++validStreamsRemaining; + if (enums[i].Current.Start < extentStart) + { + extentStart = enums[i].Current.Start; + extentEnd = enums[i].Current.Start + enums[i].Current.Length; + } + } + } + + while (validStreamsRemaining > 0) + { + // Find the end of this extent + bool foundIntersection; + do + { + foundIntersection = false; + validStreamsRemaining = 0; + for (int i = 0; i < streams.Length; ++i) + { + while (streamsValid[i] && enums[i].Current.Start + enums[i].Current.Length <= extentEnd) + { + streamsValid[i] = enums[i].MoveNext(); + } + + if (streamsValid[i]) + { + ++validStreamsRemaining; + } + + if (streamsValid[i] && enums[i].Current.Start <= extentEnd) + { + extentEnd = enums[i].Current.Start + enums[i].Current.Length; + foundIntersection = true; + streamsValid[i] = enums[i].MoveNext(); + } + } + } while (foundIntersection && validStreamsRemaining > 0); + + // Return the discovered extent + yield return new StreamExtent(extentStart, extentEnd - extentStart); + + // Find the next extent start point + extentStart = long.MaxValue; + validStreamsRemaining = 0; + for (int i = 0; i < streams.Length; ++i) + { + if (streamsValid[i]) + { + ++validStreamsRemaining; + if (enums[i].Current.Start < extentStart) + { + extentStart = enums[i].Current.Start; + extentEnd = enums[i].Current.Start + enums[i].Current.Length; + } + } + } + } + } + + /// + /// Calculates the intersection of the extents of a stream with another extent. + /// + /// The stream extents. + /// The extent to intersect. + /// The intersection of the extents. + public static IEnumerable Intersect(IEnumerable extents, StreamExtent other) + { + List otherList = new List(1); + otherList.Add(other); + return Intersect(extents, otherList); + } + + /// + /// Calculates the intersection of the extents of multiple streams. + /// + /// The stream extents. + /// The intersection of the extents from multiple streams. + /// A typical use of this method is to calculate the extents in a + /// region of a stream.. + public static IEnumerable Intersect(params IEnumerable[] streams) + { + long extentStart = long.MinValue; + long extentEnd = long.MaxValue; + + IEnumerator[] enums = new IEnumerator[streams.Length]; + for (int i = 0; i < streams.Length; ++i) + { + enums[i] = streams[i].GetEnumerator(); + if (!enums[i].MoveNext()) + { + // Gone past end of one stream (in practice was empty), so no intersections + yield break; + } + } + + int overlapsFound = 0; + while (true) + { + // We keep cycling round the streams, until we get streams.Length continuous overlaps + for (int i = 0; i < streams.Length; ++i) + { + // Move stream on past all extents that are earlier than our candidate start point + while (enums[i].Current.Length == 0 + || enums[i].Current.Start + enums[i].Current.Length <= extentStart) + { + if (!enums[i].MoveNext()) + { + // Gone past end of this stream, no more intersections possible + yield break; + } + } + + // If this stream has an extent that spans over the candidate start point + if (enums[i].Current.Start <= extentStart) + { + extentEnd = Math.Min(extentEnd, enums[i].Current.Start + enums[i].Current.Length); + overlapsFound++; + } + else + { + extentStart = enums[i].Current.Start; + extentEnd = extentStart + enums[i].Current.Length; + overlapsFound = 1; + } + + // We've just done a complete loop of all streams, they overlapped this start position + // and we've cut the extent's end down to the shortest run. + if (overlapsFound == streams.Length) + { + yield return new StreamExtent(extentStart, extentEnd - extentStart); + extentStart = extentEnd; + extentEnd = long.MaxValue; + overlapsFound = 0; + } + } + } + } + + /// + /// Calculates the subtraction of the extents of a stream by another extent. + /// + /// The stream extents. + /// The extent to subtract. + /// The subtraction of other from extents. + public static IEnumerable Subtract(IEnumerable extents, StreamExtent other) + { + return Subtract(extents, new[] { other }); + } + + /// + /// Calculates the subtraction of the extents of a stream by another stream. + /// + /// The stream extents to subtract from. + /// The stream extents to subtract. + /// The subtraction of the extents of b from a. + public static IEnumerable Subtract(IEnumerable a, IEnumerable b) + { + return Intersect(a, Invert(b)); + } + + /// + /// Calculates the inverse of the extents of a stream. + /// + /// The stream extents to inverse. + /// The inverted extents. + /// + /// This method assumes a logical stream addressable from 0 to long.MaxValue, and is undefined + /// should any stream extent start at less than 0. To constrain the extents to a specific range, use the + /// Intersect method. + /// + public static IEnumerable Invert(IEnumerable extents) + { + StreamExtent last = new StreamExtent(0, 0); + foreach (StreamExtent extent in extents) + { + // Skip over any 'noise' + if (extent.Length == 0) + { + continue; + } + + long lastEnd = last.Start + last.Length; + if (lastEnd < extent.Start) + { + yield return new StreamExtent(lastEnd, extent.Start - lastEnd); + } + + last = extent; + } + + long finalEnd = last.Start + last.Length; + if (finalEnd < long.MaxValue) + { + yield return new StreamExtent(finalEnd, long.MaxValue - finalEnd); + } + } + + /// + /// Offsets the extents of a stream. + /// + /// The stream extents. + /// The amount to offset the extents by. + /// The stream extents, offset by delta. + public static IEnumerable Offset(IEnumerable stream, long delta) + { + foreach (StreamExtent extent in stream) + { + yield return new StreamExtent(extent.Start + delta, extent.Length); + } + } + + /// + /// Returns the number of blocks containing stream data. + /// + /// The stream extents. + /// The size of each block. + /// The number of blocks containing stream data. + /// This method logically divides the stream into blocks of a specified + /// size, then indicates how many of those blocks contain actual stream data. + public static long BlockCount(IEnumerable stream, long blockSize) + { + long totalBlocks = 0; + long lastBlock = -1; + + foreach (StreamExtent extent in stream) + { + if (extent.Length > 0) + { + long extentStartBlock = extent.Start / blockSize; + long extentNextBlock = MathUtilities.Ceil(extent.Start + extent.Length, blockSize); + + long extentNumBlocks = extentNextBlock - extentStartBlock; + if (extentStartBlock == lastBlock) + { + extentNumBlocks--; + } + + lastBlock = extentNextBlock - 1; + + totalBlocks += extentNumBlocks; + } + } + + return totalBlocks; + } + + /// + /// Returns all of the blocks containing stream data. + /// + /// The stream extents. + /// The size of each block. + /// Ranges of blocks, as block indexes. + /// This method logically divides the stream into blocks of a specified + /// size, then indicates ranges of blocks that contain stream data. + public static IEnumerable> Blocks(IEnumerable stream, long blockSize) + { + long? rangeStart = null; + long rangeLength = 0; + + foreach (StreamExtent extent in stream) + { + if (extent.Length > 0) + { + long extentStartBlock = extent.Start / blockSize; + long extentNextBlock = MathUtilities.Ceil(extent.Start + extent.Length, blockSize); + + if (rangeStart != null && extentStartBlock > rangeStart + rangeLength) + { + // This extent is non-contiguous (in terms of blocks), so write out the last range and start new + yield return new Range((long)rangeStart, rangeLength); + rangeStart = extentStartBlock; + } + else if (rangeStart == null) + { + // First extent, so start first range + rangeStart = extentStartBlock; + } + + // Set the length of the current range, based on the end of this extent + rangeLength = extentNextBlock - (long)rangeStart; + } + } + + // Final range (if any ranges at all) hasn't been returned yet, so do that now + if (rangeStart != null) + { + yield return new Range((long)rangeStart, rangeLength); + } + } + + /// + /// The equality operator. + /// + /// The first extent to compare. + /// The second extent to compare. + /// Whether the two extents are equal. + public static bool operator ==(StreamExtent a, StreamExtent b) + { + if (ReferenceEquals(a, null)) + { + return ReferenceEquals(b, null); + } + return a.Equals(b); + } + + /// + /// The inequality operator. + /// + /// The first extent to compare. + /// The second extent to compare. + /// Whether the two extents are different. + public static bool operator !=(StreamExtent a, StreamExtent b) + { + return !(a == b); + } + + /// + /// The less-than operator. + /// + /// The first extent to compare. + /// The second extent to compare. + /// Whether a is less than b. + public static bool operator <(StreamExtent a, StreamExtent b) + { + return a.CompareTo(b) < 0; + } + + /// + /// The greater-than operator. + /// + /// The first extent to compare. + /// The second extent to compare. + /// Whether a is greater than b. + public static bool operator >(StreamExtent a, StreamExtent b) + { + return a.CompareTo(b) > 0; + } + + /// + /// Returns a string representation of the extent as [start:+length]. + /// + /// The string representation. + public override string ToString() + { + return "[" + Start + ":+" + Length + "]"; + } + + /// + /// Indicates if this stream extent is equal to another object. + /// + /// The object to test. + /// true if obj is equivalent, else false. + public override bool Equals(object obj) + { + return Equals(obj as StreamExtent); + } + + /// + /// Gets a hash code for this extent. + /// + /// The extent's hash code. + public override int GetHashCode() + { + return Start.GetHashCode() ^ Length.GetHashCode(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/StreamPump.cs b/DiscUtils/Streams/StreamPump.cs new file mode 100644 index 0000000..1b91eb8 --- /dev/null +++ b/DiscUtils/Streams/StreamPump.cs @@ -0,0 +1,270 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Utility class for pumping the contents of one stream into another. + /// + /// + /// This class is aware of sparse streams, and will avoid copying data that is not + /// valid in the source stream. This functionality should normally only be used + /// when the destination stream is known not to contain any existing data. + /// + public sealed class StreamPump + { + /// + /// Initializes a new instance of the StreamPump class. + /// + public StreamPump() + { + SparseChunkSize = 512; + BufferSize = (int)(512 * Sizes.OneKiB); + SparseCopy = true; + } + + /// + /// Initializes a new instance of the StreamPump class. + /// + /// The stream to read from. + /// The stream to write to. + /// The size of each sparse chunk. + public StreamPump(Stream inStream, Stream outStream, int sparseChunkSize) + { + InputStream = inStream; + OutputStream = outStream; + SparseChunkSize = sparseChunkSize; + BufferSize = (int)(512 * Sizes.OneKiB); + SparseCopy = true; + } + + /// + /// Gets or sets the amount of data to read at a time from InputStream. + /// + public int BufferSize { get; set; } + + /// + /// Gets the number of bytes read from InputStream. + /// + public long BytesRead { get; private set; } + + /// + /// Gets the number of bytes written to OutputStream. + /// + public long BytesWritten { get; private set; } + + /// + /// Gets or sets the stream that will be read from. + /// + public Stream InputStream { get; set; } + + /// + /// Gets or sets the stream that will be written to. + /// + public Stream OutputStream { get; set; } + + /// + /// Gets or sets, for sparse transfers, the size of each chunk. + /// + /// + /// A chunk is transfered if any byte in the chunk is valid, otherwise it is not. + /// This value should normally be set to reflect the underlying storage granularity + /// of OutputStream. + /// + public int SparseChunkSize { get; set; } + + /// + /// Gets or sets a value indicating whether to enable the sparse copy behaviour (default true). + /// + public bool SparseCopy { get; set; } + + /// + /// Event raised periodically through the pump operation. + /// + /// + /// This event is signalled synchronously, so to avoid slowing the pumping activity + /// implementations should return quickly. + /// + public event EventHandler ProgressEvent; + + /// + /// Performs the pump activity, blocking until complete. + /// + public void Run() + { + if (InputStream == null) + { + throw new InvalidOperationException("Input stream is null"); + } + + if (OutputStream == null) + { + throw new InvalidOperationException("Output stream is null"); + } + + if (!OutputStream.CanSeek) + { + throw new InvalidOperationException("Output stream does not support seek operations"); + } + + if (SparseChunkSize <= 1) + { + throw new InvalidOperationException("Chunk size is invalid"); + } + + if (SparseCopy) + { + RunSparse(); + } + else + { + RunNonSparse(); + } + } + + private static bool IsAllZeros(byte[] buffer, int offset, int count) + { + for (int j = 0; j < count; j++) + { + if (buffer[offset + j] != 0) + { + return false; + } + } + + return true; + } + + private void RunNonSparse() + { + byte[] copyBuffer = new byte[BufferSize]; + + InputStream.Position = 0; + OutputStream.Position = 0; + + int numRead = InputStream.Read(copyBuffer, 0, copyBuffer.Length); + while (numRead > 0) + { + BytesRead += numRead; + + OutputStream.Write(copyBuffer, 0, numRead); + BytesWritten += numRead; + + RaiseProgressEvent(); + + numRead = InputStream.Read(copyBuffer, 0, copyBuffer.Length); + } + } + + private void RunSparse() + { + SparseStream inStream = InputStream as SparseStream; + if (inStream == null) + { + inStream = SparseStream.FromStream(InputStream, Ownership.None); + } + + if (BufferSize > SparseChunkSize && BufferSize % SparseChunkSize != 0) + { + throw new InvalidOperationException("Buffer size is not a multiple of the sparse chunk size"); + } + + byte[] copyBuffer = new byte[Math.Max(BufferSize, SparseChunkSize)]; + + BytesRead = 0; + BytesWritten = 0; + + foreach (StreamExtent extent in inStream.Extents) + { + inStream.Position = extent.Start; + + long extentOffset = 0; + while (extentOffset < extent.Length) + { + int numRead = (int)Math.Min(copyBuffer.Length, extent.Length - extentOffset); + StreamUtilities.ReadExact(inStream, copyBuffer, 0, numRead); + BytesRead += numRead; + + int copyBufferOffset = 0; + for (int i = 0; i < numRead; i += SparseChunkSize) + { + if (IsAllZeros(copyBuffer, i, Math.Min(SparseChunkSize, numRead - i))) + { + if (copyBufferOffset < i) + { + OutputStream.Position = extent.Start + extentOffset + copyBufferOffset; + OutputStream.Write(copyBuffer, copyBufferOffset, i - copyBufferOffset); + BytesWritten += i - copyBufferOffset; + } + + copyBufferOffset = i + SparseChunkSize; + } + } + + if (copyBufferOffset < numRead) + { + OutputStream.Position = extent.Start + extentOffset + copyBufferOffset; + OutputStream.Write(copyBuffer, copyBufferOffset, numRead - copyBufferOffset); + BytesWritten += numRead - copyBufferOffset; + } + + extentOffset += numRead; + + RaiseProgressEvent(); + } + } + + // Ensure the output stream is at least as long as the input stream. This uses + // read/write, rather than SetLength, to avoid failing on streams that can't be + // explicitly resized. Side-effect of this, is that if outStream is an NTFS + // file stream, then actual clusters will be allocated out to at least the + // length of the input stream. + if (OutputStream.Length < inStream.Length) + { + inStream.Position = inStream.Length - 1; + int b = inStream.ReadByte(); + if (b >= 0) + { + OutputStream.Position = inStream.Length - 1; + OutputStream.WriteByte((byte)b); + } + } + } + + private void RaiseProgressEvent() + { + // Raise the event by using the () operator. + if (ProgressEvent != null) + { + PumpProgressEventArgs args = new PumpProgressEventArgs(); + args.BytesRead = BytesRead; + args.BytesWritten = BytesWritten; + args.SourcePosition = InputStream.Position; + args.DestinationPosition = OutputStream.Position; + ProgressEvent(this, args); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/StripedStream.cs b/DiscUtils/Streams/StripedStream.cs new file mode 100644 index 0000000..cff0f8f --- /dev/null +++ b/DiscUtils/Streams/StripedStream.cs @@ -0,0 +1,221 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + public class StripedStream : SparseStream + { + private readonly bool _canRead; + private readonly bool _canWrite; + private readonly long _length; + private readonly Ownership _ownsWrapped; + + private long _position; + private readonly long _stripeSize; + private List _wrapped; + + public StripedStream(long stripeSize, Ownership ownsWrapped, params SparseStream[] wrapped) + { + _wrapped = new List(wrapped); + _stripeSize = stripeSize; + _ownsWrapped = ownsWrapped; + + _canRead = _wrapped[0].CanRead; + _canWrite = _wrapped[0].CanWrite; + long subStreamLength = _wrapped[0].Length; + + foreach (SparseStream stream in _wrapped) + { + if (stream.CanRead != _canRead || stream.CanWrite != _canWrite) + { + throw new ArgumentException("All striped streams must have the same read/write permissions", + nameof(wrapped)); + } + + if (stream.Length != subStreamLength) + { + throw new ArgumentException("All striped streams must have the same length", nameof(wrapped)); + } + } + + _length = subStreamLength * wrapped.Length; + } + + public override bool CanRead + { + get { return _canRead; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return _canWrite; } + } + + public override IEnumerable Extents + { + get + { + // Temporary, indicate there are no 'unstored' extents. + // Consider combining extent information from all wrapped streams in future. + yield return new StreamExtent(0, _length); + } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + + set { _position = value; } + } + + public override void Flush() + { + foreach (SparseStream stream in _wrapped) + { + stream.Flush(); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (!CanRead) + { + throw new InvalidOperationException("Attempt to read to non-readable stream"); + } + + int maxToRead = (int)Math.Min(_length - _position, count); + + int totalRead = 0; + while (totalRead < maxToRead) + { + long stripe = _position / _stripeSize; + long stripeOffset = _position % _stripeSize; + int stripeToRead = (int)Math.Min(maxToRead - totalRead, _stripeSize - stripeOffset); + + int streamIdx = (int)(stripe % _wrapped.Count); + long streamStripe = stripe / _wrapped.Count; + + Stream targetStream = _wrapped[streamIdx]; + targetStream.Position = streamStripe * _stripeSize + stripeOffset; + + int numRead = targetStream.Read(buffer, offset + totalRead, stripeToRead); + _position += numRead; + totalRead += numRead; + } + + return totalRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += _length; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of stream"); + } + _position = effectiveOffset; + return _position; + } + + public override void SetLength(long value) + { + if (value != _length) + { + throw new InvalidOperationException("Changing the stream length is not permitted for striped streams"); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (!CanWrite) + { + throw new InvalidOperationException("Attempt to write to read-only stream"); + } + + if (_position + count > _length) + { + throw new IOException("Attempt to write beyond end of stream"); + } + + int totalWritten = 0; + while (totalWritten < count) + { + long stripe = _position / _stripeSize; + long stripeOffset = _position % _stripeSize; + int stripeToWrite = (int)Math.Min(count - totalWritten, _stripeSize - stripeOffset); + + int streamIdx = (int)(stripe % _wrapped.Count); + long streamStripe = stripe / _wrapped.Count; + + Stream targetStream = _wrapped[streamIdx]; + targetStream.Position = streamStripe * _stripeSize + stripeOffset; + targetStream.Write(buffer, offset + totalWritten, stripeToWrite); + + _position += stripeToWrite; + totalWritten += stripeToWrite; + } + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing && _ownsWrapped == Ownership.Dispose && _wrapped != null) + { + foreach (SparseStream stream in _wrapped) + { + stream.Dispose(); + } + + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/SubStream.cs b/DiscUtils/Streams/SubStream.cs new file mode 100644 index 0000000..58dce20 --- /dev/null +++ b/DiscUtils/Streams/SubStream.cs @@ -0,0 +1,212 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + public class SubStream : MappedStream + { + private readonly long _first; + private readonly long _length; + private readonly Ownership _ownsParent; + + private readonly Stream _parent; + private long _position; + + public SubStream(Stream parent, long first, long length) + { + _parent = parent; + _first = first; + _length = length; + _ownsParent = Ownership.None; + + if (_first + _length > _parent.Length) + { + throw new ArgumentException("Substream extends beyond end of parent stream"); + } + } + + public SubStream(Stream parent, Ownership ownsParent, long first, long length) + { + _parent = parent; + _ownsParent = ownsParent; + _first = first; + _length = length; + + if (_first + _length > _parent.Length) + { + throw new ArgumentException("Substream extends beyond end of parent stream"); + } + } + + public override bool CanRead + { + get { return _parent.CanRead; } + } + + public override bool CanSeek + { + get { return _parent.CanSeek; } + } + + public override bool CanWrite + { + get { return _parent.CanWrite; } + } + + public override IEnumerable Extents + { + get + { + SparseStream parentAsSparse = _parent as SparseStream; + if (parentAsSparse != null) + { + return OffsetExtents(parentAsSparse.GetExtentsInRange(_first, _length)); + } + return new[] { new StreamExtent(0, _length) }; + } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + + set + { + if (value <= _length) + { + _position = value; + } + else + { + throw new ArgumentOutOfRangeException(nameof(value), "Attempt to move beyond end of stream"); + } + } + } + + public override IEnumerable MapContent(long start, long length) + { + return new[] { new StreamExtent(start + _first, length) }; + } + + public override void Flush() + { + _parent.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Attempt to read negative bytes"); + } + + if (_position > _length) + { + return 0; + } + + _parent.Position = _first + _position; + int numRead = _parent.Read(buffer, offset, + (int)Math.Min(count, Math.Min(_length - _position, int.MaxValue))); + _position += numRead; + return numRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long absNewPos = offset; + if (origin == SeekOrigin.Current) + { + absNewPos += _position; + } + else if (origin == SeekOrigin.End) + { + absNewPos += _length; + } + + if (absNewPos < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Attempt to move before start of stream"); + } + + _position = absNewPos; + return _position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException("Attempt to change length of a substream"); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), "Attempt to write negative bytes"); + } + + if (_position + count > _length) + { + throw new ArgumentOutOfRangeException(nameof(count), "Attempt to write beyond end of substream"); + } + + _parent.Position = _first + _position; + _parent.Write(buffer, offset, count); + _position += count; + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_ownsParent == Ownership.Dispose) + { + _parent.Dispose(); + } + } + } + finally + { + base.Dispose(disposing); + } + } + + private IEnumerable OffsetExtents(IEnumerable src) + { + foreach (StreamExtent e in src) + { + yield return new StreamExtent(e.Start - _first, e.Length); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/System/Func.cs b/DiscUtils/Streams/System/Func.cs new file mode 100644 index 0000000..0a7ac0b --- /dev/null +++ b/DiscUtils/Streams/System/Func.cs @@ -0,0 +1,7 @@ + +#if NET20 +namespace System +{ + public delegate TResult Func(T arg); +} +#endif \ No newline at end of file diff --git a/DiscUtils/Streams/ThreadSafeStream.cs b/DiscUtils/Streams/ThreadSafeStream.cs new file mode 100644 index 0000000..b860135 --- /dev/null +++ b/DiscUtils/Streams/ThreadSafeStream.cs @@ -0,0 +1,336 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + /// + /// Provides a thread-safe wrapping around a sparse stream. + /// + /// + /// Streams are inherently not thread-safe (because read/write is not atomic w.r.t. Position). + /// This method enables multiple 'views' of a stream to be created (each with their own Position), and ensures + /// only a single operation is executing on the wrapped stream at any time. + /// This example shows the pattern of use: + /// + /// + /// SparseStream baseStream = ...; + /// ThreadSafeStream tss = new ThreadSafeStream(baseStream); + /// for(int i = 0; i < 10; ++i) + /// { + /// SparseStream streamForThread = tss.OpenView(); + /// } + /// + /// + /// This results in 11 streams that can be used in different streams - tss and ten 'views' created from tss. + /// Note, the stream length cannot be changed. + /// + public class ThreadSafeStream : SparseStream + { + private CommonState _common; + private readonly bool _ownsCommon; + private long _position; + + /// + /// Initializes a new instance of the ThreadSafeStream class. + /// + /// The stream to wrap. + /// Do not directly modify toWrap after wrapping it, unless the thread-safe views + /// will no longer be used. + public ThreadSafeStream(SparseStream toWrap) + : this(toWrap, Ownership.None) {} + + /// + /// Initializes a new instance of the ThreadSafeStream class. + /// + /// The stream to wrap. + /// Whether to transfer ownership of toWrap to the new instance. + /// Do not directly modify toWrap after wrapping it, unless the thread-safe views + /// will no longer be used. + public ThreadSafeStream(SparseStream toWrap, Ownership ownership) + { + if (!toWrap.CanSeek) + { + throw new ArgumentException("Wrapped stream must support seeking", nameof(toWrap)); + } + + _common = new CommonState + { + WrappedStream = toWrap, + WrappedStreamOwnership = ownership + }; + _ownsCommon = true; + } + + private ThreadSafeStream(ThreadSafeStream toClone) + { + _common = toClone._common; + if (_common == null) + { + throw new ObjectDisposedException("toClone"); + } + } + + /// + /// Gets a value indicating if this stream supports reads. + /// + public override bool CanRead + { + get + { + lock (_common) + { + return Wrapped.CanRead; + } + } + } + + /// + /// Gets a value indicating if this stream supports seeking (always true). + /// + public override bool CanSeek + { + get { return true; } + } + + /// + /// Gets a value indicating if this stream supports writes (currently, always false). + /// + public override bool CanWrite + { + get + { + lock (_common) + { + return Wrapped.CanWrite; + } + } + } + + /// + /// Gets the parts of the stream that are stored. + /// + /// This may be an empty enumeration if all bytes are zero. + public override IEnumerable Extents + { + get + { + lock (_common) + { + return Wrapped.Extents; + } + } + } + + /// + /// Gets the length of the stream. + /// + public override long Length + { + get + { + lock (_common) + { + return Wrapped.Length; + } + } + } + + /// + /// Gets the current stream position - each 'view' has it's own Position. + /// + public override long Position + { + get { return _position; } + + set { _position = value; } + } + + private SparseStream Wrapped + { + get + { + SparseStream wrapped = _common.WrappedStream; + if (wrapped == null) + { + throw new ObjectDisposedException("ThreadSafeStream"); + } + + return wrapped; + } + } + + /// + /// Opens a new thread-safe view on the stream. + /// + /// The new view. + public SparseStream OpenView() + { + return new ThreadSafeStream(this); + } + + /// + /// Gets the parts of a stream that are stored, within a specified range. + /// + /// The offset of the first byte of interest. + /// The number of bytes of interest. + /// An enumeration of stream extents, indicating stored bytes. + public override IEnumerable GetExtentsInRange(long start, long count) + { + lock (_common) + { + return Wrapped.GetExtentsInRange(start, count); + } + } + + /// + /// Causes the stream to flush all changes. + /// + public override void Flush() + { + lock (_common) + { + Wrapped.Flush(); + } + } + + /// + /// Reads data from the stream. + /// + /// The buffer to fill. + /// The first byte in buffer to fill. + /// The requested number of bytes to read. + /// The actual number of bytes read. + public override int Read(byte[] buffer, int offset, int count) + { + lock (_common) + { + SparseStream wrapped = Wrapped; + wrapped.Position = _position; + int numRead = wrapped.Read(buffer, offset, count); + _position += numRead; + return numRead; + } + } + + /// + /// Changes the current stream position (each view has it's own Position). + /// + /// The relative location to move to. + /// The origin of the location. + /// The new location as an absolute position. + public override long Seek(long offset, SeekOrigin origin) + { + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += Length; + } + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of disk"); + } + _position = effectiveOffset; + return _position; + } + + /// + /// Sets the length of the stream (not supported). + /// + /// The new length. + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + /// + /// Writes data to the stream (not currently supported). + /// + /// The data to write. + /// The first byte to write. + /// The number of bytes to write. + public override void Write(byte[] buffer, int offset, int count) + { + lock (_common) + { + SparseStream wrapped = Wrapped; + + if (_position + count > wrapped.Length) + { + throw new IOException("Attempt to extend stream"); + } + + wrapped.Position = _position; + wrapped.Write(buffer, offset, count); + _position += count; + } + } + + /// + /// Disposes of this instance, invalidating any remaining views. + /// + /// true if disposing, lese false. + protected override void Dispose(bool disposing) + { + if (disposing) + { + if (_ownsCommon && _common != null) + { + lock (_common) + { + if (_common.WrappedStreamOwnership == Ownership.Dispose) + { + _common.WrappedStream.Dispose(); + } + + _common.Dispose(); + } + } + } + + _common = null; + } + + private sealed class CommonState : IDisposable + { + public SparseStream WrappedStream; + public Ownership WrappedStreamOwnership; + + #region IDisposable Members + + public void Dispose() + { + WrappedStream = null; + } + + #endregion + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Util/BitCounter.cs b/DiscUtils/Streams/Util/BitCounter.cs new file mode 100644 index 0000000..1d4ae36 --- /dev/null +++ b/DiscUtils/Streams/Util/BitCounter.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2017, Bianco Veigel +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Helper to count the number of bits set in a byte or byte[] + /// + public static class BitCounter + { + private static readonly byte[] _lookupTable; + + static BitCounter() + { + _lookupTable = new byte[256]; + for (int i = 0; i < 256; i++) + { + byte bitCount = 0; + var value = i; + while (value != 0) + { + bitCount++; + value &= (byte)(value - 1); + } + _lookupTable[i] = bitCount; + } + } + + /// + /// count the number of bits set in + /// + /// the number of bits set in + public static byte Count(byte value) + { + return _lookupTable[value]; + } + + /// + /// count the number of bits set in each entry of + /// + /// the to process + /// the values offset to start from + /// the number of bytes to count + /// + public static long Count(byte[] values, int offset, int count) + { + var end = offset + count; + if (end > values.Length) + throw new ArgumentOutOfRangeException(nameof(count), "can't count after end of values"); + var result = 0L; + for (int i = offset; i < end; i++) + { + var value = values[i]; + result += _lookupTable[value]; + } + return result; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Util/EndianUtilities.cs b/DiscUtils/Streams/Util/EndianUtilities.cs new file mode 100644 index 0000000..4745493 --- /dev/null +++ b/DiscUtils/Streams/Util/EndianUtilities.cs @@ -0,0 +1,306 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + public static class EndianUtilities + { + #region Bit Twiddling + + public static void WriteBytesLittleEndian(ushort val, byte[] buffer, int offset) + { + buffer[offset] = (byte)(val & 0xFF); + buffer[offset + 1] = (byte)((val >> 8) & 0xFF); + } + + public static void WriteBytesLittleEndian(uint val, byte[] buffer, int offset) + { + buffer[offset] = (byte)(val & 0xFF); + buffer[offset + 1] = (byte)((val >> 8) & 0xFF); + buffer[offset + 2] = (byte)((val >> 16) & 0xFF); + buffer[offset + 3] = (byte)((val >> 24) & 0xFF); + } + + public static void WriteBytesLittleEndian(ulong val, byte[] buffer, int offset) + { + buffer[offset] = (byte)(val & 0xFF); + buffer[offset + 1] = (byte)((val >> 8) & 0xFF); + buffer[offset + 2] = (byte)((val >> 16) & 0xFF); + buffer[offset + 3] = (byte)((val >> 24) & 0xFF); + buffer[offset + 4] = (byte)((val >> 32) & 0xFF); + buffer[offset + 5] = (byte)((val >> 40) & 0xFF); + buffer[offset + 6] = (byte)((val >> 48) & 0xFF); + buffer[offset + 7] = (byte)((val >> 56) & 0xFF); + } + + public static void WriteBytesLittleEndian(short val, byte[] buffer, int offset) + { + WriteBytesLittleEndian((ushort)val, buffer, offset); + } + + public static void WriteBytesLittleEndian(int val, byte[] buffer, int offset) + { + WriteBytesLittleEndian((uint)val, buffer, offset); + } + + public static void WriteBytesLittleEndian(long val, byte[] buffer, int offset) + { + WriteBytesLittleEndian((ulong)val, buffer, offset); + } + + public static void WriteBytesLittleEndian(Guid val, byte[] buffer, int offset) + { + byte[] le = val.ToByteArray(); + Array.Copy(le, 0, buffer, offset, 16); + } + + public static void WriteBytesBigEndian(ushort val, byte[] buffer, int offset) + { + buffer[offset] = (byte)(val >> 8); + buffer[offset + 1] = (byte)(val & 0xFF); + } + + public static void WriteBytesBigEndian(uint val, byte[] buffer, int offset) + { + buffer[offset] = (byte)((val >> 24) & 0xFF); + buffer[offset + 1] = (byte)((val >> 16) & 0xFF); + buffer[offset + 2] = (byte)((val >> 8) & 0xFF); + buffer[offset + 3] = (byte)(val & 0xFF); + } + + public static void WriteBytesBigEndian(ulong val, byte[] buffer, int offset) + { + buffer[offset] = (byte)((val >> 56) & 0xFF); + buffer[offset + 1] = (byte)((val >> 48) & 0xFF); + buffer[offset + 2] = (byte)((val >> 40) & 0xFF); + buffer[offset + 3] = (byte)((val >> 32) & 0xFF); + buffer[offset + 4] = (byte)((val >> 24) & 0xFF); + buffer[offset + 5] = (byte)((val >> 16) & 0xFF); + buffer[offset + 6] = (byte)((val >> 8) & 0xFF); + buffer[offset + 7] = (byte)(val & 0xFF); + } + + public static void WriteBytesBigEndian(short val, byte[] buffer, int offset) + { + WriteBytesBigEndian((ushort)val, buffer, offset); + } + + public static void WriteBytesBigEndian(int val, byte[] buffer, int offset) + { + WriteBytesBigEndian((uint)val, buffer, offset); + } + + public static void WriteBytesBigEndian(long val, byte[] buffer, int offset) + { + WriteBytesBigEndian((ulong)val, buffer, offset); + } + + public static void WriteBytesBigEndian(Guid val, byte[] buffer, int offset) + { + byte[] le = val.ToByteArray(); + WriteBytesBigEndian(ToUInt32LittleEndian(le, 0), buffer, offset + 0); + WriteBytesBigEndian(ToUInt16LittleEndian(le, 4), buffer, offset + 4); + WriteBytesBigEndian(ToUInt16LittleEndian(le, 6), buffer, offset + 6); + Array.Copy(le, 8, buffer, offset + 8, 8); + } + + public static ushort ToUInt16LittleEndian(byte[] buffer, int offset) + { + return (ushort)(((buffer[offset + 1] << 8) & 0xFF00) | ((buffer[offset + 0] << 0) & 0x00FF)); + } + + public static uint ToUInt32LittleEndian(byte[] buffer, int offset) + { + return (uint)(((buffer[offset + 3] << 24) & 0xFF000000U) | ((buffer[offset + 2] << 16) & 0x00FF0000U) + | ((buffer[offset + 1] << 8) & 0x0000FF00U) | ((buffer[offset + 0] << 0) & 0x000000FFU)); + } + + public static ulong ToUInt64LittleEndian(byte[] buffer, int offset) + { + return ((ulong)ToUInt32LittleEndian(buffer, offset + 4) << 32) | ToUInt32LittleEndian(buffer, offset + 0); + } + + public static short ToInt16LittleEndian(byte[] buffer, int offset) + { + return (short)ToUInt16LittleEndian(buffer, offset); + } + + public static int ToInt32LittleEndian(byte[] buffer, int offset) + { + return (int)ToUInt32LittleEndian(buffer, offset); + } + + public static long ToInt64LittleEndian(byte[] buffer, int offset) + { + return (long)ToUInt64LittleEndian(buffer, offset); + } + + public static ushort ToUInt16BigEndian(byte[] buffer, int offset) + { + return (ushort)(((buffer[offset] << 8) & 0xFF00) | ((buffer[offset + 1] << 0) & 0x00FF)); + } + + public static uint ToUInt32BigEndian(byte[] buffer, int offset) + { + uint val = (uint)(((buffer[offset + 0] << 24) & 0xFF000000U) | ((buffer[offset + 1] << 16) & 0x00FF0000U) + | ((buffer[offset + 2] << 8) & 0x0000FF00U) | ((buffer[offset + 3] << 0) & 0x000000FFU)); + return val; + } + + public static ulong ToUInt64BigEndian(byte[] buffer, int offset) + { + return ((ulong)ToUInt32BigEndian(buffer, offset + 0) << 32) | ToUInt32BigEndian(buffer, offset + 4); + } + + public static short ToInt16BigEndian(byte[] buffer, int offset) + { + return (short)ToUInt16BigEndian(buffer, offset); + } + + public static int ToInt32BigEndian(byte[] buffer, int offset) + { + return (int)ToUInt32BigEndian(buffer, offset); + } + + public static long ToInt64BigEndian(byte[] buffer, int offset) + { + return (long)ToUInt64BigEndian(buffer, offset); + } + + public static Guid ToGuidLittleEndian(byte[] buffer, int offset) + { + byte[] temp = new byte[16]; + Array.Copy(buffer, offset, temp, 0, 16); + return new Guid(temp); + } + + public static Guid ToGuidBigEndian(byte[] buffer, int offset) + { + return new Guid( + ToUInt32BigEndian(buffer, offset + 0), + ToUInt16BigEndian(buffer, offset + 4), + ToUInt16BigEndian(buffer, offset + 6), + buffer[offset + 8], + buffer[offset + 9], + buffer[offset + 10], + buffer[offset + 11], + buffer[offset + 12], + buffer[offset + 13], + buffer[offset + 14], + buffer[offset + 15]); + } + + public static byte[] ToByteArray(byte[] buffer, int offset, int length) + { + byte[] result = new byte[length]; + Array.Copy(buffer, offset, result, 0, length); + return result; + } + + public static T ToStruct(byte[] buffer, int offset) + where T : IByteArraySerializable, new() + { + T result = new T(); + result.ReadFrom(buffer, offset); + return result; + } + + /// + /// Primitive conversion from Unicode to ASCII that preserves special characters. + /// + /// The string to convert. + /// The buffer to fill. + /// The start of the string in the buffer. + /// The number of characters to convert. + /// The built-in ASCIIEncoding converts characters of codepoint > 127 to ?, + /// this preserves those code points by removing the top 16 bits of each character. + public static void StringToBytes(string value, byte[] dest, int offset, int count) + { + char[] chars = value.ToCharArray(0, Math.Min(value.Length, count)); + + int i = 0; + while (i < chars.Length && i < count) + { + dest[i + offset] = (byte)chars[i]; + ++i; + } + + while (i < count) + { + dest[i + offset] = 0; + ++i; + } + } + + /// + /// Primitive conversion from ASCII to Unicode that preserves special characters. + /// + /// The data to convert. + /// The first byte to convert. + /// The number of bytes to convert. + /// The string. + /// The built-in ASCIIEncoding converts characters of codepoint > 127 to ?, + /// this preserves those code points. + public static string BytesToString(byte[] data, int offset, int count) + { + char[] result = new char[count]; + + for (int i = 0; i < count; ++i) + { + result[i] = (char)data[i + offset]; + } + + return new string(result); + } + + /// + /// Primitive conversion from ASCII to Unicode that stops at a null-terminator. + /// + /// The data to convert. + /// The first byte to convert. + /// The number of bytes to convert. + /// The string. + /// The built-in ASCIIEncoding converts characters of codepoint > 127 to ?, + /// this preserves those code points. + public static string BytesToZString(byte[] data, int offset, int count) + { + char[] result = new char[count]; + + for (int i = 0; i < count; ++i) + { + byte ch = data[i + offset]; + if (ch == 0) + { + return new string(result, 0, i); + } + + result[i] = (char)ch; + } + + return new string(result); + } + + #endregion + } +} diff --git a/DiscUtils/Streams/Util/MathUtilities.cs b/DiscUtils/Streams/Util/MathUtilities.cs new file mode 100644 index 0000000..ef73210 --- /dev/null +++ b/DiscUtils/Streams/Util/MathUtilities.cs @@ -0,0 +1,137 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + public static class MathUtilities + { + /// + /// Round up a value to a multiple of a unit size. + /// + /// The value to round up. + /// The unit (the returned value will be a multiple of this number). + /// The rounded-up value. + public static long RoundUp(long value, long unit) + { + return (value + (unit - 1)) / unit * unit; + } + + /// + /// Round up a value to a multiple of a unit size. + /// + /// The value to round up. + /// The unit (the returned value will be a multiple of this number). + /// The rounded-up value. + public static int RoundUp(int value, int unit) + { + return (value + (unit - 1)) / unit * unit; + } + + /// + /// Round down a value to a multiple of a unit size. + /// + /// The value to round down. + /// The unit (the returned value will be a multiple of this number). + /// The rounded-down value. + public static long RoundDown(long value, long unit) + { + return value / unit * unit; + } + + /// + /// Calculates the CEIL function. + /// + /// The value to divide. + /// The value to divide by. + /// The value of CEIL(numerator/denominator). + public static int Ceil(int numerator, int denominator) + { + return (numerator + (denominator - 1)) / denominator; + } + + /// + /// Calculates the CEIL function. + /// + /// The value to divide. + /// The value to divide by. + /// The value of CEIL(numerator/denominator). + public static uint Ceil(uint numerator, uint denominator) + { + return (numerator + (denominator - 1)) / denominator; + } + + /// + /// Calculates the CEIL function. + /// + /// The value to divide. + /// The value to divide by. + /// The value of CEIL(numerator/denominator). + public static long Ceil(long numerator, long denominator) + { + return (numerator + (denominator - 1)) / denominator; + } + + public static int Log2(uint val) + { + if (val == 0) + { + throw new ArgumentException("Cannot calculate log of Zero", nameof(val)); + } + + int result = 0; + while ((val & 1) != 1) + { + val >>= 1; + ++result; + } + + if (val == 1) + { + return result; + } + throw new ArgumentException("Input is not a power of Two", nameof(val)); + } + + public static int Log2(int val) + { + if (val == 0) + { + throw new ArgumentException("Cannot calculate log of Zero", nameof(val)); + } + + int result = 0; + while ((val & 1) != 1) + { + val >>= 1; + ++result; + } + + if (val == 1) + { + return result; + } + throw new ArgumentException("Input is not a power of Two", nameof(val)); + } + } +} diff --git a/DiscUtils/Streams/Util/Numbers.cs b/DiscUtils/Streams/Util/Numbers.cs new file mode 100644 index 0000000..764ec2f --- /dev/null +++ b/DiscUtils/Streams/Util/Numbers.cs @@ -0,0 +1,228 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + internal static class Numbers + where T : struct, IComparable, IEquatable + { + public delegate bool ComparisonFn(T a, T b); + + public delegate T ConvertIntFn(int a); + + public delegate T ConvertLongFn(long a); + + public delegate T DualParamFn(T a, T b); + + public delegate T NoParamFn(); + + public static readonly T Zero = default(T); + public static readonly T One = GetOne(); + public static readonly DualParamFn Add = GetAdd(); + public static readonly DualParamFn Subtract = GetSubtract(); + public static readonly DualParamFn Multiply = GetMultiply(); + public static readonly DualParamFn Divide = GetDivide(); + public static readonly DualParamFn RoundUp = GetRoundUp(); + public static readonly DualParamFn RoundDown = GetRoundDown(); + public static readonly DualParamFn Ceil = GetCeil(); + public static readonly ConvertLongFn ConvertLong = GetConvertLong(); + public static readonly ConvertIntFn ConvertInt = GetConvertInt(); + + public static bool GreaterThan(T a, T b) + { + return a.CompareTo(b) > 0; + } + + public static bool GreaterThanOrEqual(T a, T b) + { + return a.CompareTo(b) >= 0; + } + + public static bool LessThan(T a, T b) + { + return a.CompareTo(b) < 0; + } + + public static bool LessThanOrEqual(T a, T b) + { + return a.CompareTo(b) <= 0; + } + + public static bool Equal(T a, T b) + { + return a.CompareTo(b) == 0; + } + + public static bool NotEqual(T a, T b) + { + return a.CompareTo(b) != 0; + } + + private static T GetOne() + { + if (typeof(T) == typeof(long)) + { + return ((NoParamFn)(object)new LongNoParamFn(() => { return 1; }))(); + } + if (typeof(T) == typeof(int)) + { + return ((NoParamFn)(object)new IntNoParamFn(() => { return 1; }))(); + } + throw new NotSupportedException(); + } + + private static ConvertLongFn GetConvertLong() + { + if (typeof(T) == typeof(long)) + { + return (ConvertLongFn)(object)new LongConvertLongFn(x => { return x; }); + } + if (typeof(T) == typeof(int)) + { + return (ConvertLongFn)(object)new IntConvertLongFn(x => { return (int)x; }); + } + throw new NotSupportedException(); + } + + private static ConvertIntFn GetConvertInt() + { + if (typeof(T) == typeof(long)) + { + return (ConvertIntFn)(object)new LongConvertIntFn(x => { return x; }); + } + if (typeof(T) == typeof(int)) + { + return (ConvertIntFn)(object)new IntConvertIntFn(x => { return x; }); + } + throw new NotSupportedException(); + } + + private static DualParamFn GetAdd() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((a, b) => { return a + b; }); + } + if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((a, b) => { return a + b; }); + } + throw new NotSupportedException(); + } + + private static DualParamFn GetSubtract() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((a, b) => { return a - b; }); + } + if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((a, b) => { return a - b; }); + } + throw new NotSupportedException(); + } + + private static DualParamFn GetMultiply() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((a, b) => { return a * b; }); + } + if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((a, b) => { return a * b; }); + } + throw new NotSupportedException(); + } + + private static DualParamFn GetDivide() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((a, b) => { return a / b; }); + } + if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((a, b) => { return a / b; }); + } + throw new NotSupportedException(); + } + + private static DualParamFn GetRoundUp() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((a, b) => { return (a + b - 1) / b * b; }); + } + if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((a, b) => { return (a + b - 1) / b * b; }); + } + throw new NotSupportedException(); + } + + private static DualParamFn GetRoundDown() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((a, b) => { return a / b * b; }); + } + if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((a, b) => { return a / b * b; }); + } + throw new NotSupportedException(); + } + + private static DualParamFn GetCeil() + { + if (typeof(T) == typeof(long)) + { + return (DualParamFn)(object)new LongDualParamFn((a, b) => { return (a + b - 1) / b; }); + } + if (typeof(T) == typeof(int)) + { + return (DualParamFn)(object)new IntDualParamFn((a, b) => { return (a + b - 1) / b; }); + } + throw new NotSupportedException(); + } + + private delegate long LongNoParamFn(); + + private delegate long LongDualParamFn(long a, long b); + + private delegate long LongConvertLongFn(long x); + + private delegate long LongConvertIntFn(int x); + + private delegate int IntNoParamFn(); + + private delegate int IntDualParamFn(int a, int b); + + private delegate int IntConvertLongFn(long x); + + private delegate int IntConvertIntFn(int x); + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Util/Ownership.cs b/DiscUtils/Streams/Util/Ownership.cs new file mode 100644 index 0000000..856e50a --- /dev/null +++ b/DiscUtils/Streams/Util/Ownership.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Streams +{ + /// + /// Enumeration used to indicate transfer of disposable objects. + /// + public enum Ownership + { + /// + /// Indicates there is no transfer of ownership. + /// + None, + + /// + /// Indicates ownership of the stream is transfered, the owner should dispose of the stream when appropriate. + /// + Dispose + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Util/Range.cs b/DiscUtils/Streams/Util/Range.cs new file mode 100644 index 0000000..a892650 --- /dev/null +++ b/DiscUtils/Streams/Util/Range.cs @@ -0,0 +1,131 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + /// + /// Represents a range of values. + /// + /// The type of the offset element. + /// The type of the size element. + public class Range : IEquatable> + where TOffset : IEquatable + where TCount : IEquatable + { + /// + /// Initializes a new instance of the Range class. + /// + /// The offset (i.e. start) of the range. + /// The size of the range. + public Range(TOffset offset, TCount count) + { + Offset = offset; + Count = count; + } + + /// + /// Gets the size of the range. + /// + public TCount Count { get; } + + /// + /// Gets the offset (i.e. start) of the range. + /// + public TOffset Offset { get; } + + #region IEquatable> Members + + /// + /// Compares this range to another. + /// + /// The range to compare. + /// true if the ranges are equivalent, else false. + public bool Equals(Range other) + { + if (other == null) + { + return false; + } + + return Offset.Equals(other.Offset) && Count.Equals(other.Count); + } + + #endregion + + /// + /// Merges sets of ranges into chunks. + /// + /// The ranges to merge. + /// The size of each chunk. + /// Ranges combined into larger chunks. + /// The type of the offset and count in the ranges. + public static IEnumerable> Chunked(IEnumerable> ranges, T chunkSize) + where T : struct, IEquatable, IComparable + { + T? chunkStart = Numbers.Zero; + T chunkLength = Numbers.Zero; + + foreach (Range range in ranges) + { + if (Numbers.NotEqual(range.Count, Numbers.Zero)) + { + T rangeStart = Numbers.RoundDown(range.Offset, chunkSize); + T rangeNext = Numbers.RoundUp(Numbers.Add(range.Offset, range.Count), chunkSize); + + if (chunkStart.HasValue && + Numbers.GreaterThan(rangeStart, Numbers.Add(chunkStart.Value, chunkLength))) + { + // This extent is non-contiguous (in terms of blocks), so write out the last range and start new + yield return new Range(chunkStart.Value, chunkLength); + chunkStart = rangeStart; + } + else if (!chunkStart.HasValue) + { + // First extent, so start first range + chunkStart = rangeStart; + } + + // Set the length of the current range, based on the end of this extent + chunkLength = Numbers.Subtract(rangeNext, chunkStart.Value); + } + } + + // Final range (if any ranges at all) hasn't been returned yet, so do that now + if (chunkStart.HasValue) + { + yield return new Range(chunkStart.Value, chunkLength); + } + } + + /// + /// Returns a string representation of the extent as [start:+length]. + /// + /// The string representation. + public override string ToString() + { + return "[" + Offset + ":+" + Count + "]"; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Util/Sizes.cs b/DiscUtils/Streams/Util/Sizes.cs new file mode 100644 index 0000000..847239a --- /dev/null +++ b/DiscUtils/Streams/Util/Sizes.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Streams +{ + public static class Sizes + { + public const long OneKiB = 1024; + public const long OneMiB = 1024 * OneKiB; + public const long OneGiB = 1024 * OneMiB; + + public const int Sector = 512; + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/Util/StreamUtilities.cs b/DiscUtils/Streams/Util/StreamUtilities.cs new file mode 100644 index 0000000..f922012 --- /dev/null +++ b/DiscUtils/Streams/Util/StreamUtilities.cs @@ -0,0 +1,288 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Streams +{ + public static class StreamUtilities + { + /// + /// Validates standard buffer, offset, count parameters to a method. + /// + /// The byte array to read from / write to. + /// The starting offset in buffer. + /// The number of bytes to read / write. + public static void AssertBufferParameters(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset is negative"); + } + + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), count, "Count is negative"); + } + + if (buffer.Length < offset + count) + { + throw new ArgumentException("buffer is too small", nameof(buffer)); + } + } + + #region Stream Manipulation + + /// + /// Read bytes until buffer filled or throw EndOfStreamException. + /// + /// The stream to read. + /// The buffer to populate. + /// Offset in the buffer to start. + /// The number of bytes to read. + public static void ReadExact(Stream stream, byte[] buffer, int offset, int count) + { + int originalCount = count; + + while (count > 0) + { + int numRead = stream.Read(buffer, offset, count); + + if (numRead == 0) + { + throw new EndOfStreamException("Unable to complete read of " + originalCount + " bytes"); + } + + offset += numRead; + count -= numRead; + } + } + + /// + /// Read bytes until buffer filled or throw EndOfStreamException. + /// + /// The stream to read. + /// The number of bytes to read. + /// The data read from the stream. + public static byte[] ReadExact(Stream stream, int count) + { + byte[] buffer = new byte[count]; + + ReadExact(stream, buffer, 0, count); + + return buffer; + } + + /// + /// Read bytes until buffer filled or throw EndOfStreamException. + /// + /// The stream to read. + /// The position in buffer to read from. + /// The buffer to populate. + /// Offset in the buffer to start. + /// The number of bytes to read. + public static void ReadExact(IBuffer buffer, long pos, byte[] data, int offset, int count) + { + int originalCount = count; + + while (count > 0) + { + int numRead = buffer.Read(pos, data, offset, count); + + if (numRead == 0) + { + throw new EndOfStreamException("Unable to complete read of " + originalCount + " bytes"); + } + + pos += numRead; + offset += numRead; + count -= numRead; + } + } + + /// + /// Read bytes until buffer filled or throw EndOfStreamException. + /// + /// The buffer to read. + /// The position in buffer to read from. + /// The number of bytes to read. + /// The data read from the stream. + public static byte[] ReadExact(IBuffer buffer, long pos, int count) + { + byte[] result = new byte[count]; + + ReadExact(buffer, pos, result, 0, count); + + return result; + } + + /// + /// Read bytes until buffer filled or EOF. + /// + /// The stream to read. + /// The buffer to populate. + /// Offset in the buffer to start. + /// The number of bytes to read. + /// The number of bytes actually read. + public static int ReadMaximum(Stream stream, byte[] buffer, int offset, int count) + { + int totalRead = 0; + + while (count > 0) + { + int numRead = stream.Read(buffer, offset, count); + + if (numRead == 0) + { + return totalRead; + } + + offset += numRead; + count -= numRead; + totalRead += numRead; + } + + return totalRead; + } + + /// + /// Read bytes until buffer filled or EOF. + /// + /// The stream to read. + /// The position in buffer to read from. + /// The buffer to populate. + /// Offset in the buffer to start. + /// The number of bytes to read. + /// The number of bytes actually read. + public static int ReadMaximum(IBuffer buffer, long pos, byte[] data, int offset, int count) + { + int totalRead = 0; + + while (count > 0) + { + int numRead = buffer.Read(pos, data, offset, count); + + if (numRead == 0) + { + return totalRead; + } + + pos += numRead; + offset += numRead; + count -= numRead; + totalRead += numRead; + } + + return totalRead; + } + + /// + /// Read bytes until buffer filled or throw EndOfStreamException. + /// + /// The buffer to read. + /// The data read from the stream. + public static byte[] ReadAll(IBuffer buffer) + { + return ReadExact(buffer, 0, (int)buffer.Capacity); + } + + /// + /// Reads a disk sector (512 bytes). + /// + /// The stream to read. + /// The sector data as a byte array. + public static byte[] ReadSector(Stream stream) + { + return ReadExact(stream, Sizes.Sector); + } + + /// + /// Reads a structure from a stream. + /// + /// The type of the structure. + /// The stream to read. + /// The structure. + public static T ReadStruct(Stream stream) + where T : IByteArraySerializable, new() + { + T result = new T(); + byte[] buffer = ReadExact(stream, result.Size); + result.ReadFrom(buffer, 0); + return result; + } + + /// + /// Reads a structure from a stream. + /// + /// The type of the structure. + /// The stream to read. + /// The number of bytes to read. + /// The structure. + public static T ReadStruct(Stream stream, int length) + where T : IByteArraySerializable, new() + { + T result = new T(); + byte[] buffer = ReadExact(stream, length); + result.ReadFrom(buffer, 0); + return result; + } + + /// + /// Writes a structure to a stream. + /// + /// The type of the structure. + /// The stream to write to. + /// The structure to write. + public static void WriteStruct(Stream stream, T obj) + where T : IByteArraySerializable + { + byte[] buffer = new byte[obj.Size]; + obj.WriteTo(buffer, 0); + stream.Write(buffer, 0, buffer.Length); + } + + /// + /// Copies the contents of one stream to another. + /// + /// The stream to copy from. + /// The destination stream. + /// Copying starts at the current stream positions. + public static void PumpStreams(Stream source, Stream dest) + { + byte[] buffer = new byte[8192]; + int numRead; + + while ((numRead = source.Read(buffer, 0, buffer.Length)) > 0) + { + dest.Write(buffer, 0, numRead); + } + } + + #endregion + } +} diff --git a/DiscUtils/Streams/WrappingMappedStream.cs b/DiscUtils/Streams/WrappingMappedStream.cs new file mode 100644 index 0000000..6a1d655 --- /dev/null +++ b/DiscUtils/Streams/WrappingMappedStream.cs @@ -0,0 +1,165 @@ +// +// Copyright (c) 2008-2012, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; + +namespace DiscUtils.Streams +{ + /// + /// Base class for streams that wrap another stream. + /// + /// The type of stream to wrap. + /// + /// Provides the default implementation of methods & properties, so + /// wrapping streams need only override the methods they need to intercept. + /// + public class WrappingMappedStream : MappedStream + where T : Stream + { + private readonly List _extents; + private readonly Ownership _ownership; + + public WrappingMappedStream(T toWrap, Ownership ownership, IEnumerable extents) + { + WrappedStream = toWrap; + _ownership = ownership; + if (extents != null) + { + _extents = new List(extents); + } + } + + public override bool CanRead + { + get { return WrappedStream.CanRead; } + } + + public override bool CanSeek + { + get { return WrappedStream.CanSeek; } + } + + public override bool CanWrite + { + get { return WrappedStream.CanWrite; } + } + + public override IEnumerable Extents + { + get + { + if (_extents != null) + { + return _extents; + } + SparseStream sparse = WrappedStream as SparseStream; + if (sparse != null) + { + return sparse.Extents; + } + return new[] { new StreamExtent(0, WrappedStream.Length) }; + } + } + + public override long Length + { + get { return WrappedStream.Length; } + } + + public override long Position + { + get { return WrappedStream.Position; } + set { WrappedStream.Position = value; } + } + + protected T WrappedStream { get; private set; } + + public override IEnumerable MapContent(long start, long length) + { + MappedStream mapped = WrappedStream as MappedStream; + if (mapped != null) + { + return mapped.MapContent(start, length); + } + return new[] { new StreamExtent(start, length) }; + } + + public override void Flush() + { + WrappedStream.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return WrappedStream.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return WrappedStream.Seek(offset, origin); + } + + public override void SetLength(long value) + { + WrappedStream.SetLength(value); + } + + public override void Clear(int count) + { + SparseStream sparse = WrappedStream as SparseStream; + if (sparse != null) + { + sparse.Clear(count); + } + else + { + base.Clear(count); + } + } + + public override void Write(byte[] buffer, int offset, int count) + { + WrappedStream.Write(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (WrappedStream != null && _ownership == Ownership.Dispose) + { + WrappedStream.Dispose(); + } + + WrappedStream = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/WrappingStream.cs b/DiscUtils/Streams/WrappingStream.cs new file mode 100644 index 0000000..0c80378 --- /dev/null +++ b/DiscUtils/Streams/WrappingStream.cs @@ -0,0 +1,127 @@ +// +// Copyright (c) 2008-2012, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; + +namespace DiscUtils.Streams +{ + /// + /// Base class for streams that wrap another stream. + /// + /// + /// Provides the default implementation of methods & properties, so + /// wrapping streams need only override the methods they need to intercept. + /// + public class WrappingStream : SparseStream + { + private readonly Ownership _ownership; + private SparseStream _wrapped; + + public WrappingStream(SparseStream toWrap, Ownership ownership) + { + _wrapped = toWrap; + _ownership = ownership; + } + + public override bool CanRead + { + get { return _wrapped.CanRead; } + } + + public override bool CanSeek + { + get { return _wrapped.CanSeek; } + } + + public override bool CanWrite + { + get { return _wrapped.CanWrite; } + } + + public override IEnumerable Extents + { + get { return _wrapped.Extents; } + } + + public override long Length + { + get { return _wrapped.Length; } + } + + public override long Position + { + get { return _wrapped.Position; } + set { _wrapped.Position = value; } + } + + public override void Flush() + { + _wrapped.Flush(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + return _wrapped.Read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + return _wrapped.Seek(offset, origin); + } + + public override void SetLength(long value) + { + _wrapped.SetLength(value); + } + + public override void Clear(int count) + { + _wrapped.Clear(count); + } + + public override void Write(byte[] buffer, int offset, int count) + { + _wrapped.Write(buffer, offset, count); + } + + protected override void Dispose(bool disposing) + { + try + { + if (disposing) + { + if (_wrapped != null && _ownership == Ownership.Dispose) + { + _wrapped.Dispose(); + } + + _wrapped = null; + } + } + finally + { + base.Dispose(disposing); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Streams/ZeroStream.cs b/DiscUtils/Streams/ZeroStream.cs new file mode 100644 index 0000000..8e60d0c --- /dev/null +++ b/DiscUtils/Streams/ZeroStream.cs @@ -0,0 +1,144 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Streams +{ + /// + /// A stream that returns Zero's. + /// + public class ZeroStream : MappedStream + { + private bool _atEof; + private readonly long _length; + private long _position; + + public ZeroStream(long length) + { + _length = length; + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override IEnumerable Extents + { + // The stream is entirely sparse + get { return new List(0); } + } + + public override long Length + { + get { return _length; } + } + + public override long Position + { + get { return _position; } + + set + { + _position = value; + _atEof = false; + } + } + + public override IEnumerable MapContent(long start, long length) + { + return new StreamExtent[0]; + } + + public override void Flush() {} + + public override int Read(byte[] buffer, int offset, int count) + { + if (_position > _length) + { + _atEof = true; + throw new IOException("Attempt to read beyond end of stream"); + } + + if (_position == _length) + { + if (_atEof) + { + throw new IOException("Attempt to read beyond end of stream"); + } + _atEof = true; + return 0; + } + + int numToClear = (int)Math.Min(count, _length - _position); + Array.Clear(buffer, offset, numToClear); + _position += numToClear; + + return numToClear; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long effectiveOffset = offset; + if (origin == SeekOrigin.Current) + { + effectiveOffset += _position; + } + else if (origin == SeekOrigin.End) + { + effectiveOffset += _length; + } + + _atEof = false; + + if (effectiveOffset < 0) + { + throw new IOException("Attempt to move before beginning of stream"); + } + _position = effectiveOffset; + return _position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/PbpResign/Program.cs b/PbpResign/Program.cs index 80d71ea..47f1ca0 100644 --- a/PbpResign/Program.cs +++ b/PbpResign/Program.cs @@ -1072,10 +1072,10 @@ namespace PbpResign return false; } type = 0; - + /* Console.WriteLine("VersionKey: " + BitConverter.ToString(NewVersionKey.ToArray())); - UmdDisc disc = new UmdDisc("fft.iso"); + UmdInfo disc = new UmdInfo("fft.iso"); NpUmdImg npumd = new NpUmdImg(new NpDrmInfo(NewVersionKey.ToArray(), CId, npHdr.NpFlags), disc, false); npumd.CreatePsar(); @@ -1087,7 +1087,7 @@ namespace PbpResign disc.DataFiles["PIC1.PNG"], disc.DataFiles["SND0.AT3"], npumd, - "FFT.PBP"); + "FFT.PBP"); */ return CopyNpUmdImg(input, output, pbpHdr, psarBuff, npHdr); } diff --git a/PopsBuilder/GameBuilder.csproj b/PopsBuilder/GameBuilder.csproj index 34888e1..2a3fceb 100644 --- a/PopsBuilder/GameBuilder.csproj +++ b/PopsBuilder/GameBuilder.csproj @@ -7,11 +7,7 @@ - - - - - + diff --git a/PopsBuilder/Pops/DiscCompressor.cs b/PopsBuilder/Pops/DiscCompressor.cs index 3ddab68..061713b 100644 --- a/PopsBuilder/Pops/DiscCompressor.cs +++ b/PopsBuilder/Pops/DiscCompressor.cs @@ -1,5 +1,6 @@ using GameBuilder.Atrac3; using GameBuilder.Cue; +using GameBuilder.Progress; using GameBuilder.Psp; using PspCrypto; using System; @@ -11,13 +12,13 @@ using System.Threading.Tasks; namespace GameBuilder.Pops { - public class DiscCompressor + public class DiscCompressor : ProgressTracker { const int COMPRESS_BLOCK_SZ = 0x9300; const int DEFAULT_ISO_OFFSET = 0x100000; public int IsoOffset; - internal DiscCompressor(NpDrmPsar srcImg, DiscInfo disc, IAtracEncoderBase encoder, int offset = DEFAULT_ISO_OFFSET) + internal DiscCompressor(PopsImg srcImg, DiscInfo disc, IAtracEncoderBase encoder, int offset = DEFAULT_ISO_OFFSET) { this.srcImg = srcImg; this.disc = disc; @@ -100,9 +101,9 @@ namespace GameBuilder.Pops using (EccRemoverStream eccRem = new EccRemoverStream(cueStr)) { while (eccRem.Position < eccRem.Length) - { - Console.Write(Math.Floor(Convert.ToDouble(eccRem.Position) / Convert.ToDouble(eccRem.Length) * 100.0) + "%\r"); + { writeCompressedIsoBlock(eccRem); + UpdateProgress(Convert.ToInt32(eccRem.Position), Convert.ToInt32(eccRem.Length), "Compress & Encrypt Disc"); } } } @@ -162,8 +163,7 @@ namespace GameBuilder.Pops for (int i = 1; i <= totalTracks; i++) { if (cue.GetTrackNumber(i).TrackType != TrackType.TRACK_CDDA) continue; - - Console.WriteLine("Encoding track " + i + " to ATRAC3."); + UpdateProgress(i, totalTracks, "Convert CD Audio tracks to ATRAC3"); using (CueStream audioStream = cue.OpenTrack(i)) { @@ -222,7 +222,7 @@ namespace GameBuilder.Pops private DiscInfo disc; private CueReader cue; - private NpDrmPsar srcImg; + private PopsImg srcImg; public MemoryStream IsoHeader; public MemoryStream CompressedIso; diff --git a/PopsBuilder/Pops/DiscInfo.cs b/PopsBuilder/Pops/DiscInfo.cs index 229709a..ff0da81 100644 --- a/PopsBuilder/Pops/DiscInfo.cs +++ b/PopsBuilder/Pops/DiscInfo.cs @@ -1,4 +1,7 @@ -using System; +using DiscUtils.Iso9660; +using DiscUtils.Streams; +using GameBuilder.Cue; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -13,7 +16,8 @@ namespace GameBuilder.Pops private string discId; public string CueFile - { get + { + get { return cueFile; } @@ -42,11 +46,39 @@ namespace GameBuilder.Pops } } - public DiscInfo(string cueFile, string discName, string discId) + public DiscInfo(string cueFile, string discName) { this.cueFile = cueFile; this.discName = discName; - this.discId = discId; + + using(CueReader cue = new CueReader(cueFile)) + { + using (CueStream cueStream = cue.OpenTrack(cue.FirstDataTrackNo)) + { + using (CDReader cdReader = new CDReader(cueStream, false, true, cue.GetTrackNumber(cue.FirstDataTrackNo).SectorSz)) + { + using (SparseStream systemCnfStream = cdReader.OpenFile("SYSTEM.CNF", FileMode.Open)) + { + using (StreamReader systemCnfReader = new StreamReader(systemCnfStream)) + { + for (string? line = systemCnfReader.ReadLine(); line is not null; line = systemCnfReader.ReadLine()) + { + line = line.Trim().ReplaceLineEndings("").ToUpperInvariant(); + + if (line.StartsWith("BOOT")) + { + // wew thats a big one liner xD + this.discId = line.Split('=').Last().Trim().Split(';').First().Replace('\\', '/').Split('/').Last().Replace(".", "").Replace("_", ""); + } + } + } + } + } + } + } + + if (discId is null) discId = "SLUS00001"; + } } } diff --git a/PopsBuilder/Pops/PopsImg.cs b/PopsBuilder/Pops/PopsImg.cs index 872a014..219dc93 100644 --- a/PopsBuilder/Pops/PopsImg.cs +++ b/PopsBuilder/Pops/PopsImg.cs @@ -12,6 +12,7 @@ namespace GameBuilder.Pops { public class PopsImg : NpDrmPsar { + public PopsImg(NpDrmInfo versionKey) : base(versionKey) { simple = new MemoryStream(); @@ -22,7 +23,6 @@ namespace GameBuilder.Pops SimplePgd = generateSimplePgd(); } - private MemoryStream simple; private StreamUtil simpleUtil; diff --git a/PopsBuilder/Pops/PsIsoImg.cs b/PopsBuilder/Pops/PsIsoImg.cs index b14ce0d..742d1a6 100644 --- a/PopsBuilder/Pops/PsIsoImg.cs +++ b/PopsBuilder/Pops/PsIsoImg.cs @@ -7,6 +7,7 @@ using System; using System.Net; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; +using GameBuilder.Progress; namespace GameBuilder.Pops { @@ -20,11 +21,13 @@ namespace GameBuilder.Pops public PsIsoImg(NpDrmInfo versionKey, DiscInfo disc, IAtracEncoderBase encoder) : base(versionKey) { this.compressor = new DiscCompressor(this, disc, encoder); + this.compressor.RegisterCallback(onProgress); } public PsIsoImg(NpDrmInfo versionKey, DiscInfo disc) : base(versionKey) { this.compressor = new DiscCompressor(this, disc, new Atrac3ToolEncoder()); + this.compressor.RegisterCallback(onProgress); } public void CreatePsar(bool isPartOfMultiDisc=false) { @@ -59,6 +62,13 @@ namespace GameBuilder.Pops Psar.Seek(0x00, SeekOrigin.Begin); } + + + private void onProgress(ProgressInfo inf) + { + this.UpdateProgress(inf.Done, inf.Remain, inf.CurrentProcess); + } + private DiscCompressor compressor; } diff --git a/PopsBuilder/Pops/PsTitleImg.cs b/PopsBuilder/Pops/PsTitleImg.cs index 255c056..7327c08 100644 --- a/PopsBuilder/Pops/PsTitleImg.cs +++ b/PopsBuilder/Pops/PsTitleImg.cs @@ -1,4 +1,5 @@ using GameBuilder.Atrac3; +using GameBuilder.Progress; using GameBuilder.Psp; using PspCrypto; using System; @@ -14,6 +15,14 @@ namespace GameBuilder.Pops { const int MAX_DISCS = 5; const int PSISO_ALIGN = 0x8000; + + private int discNumber = 0; + + private void onProgress(ProgressInfo inf) + { + this.UpdateProgress(inf.Done, inf.Remain, inf.CurrentProcess + " (disc " + discNumber + ")"); + } + public PsTitleImg(NpDrmInfo drmInfo, DiscInfo[] discs) : base(drmInfo) { if (discs.Length > MAX_DISCS) throw new Exception("Sorry, multi disc games only support up to 5 discs... (i dont make the rules)"); @@ -22,8 +31,15 @@ namespace GameBuilder.Pops for (int i = 0; i < compressors.Length; i++) { - if (i > (discs.Length - 1)) compressors[i] = null; - else compressors[i] = new DiscCompressor(this, discs[i], new Atrac3ToolEncoder()); + if (i > (discs.Length - 1)) + { + compressors[i] = null; + } + else + { + compressors[i] = new DiscCompressor(this, discs[i], new Atrac3ToolEncoder()); + compressors[i].RegisterCallback(onProgress); + } } @@ -96,6 +112,7 @@ namespace GameBuilder.Pops byte[] checksums = new byte[0x10 * MAX_DISCS]; for(int i = 0; i < MAX_DISCS; i++) { + discNumber++; if (compressors[i] is null) { isoMapUtil.WriteInt32(0); continue; }; int padLen = Convert.ToInt32(PSISO_ALIGN - (isoPart.Position % PSISO_ALIGN)); @@ -107,7 +124,6 @@ namespace GameBuilder.Pops psIsoImg.CreatePsar(true); - psIsoImg.Psar.Seek(0x0, SeekOrigin.Begin); compressors[i].IsoHeader.Seek(0x00, SeekOrigin.Begin); diff --git a/PopsBuilder/Progress/ProgressInfo.cs b/PopsBuilder/Progress/ProgressInfo.cs new file mode 100644 index 0000000..2f2597d --- /dev/null +++ b/PopsBuilder/Progress/ProgressInfo.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GameBuilder.Progress +{ + public class ProgressInfo + { + private int totalDone; + private int totalRemain; + private string currentlyDoing; + + public int Done + { + get + { + return totalDone; + } + } + + public int Remain + { + get + { + return totalRemain; + } + } + + public string CurrentProcess + { + get + { + return currentlyDoing; + } + } + + public double Progress + { + get + { + return Convert.ToDouble(totalDone) / Convert.ToDouble(totalRemain) * 100.0; + } + } + + public int ProgressInt + { + get + { + return Convert.ToInt32(Math.Floor(Progress)); + } + } + + internal ProgressInfo(int done, int remain, string currentProcess) + { + totalDone = done; + totalRemain = remain; + currentlyDoing = currentProcess; + } + } +} diff --git a/PopsBuilder/Progress/ProgressTracker.cs b/PopsBuilder/Progress/ProgressTracker.cs new file mode 100644 index 0000000..abfa737 --- /dev/null +++ b/PopsBuilder/Progress/ProgressTracker.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GameBuilder.Progress +{ + public class ProgressTracker + { + private List> progressCallbacks = new List>(); + + public void RegisterCallback(Action cb) + { + progressCallbacks.Add(cb); + } + + public void UpdateProgress(int total, int remain, string what) + { + ProgressInfo inf = new ProgressInfo(total, remain, what); + foreach (Action cb in progressCallbacks) + cb(inf); + } + } +} diff --git a/PopsBuilder/Psp/NpDrmPsar.cs b/PopsBuilder/Psp/NpDrmPsar.cs index 3c3ede6..f8a29fb 100644 --- a/PopsBuilder/Psp/NpDrmPsar.cs +++ b/PopsBuilder/Psp/NpDrmPsar.cs @@ -1,4 +1,5 @@ -using PspCrypto; +using GameBuilder.Progress; +using PspCrypto; using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +10,7 @@ using System.Threading.Tasks; namespace GameBuilder.Psp { - public abstract class NpDrmPsar : IDisposable + public abstract class NpDrmPsar : ProgressTracker, IDisposable { public NpDrmPsar(NpDrmInfo npDrmInfo) { @@ -17,10 +18,10 @@ namespace GameBuilder.Psp Psar = new MemoryStream(); psarUtil = new StreamUtil(Psar); + } public NpDrmInfo DrmInfo; - public MemoryStream Psar; internal StreamUtil psarUtil; public abstract byte[] GenerateDataPsp(); diff --git a/PopsBuilder/Psp/NpUmdImg.cs b/PopsBuilder/Psp/NpUmdImg.cs index 343660e..7cb11de 100644 --- a/PopsBuilder/Psp/NpUmdImg.cs +++ b/PopsBuilder/Psp/NpUmdImg.cs @@ -18,7 +18,7 @@ namespace GameBuilder.Psp const int BLOCK_BASIS = 0x10; const int SECTOR_SZ = 2048; const int BLOCK_SZ = BLOCK_BASIS * SECTOR_SZ; - public NpUmdImg(NpDrmInfo drmInfo, UmdDisc umdImage, bool compress) : base(drmInfo) + public NpUmdImg(NpDrmInfo drmInfo, UmdInfo umdImage, bool compress) : base(drmInfo) { this.compress = compress; @@ -44,9 +44,9 @@ namespace GameBuilder.Psp private void patchSfo() { Sfo sfoKeys = Sfo.ReadSfo(umdImage.DataFiles["PARAM.SFO"]); - sfoKeys["DISC_ID"] = DrmInfo.ContentId.Substring(7, 9); + if ((sfoKeys["CATEGORY"] as String) == "UG") // "UMD Game" + sfoKeys["CATEGORY"] = "EG"; // set it to "Eboot Game" umdImage.DataFiles["PARAM.SFO"] = sfoKeys.WriteSfo(); - } private void createNpHdr() { @@ -74,7 +74,6 @@ namespace GameBuilder.Psp public void CreatePsar() { patchSfo(); - createNpUmdTbl(); byte[] tbl = encryptTable(); this.dataKey = hashBlock(tbl); @@ -103,8 +102,7 @@ namespace GameBuilder.Psp public override byte[] GenerateDataPsp() { - bool minis = false; // TODO: read minis flag from param.sfo - byte[] startDat = CreateStartDat(minis ? Resources.STARTDATMINIS : Resources.STARTDATPSP); + byte[] startDat = CreateStartDat(umdImage.Minis ? Resources.STARTDATMINIS : Resources.STARTDATPSP); using (MemoryStream dataPsp = new MemoryStream()) { StreamUtil dataPspUtil = new StreamUtil(dataPsp); @@ -169,7 +167,7 @@ namespace GameBuilder.Psp isoOffset += wsize; - Console.Write(Convert.ToInt32(Math.Floor((Convert.ToDouble(umdImage.IsoStream.Position) / Convert.ToDouble(umdImage.IsoStream.Length)) * 100.0)) + "%\r"); + UpdateProgress(Convert.ToInt32(umdImage.IsoStream.Position), Convert.ToInt32(umdImage.IsoStream.Length), "Compress & Encrypt UMD Image"); } } @@ -270,10 +268,19 @@ namespace GameBuilder.Psp return headerBytes; } + public override void Dispose() + { + npHdr.Dispose(); + npHdrBody.Dispose(); + isoData.Dispose(); + npTbl.Dispose(); + base.Dispose(); + } + private Int64 isoBlocks; private bool compress; - UmdDisc umdImage; + UmdInfo umdImage; private byte[] headerKey; private byte[] dataKey; diff --git a/PopsBuilder/Psp/PbpBuilder.cs b/PopsBuilder/Psp/PbpBuilder.cs index d13e501..1e9665b 100644 --- a/PopsBuilder/Psp/PbpBuilder.cs +++ b/PopsBuilder/Psp/PbpBuilder.cs @@ -1,4 +1,5 @@ -using System; +using GameBuilder.Progress; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -76,6 +77,5 @@ namespace GameBuilder.Psp } - } } diff --git a/PopsBuilder/Psp/Sfo.cs b/PopsBuilder/Psp/Sfo.cs index e99e853..b9e3ea0 100644 --- a/PopsBuilder/Psp/Sfo.cs +++ b/PopsBuilder/Psp/Sfo.cs @@ -34,25 +34,51 @@ namespace GameBuilder.Psp { get { - return sfoEntries[index].value; + if (sfoEntries.ContainsKey(index)) + return sfoEntries[index].value; + else + return null; } set { - SfoEntry sfoEnt = sfoEntries[index]; - sfoEnt.value = value; + if (sfoEntries.ContainsKey(index)) + { + SfoEntry sfoEnt = sfoEntries[index]; + sfoEnt.value = value; - // update sz - sfoEnt.valueSize = getObjectSz(sfoEnt.value); + // update sz + sfoEnt.valueSize = getObjectSz(sfoEnt.value); - if (sfoEnt.valueSize > sfoEnt.totalSize) - sfoEnt.totalSize = Convert.ToUInt32(MathUtil.CalculatePaddingAmount(Convert.ToInt32(sfoEnt.valueSize), sfoEnt.align)); - - // update type - sfoEnt.type = getPsfType(sfoEnt.value); + if (sfoEnt.valueSize > sfoEnt.totalSize) + sfoEnt.totalSize = Convert.ToUInt32(MathUtil.CalculatePaddingAmount(Convert.ToInt32(sfoEnt.valueSize), sfoEnt.align)); - sfoEntries[index] = sfoEnt; + // update type + sfoEnt.type = getPsfType(sfoEnt.value); + + sfoEntries[index] = sfoEnt; + } + else + { + UInt32 sz = getObjectSz(value); + int alg = MathUtil.CalculatePaddingAmount(Convert.ToInt32(sz), 4); + + AddKey(index, value, Convert.ToUInt32(sz + alg), 4); + } } } + + public void AddKey(string keyName, object value, UInt32 totalSize, byte align = 4) + { + SfoEntry ent = new SfoEntry(); + ent.keyName = keyName; + ent.type = getPsfType(value); + ent.valueSize = getObjectSz(value); + ent.totalSize = Convert.ToUInt32(totalSize + MathUtil.CalculatePaddingAmount(Convert.ToInt32(totalSize), align)); + ent.align = align; + ent.value = value; + sfoEntries[ent.keyName] = ent; + } + public Sfo() { sfoEntries = new Dictionary(); @@ -198,7 +224,7 @@ namespace GameBuilder.Psp break; case PSF_TYPE_VAL: - entry.value = DataUtils.ReadUint32At(valueLocation); + entry.value = DataUtils.ReadUInt32At(valueLocation); break; case PSF_TYPE_BIN: diff --git a/PopsBuilder/Psp/UmdDisc.cs b/PopsBuilder/Psp/UmdInfo.cs similarity index 86% rename from PopsBuilder/Psp/UmdDisc.cs rename to PopsBuilder/Psp/UmdInfo.cs index d5b1c0b..2962b0b 100644 --- a/PopsBuilder/Psp/UmdDisc.cs +++ b/PopsBuilder/Psp/UmdInfo.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace GameBuilder.Psp { - public class UmdDisc : IDisposable + public class UmdInfo : IDisposable { private string[] filesList = new string[] { @@ -21,7 +21,7 @@ namespace GameBuilder.Psp }; public Dictionary DataFiles = new Dictionary(); - public UmdDisc(string isoFile) + public UmdInfo(string isoFile) { this.IsoFile = isoFile; this.IsoStream = File.OpenRead(isoFile); @@ -54,12 +54,19 @@ namespace GameBuilder.Psp Sfo sfo = Sfo.ReadSfo(DataFiles["PARAM.SFO"]); this.DiscId = sfo["DISC_ID"] as String; + // check minis + if (sfo["ATTRIBUTE"] is UInt32) + this.Minis = ((UInt32)sfo["ATTRIBUTE"] & 0b00000001000000000000000000000000) != 0; + else + this.Minis = false; + IsoStream.Seek(0x00, SeekOrigin.Begin); } public string IsoFile; public FileStream IsoStream; + public bool Minis; public string DiscId; public string DiscIdSeperated { diff --git a/PopsBuilder/StreamUtil.cs b/PopsBuilder/StreamUtil.cs index 2d851f6..b6f7df2 100644 --- a/PopsBuilder/StreamUtil.cs +++ b/PopsBuilder/StreamUtil.cs @@ -14,7 +14,7 @@ namespace GameBuilder { this.s = s; } - public string ReadString() + public string ReadCStr() { using (MemoryStream ms = new MemoryStream()) { @@ -28,7 +28,7 @@ namespace GameBuilder return Encoding.UTF8.GetString(ms.ToArray()); } } - public UInt32 ReadUint32At(int location) + public UInt32 ReadUInt32At(int location) { long oldPos = s.Position; s.Seek(location, SeekOrigin.Begin); @@ -37,6 +37,15 @@ namespace GameBuilder return outp; } + public Int32 ReadInt32At(int location) + { + long oldPos = s.Position; + s.Seek(location, SeekOrigin.Begin); + Int32 outp = ReadInt32(); + s.Seek(oldPos, SeekOrigin.Begin); + return outp; + } + public byte[] ReadBytesAt(int location, int length) { long oldPos = s.Position; @@ -50,7 +59,7 @@ namespace GameBuilder { long oldPos = s.Position; s.Seek(location, SeekOrigin.Begin); - string outp = ReadString(); + string outp = ReadCStr(); s.Seek(oldPos, SeekOrigin.Begin); return outp; } diff --git a/PopsBuilder/VersionKey/ActRifMethod.cs b/PopsBuilder/VersionKey/ActRifMethod.cs new file mode 100644 index 0000000..5fa2077 --- /dev/null +++ b/PopsBuilder/VersionKey/ActRifMethod.cs @@ -0,0 +1,24 @@ +using GameBuilder.Psp; +using Org.BouncyCastle.Asn1.Pkcs; +using PspCrypto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GameBuilder.VersionKey +{ + public class ActRifMethod + { + public static NpDrmInfo GetVersionKey(byte[] actDat, byte[] licenseDat, byte[] consoleId, int keyType) + { + byte[] versionKey = new byte[0x10]; + SceNpDrm.SetPSID(consoleId); + SceNpDrm.sceNpDrmGetVersionKey(versionKey, actDat, licenseDat, keyType); + string contentId = Encoding.UTF8.GetString(licenseDat, 0x10, 0x24); + + return new NpDrmInfo(versionKey, contentId, keyType); + } + } +} diff --git a/PopsBuilder/VersionKey/EbootPbpMethod.cs b/PopsBuilder/VersionKey/EbootPbpMethod.cs new file mode 100644 index 0000000..07780ff --- /dev/null +++ b/PopsBuilder/VersionKey/EbootPbpMethod.cs @@ -0,0 +1,80 @@ +using GameBuilder.Psp; +using PspCrypto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace GameBuilder.VersionKey +{ + public class EbootPbpMethod + { + private static byte[] getKey(byte[] bbmac, byte[] headerBody) + { + byte[] versionKey = new byte[0x10]; + Span mkey = stackalloc byte[Marshal.SizeOf()]; + AMCTRL.sceDrmBBMacInit(mkey, 3); + AMCTRL.sceDrmBBMacUpdate(mkey, headerBody, headerBody.Length); + AMCTRL.bbmac_getkey(mkey, bbmac, versionKey); + return versionKey; + } + public static NpDrmInfo GetVersionKey(Stream ebootStream) + { + using (ebootStream) + { + StreamUtil ebootUtil = new StreamUtil(ebootStream); + ebootStream.Seek(0x1, SeekOrigin.Begin); + + if (ebootUtil.ReadCStr() != "PBP") + { + int dataPspLocation = ebootUtil.ReadInt32At(0x20); + int dataPsarLocation = ebootUtil.ReadInt32At(0x24); + ebootStream.Seek(dataPsarLocation, SeekOrigin.Begin); + + string magic = ebootUtil.ReadCStr(); + + switch (magic) + { + case "NPUMDIMG": + int keyType = ebootUtil.ReadInt32(); + string contentId = ebootUtil.ReadStringAt(dataPsarLocation + 0x10); + + byte[] npUmdHdr = ebootUtil.ReadBytesAt(dataPsarLocation, 0x100); + byte[] npUmdBody = ebootUtil.ReadBytesAt(dataPsarLocation + 0xC0, 0x10); + + byte[] versionkey = getKey(npUmdHdr, npUmdBody); + + return new NpDrmInfo(versionkey, contentId, keyType); + case "PSISOIMG0000": + using (DNASStream dnas = new DNASStream(ebootStream, dataPsarLocation + 0x400)) + { + contentId = ebootUtil.ReadStringAt(dataPspLocation + 0x560); + keyType = dnas.KeyIndex; + versionkey = dnas.VersionKey; + + return new NpDrmInfo(versionkey, contentId, keyType); + } + case "PSTITLEIMG000000": + using (DNASStream dnas = new DNASStream(ebootStream, dataPsarLocation + 0x200)) + { + contentId = ebootUtil.ReadStringAt(dataPspLocation + 0x560); + keyType = dnas.KeyIndex; + versionkey = dnas.VersionKey; + + return new NpDrmInfo(versionkey, contentId, keyType); + } + default: + throw new Exception("Cannot obtain versionkey from this EBOOT.PBP"); + } + + } + else + { + throw new Exception("Invalid PBP"); + } + } + } + } +} diff --git a/PspCrypto/Lz.cs b/PspCrypto/Lz.cs index f974881..5e7661e 100644 --- a/PspCrypto/Lz.cs +++ b/PspCrypto/Lz.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Drawing; using System.IO; using System.Text; -using SevenZip.Compression.LZMA; -using Decoder = SevenZip.Compression.LZMA.Decoder; namespace PspCrypto { diff --git a/PspCrypto/PspCrypto.csproj b/PspCrypto/PspCrypto.csproj index ad7e34a..20f3b98 100644 --- a/PspCrypto/PspCrypto.csproj +++ b/PspCrypto/PspCrypto.csproj @@ -8,7 +8,6 @@ - diff --git a/PspTest.sln b/PspTest.sln index 560a122..9ddbdc5 100644 --- a/PspTest.sln +++ b/PspTest.sln @@ -10,6 +10,16 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsvImage", "PsvImage\PsvImage.csproj", "{0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameBuilder", "PopsBuilder\GameBuilder.csproj", "{D1DF66DB-52BE-489C-A292-525BC71F2BB2}" + ProjectSection(ProjectDependencies) = postProject + {BF155D74-2923-432F-8566-26D83B15EEE8} = {BF155D74-2923-432F-8566-26D83B15EEE8} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChovySign-CLI", "ChovySign-CLI\ChovySign-CLI.csproj", "{90FB2C00-CC46-4B77-8E24-C5F7458AB068}" + ProjectSection(ProjectDependencies) = postProject + {A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE} = {A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscUtils", "DiscUtils\DiscUtils.csproj", "{BF155D74-2923-432F-8566-26D83B15EEE8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -33,6 +43,14 @@ Global {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Release|Any CPU.ActiveCfg = Release|Any CPU {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Release|Any CPU.Build.0 = Release|Any CPU + {90FB2C00-CC46-4B77-8E24-C5F7458AB068}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90FB2C00-CC46-4B77-8E24-C5F7458AB068}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90FB2C00-CC46-4B77-8E24-C5F7458AB068}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90FB2C00-CC46-4B77-8E24-C5F7458AB068}.Release|Any CPU.Build.0 = Release|Any CPU + {BF155D74-2923-432F-8566-26D83B15EEE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF155D74-2923-432F-8566-26D83B15EEE8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF155D74-2923-432F-8566-26D83B15EEE8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF155D74-2923-432F-8566-26D83B15EEE8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From fc5e6835ed3e1e4a5763eaf66ed65f2449546ce9 Mon Sep 17 00:00:00 2001 From: Li Date: Mon, 17 Apr 2023 08:25:45 +1200 Subject: [PATCH 09/31] Fix startdat not appearing --- ChovySign-CLI/ChovySign-CLI.csproj.user | 6 ++++++ .../PublishProfiles/FolderProfile.pubxml | 19 +++++++++++++++++++ .../PublishProfiles/FolderProfile.pubxml.user | 10 ++++++++++ PopsBuilder/Pops/DiscInfo.cs | 1 + PopsBuilder/Pops/PopsImg.cs | 6 +++--- PopsBuilder/Pops/PsIsoImg.cs | 3 --- 6 files changed, 39 insertions(+), 6 deletions(-) create mode 100644 ChovySign-CLI/ChovySign-CLI.csproj.user create mode 100644 ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user diff --git a/ChovySign-CLI/ChovySign-CLI.csproj.user b/ChovySign-CLI/ChovySign-CLI.csproj.user new file mode 100644 index 0000000..ea87748 --- /dev/null +++ b/ChovySign-CLI/ChovySign-CLI.csproj.user @@ -0,0 +1,6 @@ + + + + <_LastSelectedProfileId>C:\Users\Li\Documents\git\Chovy-Sign-v2\ChovySign-CLI\Properties\PublishProfiles\FolderProfile.pubxml + + \ No newline at end of file diff --git a/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 0000000..d1d5c25 --- /dev/null +++ b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,19 @@ + + + + + Release + Any CPU + bin\Release\ + FileSystem + <_TargetId>Folder + net6.0 + win-x64 + true + true + true + true + + \ No newline at end of file diff --git a/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user new file mode 100644 index 0000000..675644e --- /dev/null +++ b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -0,0 +1,10 @@ + + + + + True|2023-04-16T20:22:02.0531219Z; + + + \ No newline at end of file diff --git a/PopsBuilder/Pops/DiscInfo.cs b/PopsBuilder/Pops/DiscInfo.cs index ff0da81..84b601f 100644 --- a/PopsBuilder/Pops/DiscInfo.cs +++ b/PopsBuilder/Pops/DiscInfo.cs @@ -59,6 +59,7 @@ namespace GameBuilder.Pops { using (SparseStream systemCnfStream = cdReader.OpenFile("SYSTEM.CNF", FileMode.Open)) { + systemCnfStream.Seek(0x18, SeekOrigin.Begin); using (StreamReader systemCnfReader = new StreamReader(systemCnfStream)) { for (string? line = systemCnfReader.ReadLine(); line is not null; line = systemCnfReader.ReadLine()) diff --git a/PopsBuilder/Pops/PopsImg.cs b/PopsBuilder/Pops/PopsImg.cs index 219dc93..680f84b 100644 --- a/PopsBuilder/Pops/PopsImg.cs +++ b/PopsBuilder/Pops/PopsImg.cs @@ -18,9 +18,9 @@ namespace GameBuilder.Pops simple = new MemoryStream(); simpleUtil = new StreamUtil(simple); - StartDat = NpDrmPsar.CreateStartDat(Resources.STARTDATPOPS); - createSimpleDat(); - SimplePgd = generateSimplePgd(); + this.StartDat = NpDrmPsar.CreateStartDat(Resources.STARTDATPOPS); + this.createSimpleDat(); + this.SimplePgd = generateSimplePgd(); } diff --git a/PopsBuilder/Pops/PsIsoImg.cs b/PopsBuilder/Pops/PsIsoImg.cs index 742d1a6..740a3c7 100644 --- a/PopsBuilder/Pops/PsIsoImg.cs +++ b/PopsBuilder/Pops/PsIsoImg.cs @@ -46,7 +46,6 @@ namespace GameBuilder.Pops compressor.CompressedIso.Seek(0x00, SeekOrigin.Begin); compressor.CompressedIso.CopyTo(Psar); - Psar.Seek(0x00, SeekOrigin.Begin); if (isPartOfMultiDisc) return; // write STARTDAT @@ -59,8 +58,6 @@ namespace GameBuilder.Pops // set STARTDAT location Psar.Seek(0xC, SeekOrigin.Begin); psarUtil.WriteInt64(startDatLocation); - - Psar.Seek(0x00, SeekOrigin.Begin); } From 9e00f65cb93b960694f8f9146a6da7de2be4f824 Mon Sep 17 00:00:00 2001 From: Li Date: Mon, 17 Apr 2023 08:51:24 +1200 Subject: [PATCH 10/31] Fix stuff --- ChovySign-CLI/Program.cs | 8 ++++---- .../Properties/PublishProfiles/FolderProfile.pubxml.user | 2 +- PopsBuilder/Pops/DiscCompressor.cs | 2 +- PopsBuilder/Pops/PopsImg.cs | 2 +- PopsBuilder/Pops/PsTitleImg.cs | 2 +- PopsBuilder/Psp/NpDrmInfo.cs | 4 ++-- PopsBuilder/Psp/NpUmdImg.cs | 8 ++++---- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ChovySign-CLI/Program.cs b/ChovySign-CLI/Program.cs index e766822..58f4994 100644 --- a/ChovySign-CLI/Program.cs +++ b/ChovySign-CLI/Program.cs @@ -180,11 +180,11 @@ namespace ChovySign_CLI if (drmInfo is null) return Error("no versionkey was found, exiting", 6); if (pbpMode is null) return Error("no pbp mode was set, exiting", 7); - if (pbpMode == PbpMode.PSP && drmInfo.KeyType != 2) - return Error("KeyType is "+drmInfo.KeyType+", but PBP mode is PSP, you cant do that .. please use a type 1 versionkey.", 8); + if (pbpMode == PbpMode.PSP && drmInfo.KeyIndex != 2) + return Error("KeyType is "+drmInfo.KeyIndex+", but PBP mode is PSP, you cant do that .. please use a type 1 versionkey.", 8); - if (pbpMode == PbpMode.POPS && drmInfo.KeyType != 1) - return Error("KeyType is " + drmInfo.KeyType + ", but PBP mode is POPS, you cant do that .. please use a type 1 versionkey.", 8); + if (pbpMode == PbpMode.POPS && drmInfo.KeyIndex != 1) + return Error("KeyType is " + drmInfo.KeyIndex + ", but PBP mode is POPS, you cant do that .. please use a type 1 versionkey.", 8); if (pbpMode == PbpMode.POPS && (popsDiscName is null || popsIcon0File is null)) return Error("pbp mode is POPS, but you have not specified a disc title or icon file using --pops-info.", 9); diff --git a/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user index 675644e..2b95831 100644 --- a/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - True|2023-04-16T20:22:02.0531219Z; + True|2023-04-16T20:27:16.5281469Z;True|2023-04-17T08:22:02.0531219+12:00; \ No newline at end of file diff --git a/PopsBuilder/Pops/DiscCompressor.cs b/PopsBuilder/Pops/DiscCompressor.cs index 061713b..0537a4b 100644 --- a/PopsBuilder/Pops/DiscCompressor.cs +++ b/PopsBuilder/Pops/DiscCompressor.cs @@ -76,7 +76,7 @@ namespace GameBuilder.Pops int headerSize = DNASHelper.CalculateSize(isoHdr.Length, 0x400); byte[] headerEnc = new byte[headerSize]; - int sz = DNASHelper.Encrypt(headerEnc, isoHdr, srcImg.DrmInfo.VersionKey, isoHdr.Length, srcImg.DrmInfo.KeyType, 1, blockSize: 0x400); + int sz = DNASHelper.Encrypt(headerEnc, isoHdr, srcImg.DrmInfo.VersionKey, isoHdr.Length, srcImg.DrmInfo.KeyIndex, 1, blockSize: 0x400); byte[] isoHdrPgd = headerEnc.ToArray(); Array.Resize(ref isoHdrPgd, sz); diff --git a/PopsBuilder/Pops/PopsImg.cs b/PopsBuilder/Pops/PopsImg.cs index 680f84b..6d9d1b6 100644 --- a/PopsBuilder/Pops/PopsImg.cs +++ b/PopsBuilder/Pops/PopsImg.cs @@ -51,7 +51,7 @@ namespace GameBuilder.Pops byte[] simpleEnc = new byte[simpleSz]; // get pgd - int sz = DNASHelper.Encrypt(simpleEnc, simpleData, DrmInfo.VersionKey, simpleData.Length, DrmInfo.KeyType, 1, blockSize: 0x400); + int sz = DNASHelper.Encrypt(simpleEnc, simpleData, DrmInfo.VersionKey, simpleData.Length, DrmInfo.KeyIndex, 1, blockSize: 0x400); byte[] pgd = simpleEnc.ToArray(); Array.Resize(ref pgd, sz); diff --git a/PopsBuilder/Pops/PsTitleImg.cs b/PopsBuilder/Pops/PsTitleImg.cs index 7327c08..0e4c3e6 100644 --- a/PopsBuilder/Pops/PsTitleImg.cs +++ b/PopsBuilder/Pops/PsTitleImg.cs @@ -102,7 +102,7 @@ namespace GameBuilder.Pops int encryptedSz = DNASHelper.CalculateSize(isoMapBuf.Length, 1024); var isoMapEnc = new byte[encryptedSz]; - DNASHelper.Encrypt(isoMapEnc, isoMapBuf, DrmInfo.VersionKey, isoMapBuf.Length, DrmInfo.KeyType, 1); + DNASHelper.Encrypt(isoMapEnc, isoMapBuf, DrmInfo.VersionKey, isoMapBuf.Length, DrmInfo.KeyIndex, 1); return isoMapEnc; } diff --git a/PopsBuilder/Psp/NpDrmInfo.cs b/PopsBuilder/Psp/NpDrmInfo.cs index 655464d..e411200 100644 --- a/PopsBuilder/Psp/NpDrmInfo.cs +++ b/PopsBuilder/Psp/NpDrmInfo.cs @@ -10,12 +10,12 @@ namespace GameBuilder.Psp { public string ContentId; public byte[] VersionKey; - public int KeyType; + public int KeyIndex; public NpDrmInfo(byte[] versionKey, string contentId, int keyType) { this.VersionKey = versionKey; - this.KeyType = keyType; + this.KeyIndex = keyType; this.ContentId = contentId; } } diff --git a/PopsBuilder/Psp/NpUmdImg.cs b/PopsBuilder/Psp/NpUmdImg.cs index 7cb11de..edd6de2 100644 --- a/PopsBuilder/Psp/NpUmdImg.cs +++ b/PopsBuilder/Psp/NpUmdImg.cs @@ -51,7 +51,7 @@ namespace GameBuilder.Psp private void createNpHdr() { npHdrUtil.WriteStr("NPUMDIMG"); - npHdrUtil.WriteInt32(DrmInfo.KeyType); + npHdrUtil.WriteInt32(DrmInfo.KeyIndex); npHdrUtil.WriteInt32(BLOCK_BASIS); npHdrUtil.WriteStrWithPadding(DrmInfo.ContentId, 0x00, 0x30); @@ -110,7 +110,7 @@ namespace GameBuilder.Psp dataPspUtil.WriteBytes(signature); dataPspUtil.WritePadding(0x00, 0x530); dataPspUtil.WriteStrWithPadding(DrmInfo.ContentId, 0x00, 0x30); - dataPspUtil.WriteInt32BE(DrmInfo.KeyType); + dataPspUtil.WriteInt32BE(DrmInfo.KeyIndex); dataPspUtil.WriteInt32(0); dataPspUtil.WriteInt32(0); dataPspUtil.WriteInt32(0); @@ -198,7 +198,7 @@ namespace GameBuilder.Psp private byte[] encryptBlock(byte[] blockData, int offset) { AMCTRL.CIPHER_KEY ckey = new AMCTRL.CIPHER_KEY(); - AMCTRL.sceDrmBBCipherInit(out ckey, 1, DrmInfo.KeyType, headerKey, DrmInfo.VersionKey, offset >> 4); + AMCTRL.sceDrmBBCipherInit(out ckey, 1, DrmInfo.KeyIndex, headerKey, DrmInfo.VersionKey, offset >> 4); AMCTRL.sceDrmBBCipherUpdate(ref ckey, blockData, blockData.Length); AMCTRL.sceDrmBBCipherFinal(ref ckey); return blockData; @@ -262,7 +262,7 @@ namespace GameBuilder.Psp private byte[] encryptHeader(byte[] headerBytes) { AMCTRL.CIPHER_KEY ckey = new AMCTRL.CIPHER_KEY(); - AMCTRL.sceDrmBBCipherInit(out ckey, 1, DrmInfo.KeyType, headerKey, DrmInfo.VersionKey, 0); + AMCTRL.sceDrmBBCipherInit(out ckey, 1, DrmInfo.KeyIndex, headerKey, DrmInfo.VersionKey, 0); AMCTRL.sceDrmBBCipherUpdate(ref ckey, headerBytes, headerBytes.Length); AMCTRL.sceDrmBBCipherFinal(ref ckey); return headerBytes; From 8000c946bd393a44c169a67059bf85d7a375a9bd Mon Sep 17 00:00:00 2001 From: Li Date: Mon, 17 Apr 2023 10:14:13 +1200 Subject: [PATCH 11/31] Fix multidisc --- ChovySign-CLI/Program.cs | 28 +++++++++++++++---- .../PublishProfiles/FolderProfile.pubxml.user | 2 +- PopsBuilder/Pops/PsTitleImg.cs | 9 +++--- PopsBuilder/Psp/PbpBuilder.cs | 4 +-- PopsBuilder/Psp/UmdInfo.cs | 2 +- PopsBuilder/StreamUtil.cs | 4 +++ PopsBuilder/VersionKey/ActRifMethod.cs | 4 ++- PopsBuilder/VersionKey/EbootPbpMethod.cs | 8 +++--- 8 files changed, 41 insertions(+), 20 deletions(-) diff --git a/ChovySign-CLI/Program.cs b/ChovySign-CLI/Program.cs index 58f4994..cecdf8e 100644 --- a/ChovySign-CLI/Program.cs +++ b/ChovySign-CLI/Program.cs @@ -2,6 +2,7 @@ using GameBuilder.Progress; using GameBuilder.Psp; using GameBuilder.VersionKey; +using PspCrypto; namespace ChovySign_CLI { @@ -178,14 +179,17 @@ namespace ChovySign_CLI if(res != 0) return res; if (drmInfo is null) return Error("no versionkey was found, exiting", 6); - if (pbpMode is null) return Error("no pbp mode was set, exiting", 7); + Console.WriteLine("Version Key: " + BitConverter.ToString(drmInfo.VersionKey).Replace("-", "")); + + if (pbpMode is null) return Error("no pbp mode was set, exiting", 7); + if (pbpMode == PbpMode.PSP && drmInfo.KeyIndex != 2) return Error("KeyType is "+drmInfo.KeyIndex+", but PBP mode is PSP, you cant do that .. please use a type 1 versionkey.", 8); if (pbpMode == PbpMode.POPS && drmInfo.KeyIndex != 1) return Error("KeyType is " + drmInfo.KeyIndex + ", but PBP mode is POPS, you cant do that .. please use a type 1 versionkey.", 8); - + if (pbpMode == PbpMode.POPS && (popsDiscName is null || popsIcon0File is null)) return Error("pbp mode is POPS, but you have not specified a disc title or icon file using --pops-info.", 9); if (pbpMode == PbpMode.POPS) @@ -223,14 +227,18 @@ namespace ChovySign_CLI psIsoImg, "EBOOT.PBP", 0); + + byte[] ebootsig = new byte[0x200]; + SceNpDrm.KsceNpDrmEbootSigGenPs1("EBOOT.PBP", ebootsig, 0x3600000); + File.WriteAllBytes("__sce_ebootpbp", ebootsig.ToArray()); } } else { - using (PsTitleImg psIsoImg = new PsTitleImg(drmInfo, discInfs)) + using (PsTitleImg psTitleImg = new PsTitleImg(drmInfo, discInfs)) { - psIsoImg.RegisterCallback(onProgress); - psIsoImg.CreatePsar(); + psTitleImg.RegisterCallback(onProgress); + psTitleImg.CreatePsar(); PbpBuilder.CreatePbp(psfo.WriteSfo(), File.ReadAllBytes(popsIcon0File), @@ -238,9 +246,13 @@ namespace ChovySign_CLI (popsPic0File is not null) ? File.ReadAllBytes(popsPic0File) : null, Resources.PIC1, null, - psIsoImg, + psTitleImg, "EBOOT.PBP", 0); + + byte[] ebootsig = new byte[0x200]; + SceNpDrm.KsceNpDrmEbootSigGenPs1("EBOOT.PBP", ebootsig, 0x3600000); + File.WriteAllBytes("__sce_ebootpbp", ebootsig.ToArray()); } } } @@ -263,6 +275,10 @@ namespace ChovySign_CLI "EBOOT.PBP", 1); + byte[] ebootsig = new byte[0x200]; + SceNpDrm.KsceNpDrmEbootSigGenPsp("EBOOT.PBP", ebootsig, 0x3600000); + File.WriteAllBytes("__sce_ebootpbp", ebootsig.ToArray()); + } } } diff --git a/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user index 2b95831..0d6a3e7 100644 --- a/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - True|2023-04-16T20:27:16.5281469Z;True|2023-04-17T08:22:02.0531219+12:00; + True|2023-04-16T21:56:35.5065135Z;True|2023-04-17T09:22:54.8607008+12:00;True|2023-04-17T08:27:16.5281469+12:00;True|2023-04-17T08:22:02.0531219+12:00; \ No newline at end of file diff --git a/PopsBuilder/Pops/PsTitleImg.cs b/PopsBuilder/Pops/PsTitleImg.cs index 0e4c3e6..97e7b9f 100644 --- a/PopsBuilder/Pops/PsTitleImg.cs +++ b/PopsBuilder/Pops/PsTitleImg.cs @@ -57,8 +57,8 @@ namespace GameBuilder.Pops createIsoMap(); psarUtil.WriteStr("PSTITLEIMG000000"); - psarUtil.WriteInt64(PSISO_ALIGN+isoPart.Length); // location of STARTDAT - + psarUtil.WriteInt64(PSISO_ALIGN + isoPart.Length); // location of STARTDAT + psarUtil.WriteBytes(Rng.RandomBytes(0x10)); // dunno what this is psarUtil.WritePadding(0x00, 0x1D8); @@ -70,7 +70,6 @@ namespace GameBuilder.Pops isoPart.CopyTo(Psar); psarUtil.WriteBytes(StartDat); - psarUtil.WriteBytes(SimplePgd); } @@ -115,7 +114,7 @@ namespace GameBuilder.Pops discNumber++; if (compressors[i] is null) { isoMapUtil.WriteInt32(0); continue; }; - int padLen = Convert.ToInt32(PSISO_ALIGN - (isoPart.Position % PSISO_ALIGN)); + int padLen = MathUtil.CalculatePaddingAmount(Convert.ToInt32(isoPart.Position), PSISO_ALIGN); isoPartUtil.WritePadding(0x00, padLen); using (PsIsoImg psIsoImg = new PsIsoImg(this.DrmInfo, compressors[i])) @@ -144,7 +143,7 @@ namespace GameBuilder.Pops isoMapUtil.WriteStrWithPadding(discs.First().DiscIdHdr, 0x00, 0x20); isoMapUtil.WriteInt64(Convert.ToInt64(PSISO_ALIGN + isoPart.Length + StartDat.Length)); - psarUtil.WriteBytes(Rng.RandomBytes(0x80)); + isoMapUtil.WriteBytes(Rng.RandomBytes(0x80)); isoMapUtil.WriteStrWithPadding(discs.First().DiscName, 0x00, 0x80); isoMapUtil.WriteInt32(MAX_DISCS); isoMapUtil.WritePadding(0x00, 0x70); diff --git a/PopsBuilder/Psp/PbpBuilder.cs b/PopsBuilder/Psp/PbpBuilder.cs index 1e9665b..44c0bab 100644 --- a/PopsBuilder/Psp/PbpBuilder.cs +++ b/PopsBuilder/Psp/PbpBuilder.cs @@ -19,8 +19,8 @@ namespace GameBuilder.Psp byte[] dataPsp = dataPsar.GenerateDataPsp(); int padLen = MathUtil.CalculatePaddingAmount(dataPsp.Length, 0x100); - - Array.Resize(ref dataPsp, dataPsp.Length + padLen); + if(version == 1) + Array.Resize(ref dataPsp, dataPsp.Length + padLen); StreamUtil pbpUtil = new StreamUtil(pbpStream); pbpUtil.WriteByte(0x00); diff --git a/PopsBuilder/Psp/UmdInfo.cs b/PopsBuilder/Psp/UmdInfo.cs index 2962b0b..012a2da 100644 --- a/PopsBuilder/Psp/UmdInfo.cs +++ b/PopsBuilder/Psp/UmdInfo.cs @@ -25,7 +25,7 @@ namespace GameBuilder.Psp { this.IsoFile = isoFile; this.IsoStream = File.OpenRead(isoFile); - using (CDReader cdReader = new CDReader(this.IsoStream, true, true)) + using (CDReader cdReader = new CDReader(this.IsoStream, true, true, 2048)) { foreach (string file in filesList) { diff --git a/PopsBuilder/StreamUtil.cs b/PopsBuilder/StreamUtil.cs index b6f7df2..5faeec3 100644 --- a/PopsBuilder/StreamUtil.cs +++ b/PopsBuilder/StreamUtil.cs @@ -14,6 +14,10 @@ namespace GameBuilder { this.s = s; } + public string ReadStrLen(int len) + { + return Encoding.UTF8.GetString(ReadBytes(len)); + } public string ReadCStr() { using (MemoryStream ms = new MemoryStream()) diff --git a/PopsBuilder/VersionKey/ActRifMethod.cs b/PopsBuilder/VersionKey/ActRifMethod.cs index 5fa2077..24161a9 100644 --- a/PopsBuilder/VersionKey/ActRifMethod.cs +++ b/PopsBuilder/VersionKey/ActRifMethod.cs @@ -16,8 +16,10 @@ namespace GameBuilder.VersionKey byte[] versionKey = new byte[0x10]; SceNpDrm.SetPSID(consoleId); SceNpDrm.sceNpDrmGetVersionKey(versionKey, actDat, licenseDat, keyType); - string contentId = Encoding.UTF8.GetString(licenseDat, 0x10, 0x24); + SceNpDrm.Aid = BitConverter.ToUInt64(licenseDat, 0x8); + string contentId = Encoding.UTF8.GetString(licenseDat, 0x10, 0x24); + return new NpDrmInfo(versionKey, contentId, keyType); } } diff --git a/PopsBuilder/VersionKey/EbootPbpMethod.cs b/PopsBuilder/VersionKey/EbootPbpMethod.cs index 07780ff..6b6477d 100644 --- a/PopsBuilder/VersionKey/EbootPbpMethod.cs +++ b/PopsBuilder/VersionKey/EbootPbpMethod.cs @@ -33,7 +33,7 @@ namespace GameBuilder.VersionKey int dataPsarLocation = ebootUtil.ReadInt32At(0x24); ebootStream.Seek(dataPsarLocation, SeekOrigin.Begin); - string magic = ebootUtil.ReadCStr(); + string magic = ebootUtil.ReadStrLen(8); switch (magic) { @@ -47,7 +47,7 @@ namespace GameBuilder.VersionKey byte[] versionkey = getKey(npUmdHdr, npUmdBody); return new NpDrmInfo(versionkey, contentId, keyType); - case "PSISOIMG0000": + case "PSISOIMG": using (DNASStream dnas = new DNASStream(ebootStream, dataPsarLocation + 0x400)) { contentId = ebootUtil.ReadStringAt(dataPspLocation + 0x560); @@ -56,7 +56,7 @@ namespace GameBuilder.VersionKey return new NpDrmInfo(versionkey, contentId, keyType); } - case "PSTITLEIMG000000": + case "PSTITLEI": using (DNASStream dnas = new DNASStream(ebootStream, dataPsarLocation + 0x200)) { contentId = ebootUtil.ReadStringAt(dataPspLocation + 0x560); @@ -66,7 +66,7 @@ namespace GameBuilder.VersionKey return new NpDrmInfo(versionkey, contentId, keyType); } default: - throw new Exception("Cannot obtain versionkey from this EBOOT.PBP"); + throw new Exception("Cannot obtain versionkey from this EBOOT.PBP (magic:" + magic + ")"); } } From ab38aa8ed434dc7d813564493b9c4e0ab9fe9b71 Mon Sep 17 00:00:00 2001 From: Li Date: Tue, 18 Apr 2023 00:28:19 +1200 Subject: [PATCH 12/31] add stuff --- .../PublishProfiles/FolderProfile.pubxml.user | 2 +- .../{Core => }/ApplePartitionMap/BlockZero.cs | 0 .../ApplePartitionMap/PartitionMap.cs | 0 .../ApplePartitionMap/PartitionMapEntry.cs | 0 .../ApplePartitionMap/PartitionMapFactory.cs | 0 DiscUtils/{Core => }/Archives/FileRecord.cs | 0 DiscUtils/{Core => }/Archives/TarFile.cs | 0 .../{Core => }/Archives/TarFileBuilder.cs | 0 DiscUtils/{Core => }/Archives/TarHeader.cs | 0 .../{Core => }/Archives/TarHeaderExtent.cs | 0 .../Archives/UnixBuildFileRecord.cs | 0 DiscUtils/{Core => }/ChsAddress.cs | 0 DiscUtils/{Core => }/ClusterMap.cs | 0 DiscUtils/{Core => }/ClusterRoles.cs | 0 DiscUtils/{Core => }/Compression/Adler32.cs | 0 .../Compression/BZip2BlockDecoder.cs | 0 .../Compression/BZip2CombinedHuffmanTrees.cs | 0 .../Compression/BZip2DecoderStream.cs | 0 .../{Core => }/Compression/BZip2Randomizer.cs | 0 .../{Core => }/Compression/BZip2RleStream.cs | 0 .../Compression/BigEndianBitStream.cs | 0 DiscUtils/{Core => }/Compression/BitStream.cs | 0 .../{Core => }/Compression/BlockCompressor.cs | 0 .../Compression/CompressionResult.cs | 0 .../Compression/DataBlockTransform.cs | 0 .../{Core => }/Compression/HuffmanTree.cs | 0 .../Compression/InverseBurrowsWheeler.cs | 0 .../{Core => }/Compression/MoveToFront.cs | 0 .../Compression/SizedDeflateStream.cs | 0 .../{Core => }/Compression/ZlibBuffer.cs | 0 .../{Core => }/Compression/ZlibStream.cs | 0 DiscUtils/Core/DiscUtils.Core.csproj | 15 - .../{Core => }/CoreCompat/EncodingHelper.cs | 0 .../{Core => }/CoreCompat/ReflectionHelper.cs | 0 .../{Core => }/CoreCompat/StringExtensions.cs | 0 DiscUtils/{Core => }/DiscDirectoryInfo.cs | 0 DiscUtils/{Core => }/DiscFileInfo.cs | 0 DiscUtils/{Core => }/DiscFileLocator.cs | 0 DiscUtils/{Core => }/DiscFileSystem.cs | 0 DiscUtils/{Core => }/DiscFileSystemChecker.cs | 0 DiscUtils/{Core => }/DiscFileSystemInfo.cs | 0 DiscUtils/{Core => }/DiscFileSystemOptions.cs | 0 DiscUtils/{Core => }/DiskImageBuilder.cs | 0 .../{Core => }/DiskImageFileSpecification.cs | 0 DiscUtils/{Core => }/FileLocator.cs | 0 DiscUtils/{Core => }/FileSystemInfo.cs | 0 DiscUtils/{Core => }/FileSystemManager.cs | 0 DiscUtils/{Core => }/FileSystemParameters.cs | 0 DiscUtils/{Core => }/FileTransport.cs | 0 DiscUtils/{Core => }/FloppyDiskType.cs | 0 .../{Core => }/GenericDiskAdapterType.cs | 0 DiscUtils/{Core => }/Geometry.cs | 0 DiscUtils/{Core => }/GeometryCalculation.cs | 0 DiscUtils/{Core => }/GeometryTranslation.cs | 0 .../{Core => }/IClusterBasedFileSystem.cs | 0 DiscUtils/{Core => }/IDiagnosticTraceable.cs | 0 DiscUtils/{Core => }/IFileSystem.cs | 0 DiscUtils/{Core => }/IUnixFileSystem.cs | 0 DiscUtils/{Core => }/IWindowsFileSystem.cs | 0 DiscUtils/{Core => }/Internal/Crc32.cs | 0 .../{Core => }/Internal/Crc32Algorithm.cs | 0 .../{Core => }/Internal/Crc32BigEndian.cs | 0 .../{Core => }/Internal/Crc32LittleEndian.cs | 0 .../{Core => }/Internal/LocalFileLocator.cs | 0 .../Internal/LogicalVolumeFactory.cs | 0 .../Internal/LogicalVolumeFactoryAttribute.cs | 0 DiscUtils/{Core => }/Internal/ObjectCache.cs | 0 DiscUtils/{Core => }/Internal/Utilities.cs | 0 .../{Core => }/Internal/VirtualDiskFactory.cs | 0 .../Internal/VirtualDiskFactoryAttribute.cs | 0 .../Internal/VirtualDiskTransport.cs | 0 .../Internal/VirtualDiskTransportAttribute.cs | 0 .../{Core => }/InvalidFileSystemException.cs | 0 DiscUtils/Iso9660/BaseVolumeDescriptor.cs | 6 +- DiscUtils/Iso9660/BootVolumeDescriptor.cs | 4 +- .../Iso9660/BootVolumeDescriptorRegion.cs | 6 +- DiscUtils/Iso9660/BuildDirectoryInfo.cs | 24 +- DiscUtils/Iso9660/BuildParameters.cs | 3 - DiscUtils/Iso9660/CDBuilder.cs | 100 ++-- DiscUtils/Iso9660/CDReader.cs | 36 +- DiscUtils/Iso9660/CommonVolumeDescriptor.cs | 7 +- DiscUtils/Iso9660/DiscUtils.Iso9660.csproj | 14 - DiscUtils/Iso9660/ExtentStream.cs | 6 +- DiscUtils/Iso9660/File.cs | 2 +- DiscUtils/Iso9660/IsoContext.cs | 7 - DiscUtils/Iso9660/IsoUtilities.cs | 2 +- DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs | 5 +- .../Iso9660/PrimaryVolumeDescriptorRegion.cs | 6 +- DiscUtils/Iso9660/ReaderDirectory.cs | 6 +- .../Iso9660/SupplementaryVolumeDescriptor.cs | 5 +- .../SupplementaryVolumeDescriptorRegion.cs | 6 +- DiscUtils/Iso9660/VfsCDReader.cs | 55 +- .../Iso9660/VolumeDescriptorDiskRegion.cs | 4 +- .../Iso9660/VolumeDescriptorSetTerminator.cs | 4 +- .../VolumeDescriptorSetTerminatorRegion.cs | 6 +- DiscUtils/Iso9660Ps1/BaseVolumeDescriptor.cs | 60 ++ DiscUtils/Iso9660Ps1/BootDeviceEmulation.cs | 55 ++ DiscUtils/Iso9660Ps1/BootInitialEntry.cs | 60 ++ DiscUtils/Iso9660Ps1/BootValidationEntry.cs | 88 +++ DiscUtils/Iso9660Ps1/BootVolumeDescriptor.cs | 56 ++ .../Iso9660Ps1/BootVolumeDescriptorRegion.cs | 42 ++ DiscUtils/Iso9660Ps1/BuildDirectoryInfo.cs | 219 ++++++++ DiscUtils/Iso9660Ps1/BuildDirectoryMember.cs | 155 +++++ DiscUtils/Iso9660Ps1/BuildFileInfo.cs | 136 +++++ DiscUtils/Iso9660Ps1/BuildParameters.cs | 40 ++ DiscUtils/Iso9660Ps1/CDBuilder.cs | 518 +++++++++++++++++ DiscUtils/Iso9660Ps1/CDReader.cs | 229 ++++++++ .../Iso9660Ps1/CommonVolumeDescriptor.cs | 141 +++++ DiscUtils/Iso9660Ps1/DirectoryExtent.cs | 71 +++ DiscUtils/Iso9660Ps1/DirectoryRecord.cs | 114 ++++ DiscUtils/Iso9660Ps1/ExtentStream.cs | 125 +++++ DiscUtils/Iso9660Ps1/File.cs | 119 ++++ DiscUtils/Iso9660Ps1/FileExtent.cs | 85 +++ DiscUtils/Iso9660Ps1/FileFlags.cs | 16 + DiscUtils/Iso9660Ps1/Iso9660Variant.cs | 60 ++ DiscUtils/Iso9660Ps1/IsoContext.cs | 49 ++ DiscUtils/Iso9660Ps1/IsoUtilities.cs | 467 +++++++++++++++ DiscUtils/Iso9660Ps1/PathTable.cs | 100 ++++ DiscUtils/Iso9660Ps1/PathTableRecord.cs | 71 +++ .../Iso9660Ps1/PrimaryVolumeDescriptor.cs | 76 +++ .../PrimaryVolumeDescriptorRegion.cs | 42 ++ DiscUtils/Iso9660Ps1/ReaderDirEntry.cs | 195 +++++++ DiscUtils/Iso9660Ps1/ReaderDirectory.cs | 130 +++++ .../RockRidge/ChildLinkSystemUseEntry.cs | 36 ++ .../RockRidge/FileTimeSystemUseEntry.cs | 94 ++++ .../RockRidge/PosixFileInfoSystemUseEntry.cs | 48 ++ .../RockRidge/PosixNameSystemUseEntry.cs | 40 ++ .../RockRidge/RockRidgeExtension.cs | 57 ++ .../SupplementaryVolumeDescriptor.cs | 80 +++ .../SupplementaryVolumeDescriptorRegion.cs | 42 ++ .../Susp/ContinuationSystemUseEntry.cs | 40 ++ .../Susp/ExtensionSelectSystemUseEntry.cs | 36 ++ .../Susp/ExtensionSystemUseEntry.cs | 56 ++ .../Iso9660Ps1/Susp/GenericSuspExtension.cs | 41 ++ .../Iso9660Ps1/Susp/GenericSystemUseEntry.cs | 39 ++ .../Iso9660Ps1/Susp/PaddingSystemUseEntry.cs | 32 ++ .../Susp/SharingProtocolSystemUseEntry.cs | 43 ++ DiscUtils/Iso9660Ps1/Susp/SuspExtension.cs | 33 ++ DiscUtils/Iso9660Ps1/Susp/SuspRecords.cs | 182 ++++++ DiscUtils/Iso9660Ps1/Susp/SystemUseEntry.cs | 105 ++++ DiscUtils/Iso9660Ps1/VfsCDReader.cs | 530 ++++++++++++++++++ .../Iso9660Ps1/VolumeDescriptorDiskRegion.cs | 60 ++ .../VolumeDescriptorSetTerminator.cs | 30 + .../VolumeDescriptorSetTerminatorRegion.cs | 42 ++ DiscUtils/Iso9660Ps1/VolumeDescriptorType.cs | 11 + .../LogicalDiskManager/ComponentRecord.cs | 0 .../{Core => }/LogicalDiskManager/Database.cs | 0 .../LogicalDiskManager/DatabaseHeader.cs | 0 .../LogicalDiskManager/DatabaseRecord.cs | 0 .../LogicalDiskManager/DiskGroupRecord.cs | 0 .../LogicalDiskManager/DiskRecord.cs | 0 .../LogicalDiskManager/DynamicDisk.cs | 0 .../LogicalDiskManager/DynamicDiskGroup.cs | 0 .../LogicalDiskManager/DynamicDiskManager.cs | 0 .../DynamicDiskManagerFactory.cs | 0 .../LogicalDiskManager/DynamicVolume.cs | 0 .../LogicalDiskManager/ExtentMergeType.cs | 0 .../LogicalDiskManager/ExtentRecord.cs | 0 .../LogicalDiskManager/PrivateHeader.cs | 0 .../LogicalDiskManager/RecordType.cs | 0 .../{Core => }/LogicalDiskManager/TocBlock.cs | 0 .../LogicalDiskManager/VolumeRecord.cs | 0 DiscUtils/{Core => }/LogicalVolumeInfo.cs | 0 DiscUtils/{Core => }/LogicalVolumeStatus.cs | 0 DiscUtils/{Core => }/NativeFileSystem.cs | 0 .../Partitions/BiosExtendedPartitionTable.cs | 0 .../Partitions/BiosPartitionInfo.cs | 0 .../Partitions/BiosPartitionRecord.cs | 0 .../Partitions/BiosPartitionTable.cs | 0 .../Partitions/BiosPartitionTypes.cs | 0 .../Partitions/BiosPartitionedDiskBuilder.cs | 0 .../DefaultPartitionTableFactory.cs | 0 DiscUtils/{Core => }/Partitions/GptEntry.cs | 0 DiscUtils/{Core => }/Partitions/GptHeader.cs | 0 .../Partitions/GuidPartitionInfo.cs | 0 .../Partitions/GuidPartitionTable.cs | 0 .../Partitions/GuidPartitionTypes.cs | 0 .../{Core => }/Partitions/PartitionInfo.cs | 0 .../{Core => }/Partitions/PartitionTable.cs | 0 .../Partitions/PartitionTableFactory.cs | 0 .../PartitionTableFactoryAttribute.cs | 0 .../Partitions/WellKnownPartitionType.cs | 0 DiscUtils/{Core => }/PhysicalVolumeInfo.cs | 0 DiscUtils/{Core => }/PhysicalVolumeType.cs | 0 DiscUtils/{Core => }/Plist.cs | 0 DiscUtils/{Core => }/Raw/Disk.cs | 0 DiscUtils/{Core => }/Raw/DiskFactory.cs | 0 DiscUtils/{Core => }/Raw/DiskImageFile.cs | 0 .../{Core => }/ReadOnlyDiscFileSystem.cs | 0 DiscUtils/{Core => }/ReparsePoint.cs | 0 DiscUtils/{Core => }/ReportLevels.cs | 0 .../{Core => }/Setup/FileOpenEventArgs.cs | 0 DiscUtils/{Core => }/Setup/SetupHelper.cs | 0 DiscUtils/Streams/DiscUtils.Streams.csproj | 10 - .../System/DateTimeOffsetExtensions.cs | 0 .../{Core => }/System/ExtensionAttribute.cs | 0 DiscUtils/{Core => }/System/HashSet.cs | 0 DiscUtils/{Core => }/System/Tuple.cs | 0 DiscUtils/{Core => }/TimeConverter.cs | 0 DiscUtils/{Core => }/UnixFilePermissions.cs | 0 DiscUtils/{Core => }/UnixFileSystemInfo.cs | 0 DiscUtils/{Core => }/UnixFileType.cs | 0 DiscUtils/{Core => }/Vfs/IVfsDirectory.cs | 0 DiscUtils/{Core => }/Vfs/IVfsFile.cs | 0 .../{Core => }/Vfs/IVfsFileWithStreams.cs | 0 DiscUtils/{Core => }/Vfs/IVfsSymlink.cs | 0 DiscUtils/{Core => }/Vfs/VfsContext.cs | 0 DiscUtils/{Core => }/Vfs/VfsDirEntry.cs | 0 DiscUtils/{Core => }/Vfs/VfsFileSystem.cs | 0 .../{Core => }/Vfs/VfsFileSystemFacade.cs | 0 .../{Core => }/Vfs/VfsFileSystemFactory.cs | 0 .../Vfs/VfsFileSystemFactoryAttribute.cs | 0 DiscUtils/{Core => }/Vfs/VfsFileSystemInfo.cs | 0 .../{Core => }/Vfs/VfsFileSystemOpener.cs | 0 .../{Core => }/Vfs/VfsReadOnlyFileSystem.cs | 0 DiscUtils/{Core => }/VirtualDisk.cs | 0 DiscUtils/{Core => }/VirtualDiskClass.cs | 0 DiscUtils/{Core => }/VirtualDiskExtent.cs | 0 DiscUtils/{Core => }/VirtualDiskLayer.cs | 0 DiscUtils/{Core => }/VirtualDiskManager.cs | 0 DiscUtils/{Core => }/VirtualDiskParameters.cs | 0 DiscUtils/{Core => }/VirtualDiskTypeInfo.cs | 0 DiscUtils/{Core => }/VolumeInfo.cs | 0 DiscUtils/{Core => }/VolumeManager.cs | 0 .../{Core => }/WindowsFileInformation.cs | 0 PopsBuilder/Pops/DiscInfo.cs | 2 +- PopsBuilder/Pops/PsIsoImg.cs | 5 +- PopsBuilder/Psp/UmdInfo.cs | 2 +- 228 files changed, 5315 insertions(+), 231 deletions(-) rename DiscUtils/{Core => }/ApplePartitionMap/BlockZero.cs (100%) rename DiscUtils/{Core => }/ApplePartitionMap/PartitionMap.cs (100%) rename DiscUtils/{Core => }/ApplePartitionMap/PartitionMapEntry.cs (100%) rename DiscUtils/{Core => }/ApplePartitionMap/PartitionMapFactory.cs (100%) rename DiscUtils/{Core => }/Archives/FileRecord.cs (100%) rename DiscUtils/{Core => }/Archives/TarFile.cs (100%) rename DiscUtils/{Core => }/Archives/TarFileBuilder.cs (100%) rename DiscUtils/{Core => }/Archives/TarHeader.cs (100%) rename DiscUtils/{Core => }/Archives/TarHeaderExtent.cs (100%) rename DiscUtils/{Core => }/Archives/UnixBuildFileRecord.cs (100%) rename DiscUtils/{Core => }/ChsAddress.cs (100%) rename DiscUtils/{Core => }/ClusterMap.cs (100%) rename DiscUtils/{Core => }/ClusterRoles.cs (100%) rename DiscUtils/{Core => }/Compression/Adler32.cs (100%) rename DiscUtils/{Core => }/Compression/BZip2BlockDecoder.cs (100%) rename DiscUtils/{Core => }/Compression/BZip2CombinedHuffmanTrees.cs (100%) rename DiscUtils/{Core => }/Compression/BZip2DecoderStream.cs (100%) rename DiscUtils/{Core => }/Compression/BZip2Randomizer.cs (100%) rename DiscUtils/{Core => }/Compression/BZip2RleStream.cs (100%) rename DiscUtils/{Core => }/Compression/BigEndianBitStream.cs (100%) rename DiscUtils/{Core => }/Compression/BitStream.cs (100%) rename DiscUtils/{Core => }/Compression/BlockCompressor.cs (100%) rename DiscUtils/{Core => }/Compression/CompressionResult.cs (100%) rename DiscUtils/{Core => }/Compression/DataBlockTransform.cs (100%) rename DiscUtils/{Core => }/Compression/HuffmanTree.cs (100%) rename DiscUtils/{Core => }/Compression/InverseBurrowsWheeler.cs (100%) rename DiscUtils/{Core => }/Compression/MoveToFront.cs (100%) rename DiscUtils/{Core => }/Compression/SizedDeflateStream.cs (100%) rename DiscUtils/{Core => }/Compression/ZlibBuffer.cs (100%) rename DiscUtils/{Core => }/Compression/ZlibStream.cs (100%) delete mode 100644 DiscUtils/Core/DiscUtils.Core.csproj rename DiscUtils/{Core => }/CoreCompat/EncodingHelper.cs (100%) rename DiscUtils/{Core => }/CoreCompat/ReflectionHelper.cs (100%) rename DiscUtils/{Core => }/CoreCompat/StringExtensions.cs (100%) rename DiscUtils/{Core => }/DiscDirectoryInfo.cs (100%) rename DiscUtils/{Core => }/DiscFileInfo.cs (100%) rename DiscUtils/{Core => }/DiscFileLocator.cs (100%) rename DiscUtils/{Core => }/DiscFileSystem.cs (100%) rename DiscUtils/{Core => }/DiscFileSystemChecker.cs (100%) rename DiscUtils/{Core => }/DiscFileSystemInfo.cs (100%) rename DiscUtils/{Core => }/DiscFileSystemOptions.cs (100%) rename DiscUtils/{Core => }/DiskImageBuilder.cs (100%) rename DiscUtils/{Core => }/DiskImageFileSpecification.cs (100%) rename DiscUtils/{Core => }/FileLocator.cs (100%) rename DiscUtils/{Core => }/FileSystemInfo.cs (100%) rename DiscUtils/{Core => }/FileSystemManager.cs (100%) rename DiscUtils/{Core => }/FileSystemParameters.cs (100%) rename DiscUtils/{Core => }/FileTransport.cs (100%) rename DiscUtils/{Core => }/FloppyDiskType.cs (100%) rename DiscUtils/{Core => }/GenericDiskAdapterType.cs (100%) rename DiscUtils/{Core => }/Geometry.cs (100%) rename DiscUtils/{Core => }/GeometryCalculation.cs (100%) rename DiscUtils/{Core => }/GeometryTranslation.cs (100%) rename DiscUtils/{Core => }/IClusterBasedFileSystem.cs (100%) rename DiscUtils/{Core => }/IDiagnosticTraceable.cs (100%) rename DiscUtils/{Core => }/IFileSystem.cs (100%) rename DiscUtils/{Core => }/IUnixFileSystem.cs (100%) rename DiscUtils/{Core => }/IWindowsFileSystem.cs (100%) rename DiscUtils/{Core => }/Internal/Crc32.cs (100%) rename DiscUtils/{Core => }/Internal/Crc32Algorithm.cs (100%) rename DiscUtils/{Core => }/Internal/Crc32BigEndian.cs (100%) rename DiscUtils/{Core => }/Internal/Crc32LittleEndian.cs (100%) rename DiscUtils/{Core => }/Internal/LocalFileLocator.cs (100%) rename DiscUtils/{Core => }/Internal/LogicalVolumeFactory.cs (100%) rename DiscUtils/{Core => }/Internal/LogicalVolumeFactoryAttribute.cs (100%) rename DiscUtils/{Core => }/Internal/ObjectCache.cs (100%) rename DiscUtils/{Core => }/Internal/Utilities.cs (100%) rename DiscUtils/{Core => }/Internal/VirtualDiskFactory.cs (100%) rename DiscUtils/{Core => }/Internal/VirtualDiskFactoryAttribute.cs (100%) rename DiscUtils/{Core => }/Internal/VirtualDiskTransport.cs (100%) rename DiscUtils/{Core => }/Internal/VirtualDiskTransportAttribute.cs (100%) rename DiscUtils/{Core => }/InvalidFileSystemException.cs (100%) delete mode 100644 DiscUtils/Iso9660/DiscUtils.Iso9660.csproj create mode 100644 DiscUtils/Iso9660Ps1/BaseVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660Ps1/BootDeviceEmulation.cs create mode 100644 DiscUtils/Iso9660Ps1/BootInitialEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/BootValidationEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/BootVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660Ps1/BootVolumeDescriptorRegion.cs create mode 100644 DiscUtils/Iso9660Ps1/BuildDirectoryInfo.cs create mode 100644 DiscUtils/Iso9660Ps1/BuildDirectoryMember.cs create mode 100644 DiscUtils/Iso9660Ps1/BuildFileInfo.cs create mode 100644 DiscUtils/Iso9660Ps1/BuildParameters.cs create mode 100644 DiscUtils/Iso9660Ps1/CDBuilder.cs create mode 100644 DiscUtils/Iso9660Ps1/CDReader.cs create mode 100644 DiscUtils/Iso9660Ps1/CommonVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660Ps1/DirectoryExtent.cs create mode 100644 DiscUtils/Iso9660Ps1/DirectoryRecord.cs create mode 100644 DiscUtils/Iso9660Ps1/ExtentStream.cs create mode 100644 DiscUtils/Iso9660Ps1/File.cs create mode 100644 DiscUtils/Iso9660Ps1/FileExtent.cs create mode 100644 DiscUtils/Iso9660Ps1/FileFlags.cs create mode 100644 DiscUtils/Iso9660Ps1/Iso9660Variant.cs create mode 100644 DiscUtils/Iso9660Ps1/IsoContext.cs create mode 100644 DiscUtils/Iso9660Ps1/IsoUtilities.cs create mode 100644 DiscUtils/Iso9660Ps1/PathTable.cs create mode 100644 DiscUtils/Iso9660Ps1/PathTableRecord.cs create mode 100644 DiscUtils/Iso9660Ps1/PrimaryVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660Ps1/PrimaryVolumeDescriptorRegion.cs create mode 100644 DiscUtils/Iso9660Ps1/ReaderDirEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/ReaderDirectory.cs create mode 100644 DiscUtils/Iso9660Ps1/RockRidge/ChildLinkSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/RockRidge/FileTimeSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/RockRidge/PosixFileInfoSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/RockRidge/PosixNameSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/RockRidge/RockRidgeExtension.cs create mode 100644 DiscUtils/Iso9660Ps1/SupplementaryVolumeDescriptor.cs create mode 100644 DiscUtils/Iso9660Ps1/SupplementaryVolumeDescriptorRegion.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/ContinuationSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/ExtensionSelectSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/ExtensionSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/GenericSuspExtension.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/GenericSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/PaddingSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/SharingProtocolSystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/SuspExtension.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/SuspRecords.cs create mode 100644 DiscUtils/Iso9660Ps1/Susp/SystemUseEntry.cs create mode 100644 DiscUtils/Iso9660Ps1/VfsCDReader.cs create mode 100644 DiscUtils/Iso9660Ps1/VolumeDescriptorDiskRegion.cs create mode 100644 DiscUtils/Iso9660Ps1/VolumeDescriptorSetTerminator.cs create mode 100644 DiscUtils/Iso9660Ps1/VolumeDescriptorSetTerminatorRegion.cs create mode 100644 DiscUtils/Iso9660Ps1/VolumeDescriptorType.cs rename DiscUtils/{Core => }/LogicalDiskManager/ComponentRecord.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/Database.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/DatabaseHeader.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/DatabaseRecord.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/DiskGroupRecord.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/DiskRecord.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/DynamicDisk.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/DynamicDiskGroup.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/DynamicDiskManager.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/DynamicDiskManagerFactory.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/DynamicVolume.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/ExtentMergeType.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/ExtentRecord.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/PrivateHeader.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/RecordType.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/TocBlock.cs (100%) rename DiscUtils/{Core => }/LogicalDiskManager/VolumeRecord.cs (100%) rename DiscUtils/{Core => }/LogicalVolumeInfo.cs (100%) rename DiscUtils/{Core => }/LogicalVolumeStatus.cs (100%) rename DiscUtils/{Core => }/NativeFileSystem.cs (100%) rename DiscUtils/{Core => }/Partitions/BiosExtendedPartitionTable.cs (100%) rename DiscUtils/{Core => }/Partitions/BiosPartitionInfo.cs (100%) rename DiscUtils/{Core => }/Partitions/BiosPartitionRecord.cs (100%) rename DiscUtils/{Core => }/Partitions/BiosPartitionTable.cs (100%) rename DiscUtils/{Core => }/Partitions/BiosPartitionTypes.cs (100%) rename DiscUtils/{Core => }/Partitions/BiosPartitionedDiskBuilder.cs (100%) rename DiscUtils/{Core => }/Partitions/DefaultPartitionTableFactory.cs (100%) rename DiscUtils/{Core => }/Partitions/GptEntry.cs (100%) rename DiscUtils/{Core => }/Partitions/GptHeader.cs (100%) rename DiscUtils/{Core => }/Partitions/GuidPartitionInfo.cs (100%) rename DiscUtils/{Core => }/Partitions/GuidPartitionTable.cs (100%) rename DiscUtils/{Core => }/Partitions/GuidPartitionTypes.cs (100%) rename DiscUtils/{Core => }/Partitions/PartitionInfo.cs (100%) rename DiscUtils/{Core => }/Partitions/PartitionTable.cs (100%) rename DiscUtils/{Core => }/Partitions/PartitionTableFactory.cs (100%) rename DiscUtils/{Core => }/Partitions/PartitionTableFactoryAttribute.cs (100%) rename DiscUtils/{Core => }/Partitions/WellKnownPartitionType.cs (100%) rename DiscUtils/{Core => }/PhysicalVolumeInfo.cs (100%) rename DiscUtils/{Core => }/PhysicalVolumeType.cs (100%) rename DiscUtils/{Core => }/Plist.cs (100%) rename DiscUtils/{Core => }/Raw/Disk.cs (100%) rename DiscUtils/{Core => }/Raw/DiskFactory.cs (100%) rename DiscUtils/{Core => }/Raw/DiskImageFile.cs (100%) rename DiscUtils/{Core => }/ReadOnlyDiscFileSystem.cs (100%) rename DiscUtils/{Core => }/ReparsePoint.cs (100%) rename DiscUtils/{Core => }/ReportLevels.cs (100%) rename DiscUtils/{Core => }/Setup/FileOpenEventArgs.cs (100%) rename DiscUtils/{Core => }/Setup/SetupHelper.cs (100%) delete mode 100644 DiscUtils/Streams/DiscUtils.Streams.csproj rename DiscUtils/{Core => }/System/DateTimeOffsetExtensions.cs (100%) rename DiscUtils/{Core => }/System/ExtensionAttribute.cs (100%) rename DiscUtils/{Core => }/System/HashSet.cs (100%) rename DiscUtils/{Core => }/System/Tuple.cs (100%) rename DiscUtils/{Core => }/TimeConverter.cs (100%) rename DiscUtils/{Core => }/UnixFilePermissions.cs (100%) rename DiscUtils/{Core => }/UnixFileSystemInfo.cs (100%) rename DiscUtils/{Core => }/UnixFileType.cs (100%) rename DiscUtils/{Core => }/Vfs/IVfsDirectory.cs (100%) rename DiscUtils/{Core => }/Vfs/IVfsFile.cs (100%) rename DiscUtils/{Core => }/Vfs/IVfsFileWithStreams.cs (100%) rename DiscUtils/{Core => }/Vfs/IVfsSymlink.cs (100%) rename DiscUtils/{Core => }/Vfs/VfsContext.cs (100%) rename DiscUtils/{Core => }/Vfs/VfsDirEntry.cs (100%) rename DiscUtils/{Core => }/Vfs/VfsFileSystem.cs (100%) rename DiscUtils/{Core => }/Vfs/VfsFileSystemFacade.cs (100%) rename DiscUtils/{Core => }/Vfs/VfsFileSystemFactory.cs (100%) rename DiscUtils/{Core => }/Vfs/VfsFileSystemFactoryAttribute.cs (100%) rename DiscUtils/{Core => }/Vfs/VfsFileSystemInfo.cs (100%) rename DiscUtils/{Core => }/Vfs/VfsFileSystemOpener.cs (100%) rename DiscUtils/{Core => }/Vfs/VfsReadOnlyFileSystem.cs (100%) rename DiscUtils/{Core => }/VirtualDisk.cs (100%) rename DiscUtils/{Core => }/VirtualDiskClass.cs (100%) rename DiscUtils/{Core => }/VirtualDiskExtent.cs (100%) rename DiscUtils/{Core => }/VirtualDiskLayer.cs (100%) rename DiscUtils/{Core => }/VirtualDiskManager.cs (100%) rename DiscUtils/{Core => }/VirtualDiskParameters.cs (100%) rename DiscUtils/{Core => }/VirtualDiskTypeInfo.cs (100%) rename DiscUtils/{Core => }/VolumeInfo.cs (100%) rename DiscUtils/{Core => }/VolumeManager.cs (100%) rename DiscUtils/{Core => }/WindowsFileInformation.cs (100%) diff --git a/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user index 0d6a3e7..1fb038a 100644 --- a/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user +++ b/ChovySign-CLI/Properties/PublishProfiles/FolderProfile.pubxml.user @@ -4,7 +4,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. --> - True|2023-04-16T21:56:35.5065135Z;True|2023-04-17T09:22:54.8607008+12:00;True|2023-04-17T08:27:16.5281469+12:00;True|2023-04-17T08:22:02.0531219+12:00; + True|2023-04-17T12:00:51.4131559Z;True|2023-04-17T09:56:35.5065135+12:00;True|2023-04-17T09:22:54.8607008+12:00;True|2023-04-17T08:27:16.5281469+12:00;True|2023-04-17T08:22:02.0531219+12:00; \ No newline at end of file diff --git a/DiscUtils/Core/ApplePartitionMap/BlockZero.cs b/DiscUtils/ApplePartitionMap/BlockZero.cs similarity index 100% rename from DiscUtils/Core/ApplePartitionMap/BlockZero.cs rename to DiscUtils/ApplePartitionMap/BlockZero.cs diff --git a/DiscUtils/Core/ApplePartitionMap/PartitionMap.cs b/DiscUtils/ApplePartitionMap/PartitionMap.cs similarity index 100% rename from DiscUtils/Core/ApplePartitionMap/PartitionMap.cs rename to DiscUtils/ApplePartitionMap/PartitionMap.cs diff --git a/DiscUtils/Core/ApplePartitionMap/PartitionMapEntry.cs b/DiscUtils/ApplePartitionMap/PartitionMapEntry.cs similarity index 100% rename from DiscUtils/Core/ApplePartitionMap/PartitionMapEntry.cs rename to DiscUtils/ApplePartitionMap/PartitionMapEntry.cs diff --git a/DiscUtils/Core/ApplePartitionMap/PartitionMapFactory.cs b/DiscUtils/ApplePartitionMap/PartitionMapFactory.cs similarity index 100% rename from DiscUtils/Core/ApplePartitionMap/PartitionMapFactory.cs rename to DiscUtils/ApplePartitionMap/PartitionMapFactory.cs diff --git a/DiscUtils/Core/Archives/FileRecord.cs b/DiscUtils/Archives/FileRecord.cs similarity index 100% rename from DiscUtils/Core/Archives/FileRecord.cs rename to DiscUtils/Archives/FileRecord.cs diff --git a/DiscUtils/Core/Archives/TarFile.cs b/DiscUtils/Archives/TarFile.cs similarity index 100% rename from DiscUtils/Core/Archives/TarFile.cs rename to DiscUtils/Archives/TarFile.cs diff --git a/DiscUtils/Core/Archives/TarFileBuilder.cs b/DiscUtils/Archives/TarFileBuilder.cs similarity index 100% rename from DiscUtils/Core/Archives/TarFileBuilder.cs rename to DiscUtils/Archives/TarFileBuilder.cs diff --git a/DiscUtils/Core/Archives/TarHeader.cs b/DiscUtils/Archives/TarHeader.cs similarity index 100% rename from DiscUtils/Core/Archives/TarHeader.cs rename to DiscUtils/Archives/TarHeader.cs diff --git a/DiscUtils/Core/Archives/TarHeaderExtent.cs b/DiscUtils/Archives/TarHeaderExtent.cs similarity index 100% rename from DiscUtils/Core/Archives/TarHeaderExtent.cs rename to DiscUtils/Archives/TarHeaderExtent.cs diff --git a/DiscUtils/Core/Archives/UnixBuildFileRecord.cs b/DiscUtils/Archives/UnixBuildFileRecord.cs similarity index 100% rename from DiscUtils/Core/Archives/UnixBuildFileRecord.cs rename to DiscUtils/Archives/UnixBuildFileRecord.cs diff --git a/DiscUtils/Core/ChsAddress.cs b/DiscUtils/ChsAddress.cs similarity index 100% rename from DiscUtils/Core/ChsAddress.cs rename to DiscUtils/ChsAddress.cs diff --git a/DiscUtils/Core/ClusterMap.cs b/DiscUtils/ClusterMap.cs similarity index 100% rename from DiscUtils/Core/ClusterMap.cs rename to DiscUtils/ClusterMap.cs diff --git a/DiscUtils/Core/ClusterRoles.cs b/DiscUtils/ClusterRoles.cs similarity index 100% rename from DiscUtils/Core/ClusterRoles.cs rename to DiscUtils/ClusterRoles.cs diff --git a/DiscUtils/Core/Compression/Adler32.cs b/DiscUtils/Compression/Adler32.cs similarity index 100% rename from DiscUtils/Core/Compression/Adler32.cs rename to DiscUtils/Compression/Adler32.cs diff --git a/DiscUtils/Core/Compression/BZip2BlockDecoder.cs b/DiscUtils/Compression/BZip2BlockDecoder.cs similarity index 100% rename from DiscUtils/Core/Compression/BZip2BlockDecoder.cs rename to DiscUtils/Compression/BZip2BlockDecoder.cs diff --git a/DiscUtils/Core/Compression/BZip2CombinedHuffmanTrees.cs b/DiscUtils/Compression/BZip2CombinedHuffmanTrees.cs similarity index 100% rename from DiscUtils/Core/Compression/BZip2CombinedHuffmanTrees.cs rename to DiscUtils/Compression/BZip2CombinedHuffmanTrees.cs diff --git a/DiscUtils/Core/Compression/BZip2DecoderStream.cs b/DiscUtils/Compression/BZip2DecoderStream.cs similarity index 100% rename from DiscUtils/Core/Compression/BZip2DecoderStream.cs rename to DiscUtils/Compression/BZip2DecoderStream.cs diff --git a/DiscUtils/Core/Compression/BZip2Randomizer.cs b/DiscUtils/Compression/BZip2Randomizer.cs similarity index 100% rename from DiscUtils/Core/Compression/BZip2Randomizer.cs rename to DiscUtils/Compression/BZip2Randomizer.cs diff --git a/DiscUtils/Core/Compression/BZip2RleStream.cs b/DiscUtils/Compression/BZip2RleStream.cs similarity index 100% rename from DiscUtils/Core/Compression/BZip2RleStream.cs rename to DiscUtils/Compression/BZip2RleStream.cs diff --git a/DiscUtils/Core/Compression/BigEndianBitStream.cs b/DiscUtils/Compression/BigEndianBitStream.cs similarity index 100% rename from DiscUtils/Core/Compression/BigEndianBitStream.cs rename to DiscUtils/Compression/BigEndianBitStream.cs diff --git a/DiscUtils/Core/Compression/BitStream.cs b/DiscUtils/Compression/BitStream.cs similarity index 100% rename from DiscUtils/Core/Compression/BitStream.cs rename to DiscUtils/Compression/BitStream.cs diff --git a/DiscUtils/Core/Compression/BlockCompressor.cs b/DiscUtils/Compression/BlockCompressor.cs similarity index 100% rename from DiscUtils/Core/Compression/BlockCompressor.cs rename to DiscUtils/Compression/BlockCompressor.cs diff --git a/DiscUtils/Core/Compression/CompressionResult.cs b/DiscUtils/Compression/CompressionResult.cs similarity index 100% rename from DiscUtils/Core/Compression/CompressionResult.cs rename to DiscUtils/Compression/CompressionResult.cs diff --git a/DiscUtils/Core/Compression/DataBlockTransform.cs b/DiscUtils/Compression/DataBlockTransform.cs similarity index 100% rename from DiscUtils/Core/Compression/DataBlockTransform.cs rename to DiscUtils/Compression/DataBlockTransform.cs diff --git a/DiscUtils/Core/Compression/HuffmanTree.cs b/DiscUtils/Compression/HuffmanTree.cs similarity index 100% rename from DiscUtils/Core/Compression/HuffmanTree.cs rename to DiscUtils/Compression/HuffmanTree.cs diff --git a/DiscUtils/Core/Compression/InverseBurrowsWheeler.cs b/DiscUtils/Compression/InverseBurrowsWheeler.cs similarity index 100% rename from DiscUtils/Core/Compression/InverseBurrowsWheeler.cs rename to DiscUtils/Compression/InverseBurrowsWheeler.cs diff --git a/DiscUtils/Core/Compression/MoveToFront.cs b/DiscUtils/Compression/MoveToFront.cs similarity index 100% rename from DiscUtils/Core/Compression/MoveToFront.cs rename to DiscUtils/Compression/MoveToFront.cs diff --git a/DiscUtils/Core/Compression/SizedDeflateStream.cs b/DiscUtils/Compression/SizedDeflateStream.cs similarity index 100% rename from DiscUtils/Core/Compression/SizedDeflateStream.cs rename to DiscUtils/Compression/SizedDeflateStream.cs diff --git a/DiscUtils/Core/Compression/ZlibBuffer.cs b/DiscUtils/Compression/ZlibBuffer.cs similarity index 100% rename from DiscUtils/Core/Compression/ZlibBuffer.cs rename to DiscUtils/Compression/ZlibBuffer.cs diff --git a/DiscUtils/Core/Compression/ZlibStream.cs b/DiscUtils/Compression/ZlibStream.cs similarity index 100% rename from DiscUtils/Core/Compression/ZlibStream.cs rename to DiscUtils/Compression/ZlibStream.cs diff --git a/DiscUtils/Core/DiscUtils.Core.csproj b/DiscUtils/Core/DiscUtils.Core.csproj deleted file mode 100644 index 8abd0ad..0000000 --- a/DiscUtils/Core/DiscUtils.Core.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Implementation of the ISO, UDF, FAT and NTFS file systems is now fairly stable. VHD, XVA, VMDK and VDI disk formats are implemented, as well as read/write Registry support. The library also includes a simple iSCSI initiator, for accessing disks via iSCSI and an NFS client implementation. - DiscUtils (for .NET and .NET Core), core library that supports parts of DiscUtils - Kenneth Bell;Quamotion;LordMike - DiscUtils;VHD;VDI;XVA;VMDK;ISO;NTFS;EXT2FS - - - - - - - diff --git a/DiscUtils/Core/CoreCompat/EncodingHelper.cs b/DiscUtils/CoreCompat/EncodingHelper.cs similarity index 100% rename from DiscUtils/Core/CoreCompat/EncodingHelper.cs rename to DiscUtils/CoreCompat/EncodingHelper.cs diff --git a/DiscUtils/Core/CoreCompat/ReflectionHelper.cs b/DiscUtils/CoreCompat/ReflectionHelper.cs similarity index 100% rename from DiscUtils/Core/CoreCompat/ReflectionHelper.cs rename to DiscUtils/CoreCompat/ReflectionHelper.cs diff --git a/DiscUtils/Core/CoreCompat/StringExtensions.cs b/DiscUtils/CoreCompat/StringExtensions.cs similarity index 100% rename from DiscUtils/Core/CoreCompat/StringExtensions.cs rename to DiscUtils/CoreCompat/StringExtensions.cs diff --git a/DiscUtils/Core/DiscDirectoryInfo.cs b/DiscUtils/DiscDirectoryInfo.cs similarity index 100% rename from DiscUtils/Core/DiscDirectoryInfo.cs rename to DiscUtils/DiscDirectoryInfo.cs diff --git a/DiscUtils/Core/DiscFileInfo.cs b/DiscUtils/DiscFileInfo.cs similarity index 100% rename from DiscUtils/Core/DiscFileInfo.cs rename to DiscUtils/DiscFileInfo.cs diff --git a/DiscUtils/Core/DiscFileLocator.cs b/DiscUtils/DiscFileLocator.cs similarity index 100% rename from DiscUtils/Core/DiscFileLocator.cs rename to DiscUtils/DiscFileLocator.cs diff --git a/DiscUtils/Core/DiscFileSystem.cs b/DiscUtils/DiscFileSystem.cs similarity index 100% rename from DiscUtils/Core/DiscFileSystem.cs rename to DiscUtils/DiscFileSystem.cs diff --git a/DiscUtils/Core/DiscFileSystemChecker.cs b/DiscUtils/DiscFileSystemChecker.cs similarity index 100% rename from DiscUtils/Core/DiscFileSystemChecker.cs rename to DiscUtils/DiscFileSystemChecker.cs diff --git a/DiscUtils/Core/DiscFileSystemInfo.cs b/DiscUtils/DiscFileSystemInfo.cs similarity index 100% rename from DiscUtils/Core/DiscFileSystemInfo.cs rename to DiscUtils/DiscFileSystemInfo.cs diff --git a/DiscUtils/Core/DiscFileSystemOptions.cs b/DiscUtils/DiscFileSystemOptions.cs similarity index 100% rename from DiscUtils/Core/DiscFileSystemOptions.cs rename to DiscUtils/DiscFileSystemOptions.cs diff --git a/DiscUtils/Core/DiskImageBuilder.cs b/DiscUtils/DiskImageBuilder.cs similarity index 100% rename from DiscUtils/Core/DiskImageBuilder.cs rename to DiscUtils/DiskImageBuilder.cs diff --git a/DiscUtils/Core/DiskImageFileSpecification.cs b/DiscUtils/DiskImageFileSpecification.cs similarity index 100% rename from DiscUtils/Core/DiskImageFileSpecification.cs rename to DiscUtils/DiskImageFileSpecification.cs diff --git a/DiscUtils/Core/FileLocator.cs b/DiscUtils/FileLocator.cs similarity index 100% rename from DiscUtils/Core/FileLocator.cs rename to DiscUtils/FileLocator.cs diff --git a/DiscUtils/Core/FileSystemInfo.cs b/DiscUtils/FileSystemInfo.cs similarity index 100% rename from DiscUtils/Core/FileSystemInfo.cs rename to DiscUtils/FileSystemInfo.cs diff --git a/DiscUtils/Core/FileSystemManager.cs b/DiscUtils/FileSystemManager.cs similarity index 100% rename from DiscUtils/Core/FileSystemManager.cs rename to DiscUtils/FileSystemManager.cs diff --git a/DiscUtils/Core/FileSystemParameters.cs b/DiscUtils/FileSystemParameters.cs similarity index 100% rename from DiscUtils/Core/FileSystemParameters.cs rename to DiscUtils/FileSystemParameters.cs diff --git a/DiscUtils/Core/FileTransport.cs b/DiscUtils/FileTransport.cs similarity index 100% rename from DiscUtils/Core/FileTransport.cs rename to DiscUtils/FileTransport.cs diff --git a/DiscUtils/Core/FloppyDiskType.cs b/DiscUtils/FloppyDiskType.cs similarity index 100% rename from DiscUtils/Core/FloppyDiskType.cs rename to DiscUtils/FloppyDiskType.cs diff --git a/DiscUtils/Core/GenericDiskAdapterType.cs b/DiscUtils/GenericDiskAdapterType.cs similarity index 100% rename from DiscUtils/Core/GenericDiskAdapterType.cs rename to DiscUtils/GenericDiskAdapterType.cs diff --git a/DiscUtils/Core/Geometry.cs b/DiscUtils/Geometry.cs similarity index 100% rename from DiscUtils/Core/Geometry.cs rename to DiscUtils/Geometry.cs diff --git a/DiscUtils/Core/GeometryCalculation.cs b/DiscUtils/GeometryCalculation.cs similarity index 100% rename from DiscUtils/Core/GeometryCalculation.cs rename to DiscUtils/GeometryCalculation.cs diff --git a/DiscUtils/Core/GeometryTranslation.cs b/DiscUtils/GeometryTranslation.cs similarity index 100% rename from DiscUtils/Core/GeometryTranslation.cs rename to DiscUtils/GeometryTranslation.cs diff --git a/DiscUtils/Core/IClusterBasedFileSystem.cs b/DiscUtils/IClusterBasedFileSystem.cs similarity index 100% rename from DiscUtils/Core/IClusterBasedFileSystem.cs rename to DiscUtils/IClusterBasedFileSystem.cs diff --git a/DiscUtils/Core/IDiagnosticTraceable.cs b/DiscUtils/IDiagnosticTraceable.cs similarity index 100% rename from DiscUtils/Core/IDiagnosticTraceable.cs rename to DiscUtils/IDiagnosticTraceable.cs diff --git a/DiscUtils/Core/IFileSystem.cs b/DiscUtils/IFileSystem.cs similarity index 100% rename from DiscUtils/Core/IFileSystem.cs rename to DiscUtils/IFileSystem.cs diff --git a/DiscUtils/Core/IUnixFileSystem.cs b/DiscUtils/IUnixFileSystem.cs similarity index 100% rename from DiscUtils/Core/IUnixFileSystem.cs rename to DiscUtils/IUnixFileSystem.cs diff --git a/DiscUtils/Core/IWindowsFileSystem.cs b/DiscUtils/IWindowsFileSystem.cs similarity index 100% rename from DiscUtils/Core/IWindowsFileSystem.cs rename to DiscUtils/IWindowsFileSystem.cs diff --git a/DiscUtils/Core/Internal/Crc32.cs b/DiscUtils/Internal/Crc32.cs similarity index 100% rename from DiscUtils/Core/Internal/Crc32.cs rename to DiscUtils/Internal/Crc32.cs diff --git a/DiscUtils/Core/Internal/Crc32Algorithm.cs b/DiscUtils/Internal/Crc32Algorithm.cs similarity index 100% rename from DiscUtils/Core/Internal/Crc32Algorithm.cs rename to DiscUtils/Internal/Crc32Algorithm.cs diff --git a/DiscUtils/Core/Internal/Crc32BigEndian.cs b/DiscUtils/Internal/Crc32BigEndian.cs similarity index 100% rename from DiscUtils/Core/Internal/Crc32BigEndian.cs rename to DiscUtils/Internal/Crc32BigEndian.cs diff --git a/DiscUtils/Core/Internal/Crc32LittleEndian.cs b/DiscUtils/Internal/Crc32LittleEndian.cs similarity index 100% rename from DiscUtils/Core/Internal/Crc32LittleEndian.cs rename to DiscUtils/Internal/Crc32LittleEndian.cs diff --git a/DiscUtils/Core/Internal/LocalFileLocator.cs b/DiscUtils/Internal/LocalFileLocator.cs similarity index 100% rename from DiscUtils/Core/Internal/LocalFileLocator.cs rename to DiscUtils/Internal/LocalFileLocator.cs diff --git a/DiscUtils/Core/Internal/LogicalVolumeFactory.cs b/DiscUtils/Internal/LogicalVolumeFactory.cs similarity index 100% rename from DiscUtils/Core/Internal/LogicalVolumeFactory.cs rename to DiscUtils/Internal/LogicalVolumeFactory.cs diff --git a/DiscUtils/Core/Internal/LogicalVolumeFactoryAttribute.cs b/DiscUtils/Internal/LogicalVolumeFactoryAttribute.cs similarity index 100% rename from DiscUtils/Core/Internal/LogicalVolumeFactoryAttribute.cs rename to DiscUtils/Internal/LogicalVolumeFactoryAttribute.cs diff --git a/DiscUtils/Core/Internal/ObjectCache.cs b/DiscUtils/Internal/ObjectCache.cs similarity index 100% rename from DiscUtils/Core/Internal/ObjectCache.cs rename to DiscUtils/Internal/ObjectCache.cs diff --git a/DiscUtils/Core/Internal/Utilities.cs b/DiscUtils/Internal/Utilities.cs similarity index 100% rename from DiscUtils/Core/Internal/Utilities.cs rename to DiscUtils/Internal/Utilities.cs diff --git a/DiscUtils/Core/Internal/VirtualDiskFactory.cs b/DiscUtils/Internal/VirtualDiskFactory.cs similarity index 100% rename from DiscUtils/Core/Internal/VirtualDiskFactory.cs rename to DiscUtils/Internal/VirtualDiskFactory.cs diff --git a/DiscUtils/Core/Internal/VirtualDiskFactoryAttribute.cs b/DiscUtils/Internal/VirtualDiskFactoryAttribute.cs similarity index 100% rename from DiscUtils/Core/Internal/VirtualDiskFactoryAttribute.cs rename to DiscUtils/Internal/VirtualDiskFactoryAttribute.cs diff --git a/DiscUtils/Core/Internal/VirtualDiskTransport.cs b/DiscUtils/Internal/VirtualDiskTransport.cs similarity index 100% rename from DiscUtils/Core/Internal/VirtualDiskTransport.cs rename to DiscUtils/Internal/VirtualDiskTransport.cs diff --git a/DiscUtils/Core/Internal/VirtualDiskTransportAttribute.cs b/DiscUtils/Internal/VirtualDiskTransportAttribute.cs similarity index 100% rename from DiscUtils/Core/Internal/VirtualDiskTransportAttribute.cs rename to DiscUtils/Internal/VirtualDiskTransportAttribute.cs diff --git a/DiscUtils/Core/InvalidFileSystemException.cs b/DiscUtils/InvalidFileSystemException.cs similarity index 100% rename from DiscUtils/Core/InvalidFileSystemException.cs rename to DiscUtils/InvalidFileSystemException.cs diff --git a/DiscUtils/Iso9660/BaseVolumeDescriptor.cs b/DiscUtils/Iso9660/BaseVolumeDescriptor.cs index 3aeb519..bab65d6 100644 --- a/DiscUtils/Iso9660/BaseVolumeDescriptor.cs +++ b/DiscUtils/Iso9660/BaseVolumeDescriptor.cs @@ -32,14 +32,12 @@ namespace DiscUtils.Iso9660 public readonly string StandardIdentifier; public readonly VolumeDescriptorType VolumeDescriptorType; public readonly byte VolumeDescriptorVersion; - private int SectorSize; - public BaseVolumeDescriptor(VolumeDescriptorType type, byte version, int sectorSize) + public BaseVolumeDescriptor(VolumeDescriptorType type, byte version) { VolumeDescriptorType = type; StandardIdentifier = "CD001"; VolumeDescriptorVersion = version; - SectorSize = sectorSize; } public BaseVolumeDescriptor(byte[] src, int offset) @@ -51,7 +49,7 @@ namespace DiscUtils.Iso9660 internal virtual void WriteTo(byte[] buffer, int offset) { - Array.Clear(buffer, offset, SectorSize); + Array.Clear(buffer, offset, IsoUtilities.SectorSize); buffer[offset] = (byte)VolumeDescriptorType; IsoUtilities.WriteAChars(buffer, offset + 1, 5, StandardIdentifier); buffer[offset + 6] = VolumeDescriptorVersion; diff --git a/DiscUtils/Iso9660/BootVolumeDescriptor.cs b/DiscUtils/Iso9660/BootVolumeDescriptor.cs index d32908a..335711b 100644 --- a/DiscUtils/Iso9660/BootVolumeDescriptor.cs +++ b/DiscUtils/Iso9660/BootVolumeDescriptor.cs @@ -28,8 +28,8 @@ namespace DiscUtils.Iso9660 { public const string ElToritoSystemIdentifier = "EL TORITO SPECIFICATION"; - public BootVolumeDescriptor(uint catalogSector, int sectorSize) - : base(VolumeDescriptorType.Boot, 1, sectorSize) + public BootVolumeDescriptor(uint catalogSector) + : base(VolumeDescriptorType.Boot, 1) { CatalogSector = catalogSector; } diff --git a/DiscUtils/Iso9660/BootVolumeDescriptorRegion.cs b/DiscUtils/Iso9660/BootVolumeDescriptorRegion.cs index 8e73eb0..f98d1b7 100644 --- a/DiscUtils/Iso9660/BootVolumeDescriptorRegion.cs +++ b/DiscUtils/Iso9660/BootVolumeDescriptorRegion.cs @@ -26,15 +26,15 @@ namespace DiscUtils.Iso9660 { private readonly BootVolumeDescriptor _descriptor; - public BootVolumeDescriptorRegion(BootVolumeDescriptor descriptor, long start, int sectorSize) - : base(start, sectorSize) + public BootVolumeDescriptorRegion(BootVolumeDescriptor descriptor, long start) + : base(start) { _descriptor = descriptor; } protected override byte[] GetBlockData() { - byte[] buffer = new byte[Length]; + byte[] buffer = new byte[IsoUtilities.SectorSize]; _descriptor.WriteTo(buffer, 0); return buffer; } diff --git a/DiscUtils/Iso9660/BuildDirectoryInfo.cs b/DiscUtils/Iso9660/BuildDirectoryInfo.cs index 3212654..2fdc382 100644 --- a/DiscUtils/Iso9660/BuildDirectoryInfo.cs +++ b/DiscUtils/Iso9660/BuildDirectoryInfo.cs @@ -39,15 +39,13 @@ namespace DiscUtils.Iso9660 private readonly BuildDirectoryInfo _parent; private List _sortedMembers; - private int _sectorSize; - internal BuildDirectoryInfo(string name, BuildDirectoryInfo parent, int sectorSize) + internal BuildDirectoryInfo(string name, BuildDirectoryInfo parent) : base(name, MakeShortDirName(name, parent)) { _parent = parent == null ? this : parent; HierarchyDepth = parent == null ? 0 : parent.HierarchyDepth + 1; _members = new Dictionary(); - _sectorSize = sectorSize; } internal int HierarchyDepth { get; } @@ -89,16 +87,16 @@ namespace DiscUtils.Iso9660 // If this record would span a sector boundary, then the current sector is // zero-padded, and the record goes at the start of the next sector. - if (total % _sectorSize + recordSize > _sectorSize) + if (total % IsoUtilities.SectorSize + recordSize > IsoUtilities.SectorSize) { - long padLength = _sectorSize - total % _sectorSize; + long padLength = IsoUtilities.SectorSize - total % IsoUtilities.SectorSize; total += padLength; } total += recordSize; } - return MathUtilities.RoundUp(total, _sectorSize); + return MathUtilities.RoundUp(total, IsoUtilities.SectorSize); } internal uint GetPathTableEntrySize(Encoding enc) @@ -115,31 +113,31 @@ namespace DiscUtils.Iso9660 List sorted = GetSortedMembers(); // Two pseudo entries, effectively '.' and '..' - pos += WriteMember(this, "\0", Encoding.ASCII, buffer, offset + pos, locationTable, enc, _sectorSize); - pos += WriteMember(_parent, "\x01", Encoding.ASCII, buffer, offset + pos, locationTable, enc, _sectorSize); + pos += WriteMember(this, "\0", Encoding.ASCII, buffer, offset + pos, locationTable, enc); + pos += WriteMember(_parent, "\x01", Encoding.ASCII, buffer, offset + pos, locationTable, enc); foreach (BuildDirectoryMember m in sorted) { uint recordSize = m.GetDirectoryRecordSize(enc); - if (pos % _sectorSize + recordSize > _sectorSize) + if (pos % IsoUtilities.SectorSize + recordSize > IsoUtilities.SectorSize) { - int padLength = _sectorSize - pos % _sectorSize; + int padLength = IsoUtilities.SectorSize - pos % IsoUtilities.SectorSize; Array.Clear(buffer, offset + pos, padLength); pos += padLength; } - pos += WriteMember(m, null, enc, buffer, offset + pos, locationTable, enc, _sectorSize); + pos += WriteMember(m, null, enc, buffer, offset + pos, locationTable, enc); } // Ensure final padding data is zero'd - int finalPadLength = MathUtilities.RoundUp(pos, _sectorSize) - pos; + int finalPadLength = MathUtilities.RoundUp(pos, IsoUtilities.SectorSize) - pos; Array.Clear(buffer, offset + pos, finalPadLength); return pos + finalPadLength; } - private static int WriteMember(BuildDirectoryMember m, string nameOverride, Encoding nameEnc, byte[] buffer, int offset, Dictionary locationTable, Encoding dataEnc, int sectorSize) + private static int WriteMember(BuildDirectoryMember m, string nameOverride, Encoding nameEnc, byte[] buffer, int offset, Dictionary locationTable, Encoding dataEnc) { DirectoryRecord dr = new DirectoryRecord(); dr.FileIdentifier = m.PickName(nameOverride, nameEnc); diff --git a/DiscUtils/Iso9660/BuildParameters.cs b/DiscUtils/Iso9660/BuildParameters.cs index 63f7658..1ac3892 100644 --- a/DiscUtils/Iso9660/BuildParameters.cs +++ b/DiscUtils/Iso9660/BuildParameters.cs @@ -28,11 +28,8 @@ namespace DiscUtils.Iso9660 { VolumeIdentifier = string.Empty; UseJoliet = true; - SectorSize = 2048; } - public int SectorSize { get; set; } - public bool UseJoliet { get; set; } public string VolumeIdentifier { get; set; } diff --git a/DiscUtils/Iso9660/CDBuilder.cs b/DiscUtils/Iso9660/CDBuilder.cs index 0d7a275..7042d1e 100644 --- a/DiscUtils/Iso9660/CDBuilder.cs +++ b/DiscUtils/Iso9660/CDBuilder.cs @@ -51,7 +51,6 @@ namespace DiscUtils.Iso9660 private readonly List _files; private readonly BuildDirectoryInfo _rootDirectory; - private readonly int _sectorSize; /// /// Initializes a new instance of the CDBuilder class. @@ -60,24 +59,7 @@ namespace DiscUtils.Iso9660 { _files = new List(); _dirs = new List(); - _sectorSize = 2048; - _rootDirectory = new BuildDirectoryInfo("\0", null, _sectorSize); - _dirs.Add(_rootDirectory); - - _buildParams = new BuildParameters(); - _buildParams.UseJoliet = true; - } - - - /// - /// Initializes a new instance of the CDBuilder class. - /// - public CDBuilder(int sectorSize) - { - _files = new List(); - _dirs = new List(); - _sectorSize = sectorSize; - _rootDirectory = new BuildDirectoryInfo("\0", null, _sectorSize); + _rootDirectory = new BuildDirectoryInfo("\0", null); _dirs.Add(_rootDirectory); _buildParams = new BuildParameters(); @@ -270,10 +252,10 @@ namespace DiscUtils.Iso9660 Dictionary supplementaryLocationTable = new Dictionary(); - long focus = DiskStart + 3 * _buildParams.SectorSize; // Primary, Supplementary, End (fixed at end...) + long focus = DiskStart + 3 * IsoUtilities.SectorSize; // Primary, Supplementary, End (fixed at end...) if (_bootEntry != null) { - focus += _buildParams.SectorSize; + focus += IsoUtilities.SectorSize; } // #################################################################### @@ -283,21 +265,21 @@ namespace DiscUtils.Iso9660 if (_bootEntry != null) { long bootImagePos = focus; - Stream realBootImage = PatchBootImage(_bootImage, (uint)(DiskStart / _buildParams.SectorSize), - (uint)(bootImagePos / _buildParams.SectorSize)); + Stream realBootImage = PatchBootImage(_bootImage, (uint)(DiskStart / IsoUtilities.SectorSize), + (uint)(bootImagePos / IsoUtilities.SectorSize)); BuilderStreamExtent bootImageExtent = new BuilderStreamExtent(focus, realBootImage); fixedRegions.Add(bootImageExtent); - focus += MathUtilities.RoundUp(bootImageExtent.Length, _buildParams.SectorSize); + focus += MathUtilities.RoundUp(bootImageExtent.Length, IsoUtilities.SectorSize); bootCatalogPos = focus; - byte[] bootCatalog = new byte[_buildParams.SectorSize]; + byte[] bootCatalog = new byte[IsoUtilities.SectorSize]; BootValidationEntry bve = new BootValidationEntry(); bve.WriteTo(bootCatalog, 0x00); - _bootEntry.ImageStart = (uint)MathUtilities.Ceil(bootImagePos, _buildParams.SectorSize); + _bootEntry.ImageStart = (uint)MathUtilities.Ceil(bootImagePos, IsoUtilities.SectorSize); _bootEntry.SectorCount = (ushort)MathUtilities.Ceil(_bootImage.Length, Sizes.Sector); _bootEntry.WriteTo(bootCatalog, 0x20); fixedRegions.Add(new BuilderBufferExtent(bootCatalogPos, bootCatalog)); - focus += _buildParams.SectorSize; + focus += IsoUtilities.SectorSize; } // #################################################################### @@ -307,8 +289,8 @@ namespace DiscUtils.Iso9660 // Find end of the file data, fixing the files in place as we go foreach (BuildFileInfo fi in _files) { - primaryLocationTable.Add(fi, (uint)(focus / _buildParams.SectorSize)); - supplementaryLocationTable.Add(fi, (uint)(focus / _buildParams.SectorSize)); + primaryLocationTable.Add(fi, (uint)(focus / IsoUtilities.SectorSize)); + supplementaryLocationTable.Add(fi, (uint)(focus / IsoUtilities.SectorSize)); FileExtent extent = new FileExtent(fi, focus); // Only remember files of non-zero length (otherwise we'll stomp on a valid file) @@ -317,7 +299,7 @@ namespace DiscUtils.Iso9660 fixedRegions.Add(extent); } - focus += MathUtilities.RoundUp(extent.Length, _buildParams.SectorSize); + focus += MathUtilities.RoundUp(extent.Length, IsoUtilities.SectorSize); } // #################################################################### @@ -332,20 +314,20 @@ namespace DiscUtils.Iso9660 long startOfFirstDirData = focus; foreach (BuildDirectoryInfo di in _dirs) { - primaryLocationTable.Add(di, (uint)(focus / _buildParams.SectorSize)); + primaryLocationTable.Add(di, (uint)(focus / IsoUtilities.SectorSize)); DirectoryExtent extent = new DirectoryExtent(di, primaryLocationTable, Encoding.ASCII, focus); fixedRegions.Add(extent); - focus += MathUtilities.RoundUp(extent.Length, _buildParams.SectorSize); + focus += MathUtilities.RoundUp(extent.Length, IsoUtilities.SectorSize); } // Find end of the second directory table, fixing supplementary directories in place. long startOfSecondDirData = focus; foreach (BuildDirectoryInfo di in _dirs) { - supplementaryLocationTable.Add(di, (uint)(focus / _buildParams.SectorSize)); + supplementaryLocationTable.Add(di, (uint)(focus / IsoUtilities.SectorSize)); DirectoryExtent extent = new DirectoryExtent(di, supplementaryLocationTable, suppEncoding, focus); fixedRegions.Add(extent); - focus += MathUtilities.RoundUp(extent.Length, _buildParams.SectorSize); + focus += MathUtilities.RoundUp(extent.Length, IsoUtilities.SectorSize); } // #################################################################### @@ -362,24 +344,24 @@ namespace DiscUtils.Iso9660 long startOfFirstPathTable = focus; PathTable pathTable = new PathTable(false, Encoding.ASCII, _dirs, primaryLocationTable, focus); fixedRegions.Add(pathTable); - focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + focus += MathUtilities.RoundUp(pathTable.Length, IsoUtilities.SectorSize); long primaryPathTableLength = pathTable.Length; long startOfSecondPathTable = focus; pathTable = new PathTable(true, Encoding.ASCII, _dirs, primaryLocationTable, focus); fixedRegions.Add(pathTable); - focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + focus += MathUtilities.RoundUp(pathTable.Length, IsoUtilities.SectorSize); long startOfThirdPathTable = focus; pathTable = new PathTable(false, suppEncoding, _dirs, supplementaryLocationTable, focus); fixedRegions.Add(pathTable); - focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + focus += MathUtilities.RoundUp(pathTable.Length, IsoUtilities.SectorSize); long supplementaryPathTableLength = pathTable.Length; long startOfFourthPathTable = focus; pathTable = new PathTable(true, suppEncoding, _dirs, supplementaryLocationTable, focus); fixedRegions.Add(pathTable); - focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + focus += MathUtilities.RoundUp(pathTable.Length, IsoUtilities.SectorSize); // Find the end of the disk totalLength = focus; @@ -390,45 +372,43 @@ namespace DiscUtils.Iso9660 int regionIdx = 0; focus = DiskStart; PrimaryVolumeDescriptor pvDesc = new PrimaryVolumeDescriptor( - (uint)(totalLength / _buildParams.SectorSize), // VolumeSpaceSize + (uint)(totalLength / IsoUtilities.SectorSize), // VolumeSpaceSize (uint)primaryPathTableLength, // PathTableSize - (uint)(startOfFirstPathTable / _buildParams.SectorSize), // TypeLPathTableLocation - (uint)(startOfSecondPathTable / _buildParams.SectorSize), // TypeMPathTableLocation - (uint)(startOfFirstDirData / _buildParams.SectorSize), // RootDirectory.LocationOfExtent + (uint)(startOfFirstPathTable / IsoUtilities.SectorSize), // TypeLPathTableLocation + (uint)(startOfSecondPathTable / IsoUtilities.SectorSize), // TypeMPathTableLocation + (uint)(startOfFirstDirData / IsoUtilities.SectorSize), // RootDirectory.LocationOfExtent (uint)_rootDirectory.GetDataSize(Encoding.ASCII), // RootDirectory.DataLength - buildTime, - _sectorSize); + buildTime); pvDesc.VolumeIdentifier = _buildParams.VolumeIdentifier; - PrimaryVolumeDescriptorRegion pvdr = new PrimaryVolumeDescriptorRegion(pvDesc, focus, _sectorSize); + PrimaryVolumeDescriptorRegion pvdr = new PrimaryVolumeDescriptorRegion(pvDesc, focus); fixedRegions.Insert(regionIdx++, pvdr); - focus += _buildParams.SectorSize; + focus += IsoUtilities.SectorSize; if (_bootEntry != null) { BootVolumeDescriptor bvDesc = new BootVolumeDescriptor( - (uint)(bootCatalogPos / _buildParams.SectorSize), _buildParams.SectorSize); - BootVolumeDescriptorRegion bvdr = new BootVolumeDescriptorRegion(bvDesc, focus, _buildParams.SectorSize); + (uint)(bootCatalogPos / IsoUtilities.SectorSize)); + BootVolumeDescriptorRegion bvdr = new BootVolumeDescriptorRegion(bvDesc, focus); fixedRegions.Insert(regionIdx++, bvdr); - focus += _buildParams.SectorSize; + focus += IsoUtilities.SectorSize; } SupplementaryVolumeDescriptor svDesc = new SupplementaryVolumeDescriptor( - (uint)(totalLength / _buildParams.SectorSize), // VolumeSpaceSize + (uint)(totalLength / IsoUtilities.SectorSize), // VolumeSpaceSize (uint)supplementaryPathTableLength, // PathTableSize - (uint)(startOfThirdPathTable / _buildParams.SectorSize), // TypeLPathTableLocation - (uint)(startOfFourthPathTable / _buildParams.SectorSize), // TypeMPathTableLocation - (uint)(startOfSecondDirData / _buildParams.SectorSize), // RootDirectory.LocationOfExtent + (uint)(startOfThirdPathTable / IsoUtilities.SectorSize), // TypeLPathTableLocation + (uint)(startOfFourthPathTable / IsoUtilities.SectorSize), // TypeMPathTableLocation + (uint)(startOfSecondDirData / IsoUtilities.SectorSize), // RootDirectory.LocationOfExtent (uint)_rootDirectory.GetDataSize(suppEncoding), // RootDirectory.DataLength buildTime, - suppEncoding, - _sectorSize); + suppEncoding); svDesc.VolumeIdentifier = _buildParams.VolumeIdentifier; - SupplementaryVolumeDescriptorRegion svdr = new SupplementaryVolumeDescriptorRegion(svDesc, focus, _sectorSize); + SupplementaryVolumeDescriptorRegion svdr = new SupplementaryVolumeDescriptorRegion(svDesc, focus); fixedRegions.Insert(regionIdx++, svdr); - focus += _buildParams.SectorSize; + focus += IsoUtilities.SectorSize; - VolumeDescriptorSetTerminator evDesc = new VolumeDescriptorSetTerminator(_sectorSize); - VolumeDescriptorSetTerminatorRegion evdr = new VolumeDescriptorSetTerminatorRegion(evDesc, focus, _sectorSize); + VolumeDescriptorSetTerminator evDesc = new VolumeDescriptorSetTerminator(); + VolumeDescriptorSetTerminatorRegion evdr = new VolumeDescriptorSetTerminatorRegion(evDesc, focus); fixedRegions.Insert(regionIdx++, evdr); return fixedRegions; @@ -491,7 +471,7 @@ namespace DiscUtils.Iso9660 if (createMissing) { // This directory doesn't exist, create it... - BuildDirectoryInfo di = new BuildDirectoryInfo(path[i], focus, _sectorSize); + BuildDirectoryInfo di = new BuildDirectoryInfo(path[i], focus); focus.Add(di); _dirs.Add(di); focus = di; diff --git a/DiscUtils/Iso9660/CDReader.cs b/DiscUtils/Iso9660/CDReader.cs index 82bfe41..8a544ca 100644 --- a/DiscUtils/Iso9660/CDReader.cs +++ b/DiscUtils/Iso9660/CDReader.cs @@ -37,9 +37,7 @@ namespace DiscUtils.Iso9660 /// The stream to read the ISO image from. /// Whether to read Joliet extensions. public CDReader(Stream data, bool joliet) - : base(new VfsCDReader(data, joliet, false, 2048)) - { - } + : base(new VfsCDReader(data, joliet, false)) {} /// /// Initializes a new instance of the CDReader class. @@ -48,27 +46,7 @@ namespace DiscUtils.Iso9660 /// Whether to read Joliet extensions. /// Hides version numbers (e.g. ";1") from the end of files. public CDReader(Stream data, bool joliet, bool hideVersions) - : base(new VfsCDReader(data, joliet, hideVersions, 2048)) { } - - - /// - /// Initializes a new instance of the CDReader class. - /// - /// The stream to read the ISO image from. - /// Whether to read Joliet extensions. - /// The size of a sector - public CDReader(Stream data, bool joliet, int sectorSize) - : base(new VfsCDReader(data, joliet, false, sectorSize)) { - } - - /// - /// Initializes a new instance of the CDReader class. - /// - /// The stream to read the ISO image from. - /// Whether to read Joliet extensions. - /// Hides version numbers (e.g. ";1") from the end of files. - public CDReader(Stream data, bool joliet, bool hideVersions, int sectorSize) - : base(new VfsCDReader(data, joliet, hideVersions, sectorSize)) {} + : base(new VfsCDReader(data, joliet, hideVersions)) {} /// /// Gets which of the Iso9660 variants is being used. @@ -196,18 +174,18 @@ namespace DiscUtils.Iso9660 /// /// The stream to inspect. /// true if the stream contains an ISO file system, else false. - public static bool Detect(Stream data, int sectorSize) + public static bool Detect(Stream data) { - byte[] buffer = new byte[sectorSize]; + byte[] buffer = new byte[IsoUtilities.SectorSize]; - if (data.Length < 0x8000 + sectorSize) + if (data.Length < 0x8000 + IsoUtilities.SectorSize) { return false; } data.Position = 0x8000; - int numRead = StreamUtilities.ReadMaximum(data, buffer, 0, sectorSize); - if (numRead != sectorSize) + int numRead = StreamUtilities.ReadMaximum(data, buffer, 0, IsoUtilities.SectorSize); + if (numRead != IsoUtilities.SectorSize) { return false; } diff --git a/DiscUtils/Iso9660/CommonVolumeDescriptor.cs b/DiscUtils/Iso9660/CommonVolumeDescriptor.cs index f4a4e0a..7ef3bf6 100644 --- a/DiscUtils/Iso9660/CommonVolumeDescriptor.cs +++ b/DiscUtils/Iso9660/CommonVolumeDescriptor.cs @@ -97,9 +97,8 @@ namespace DiscUtils.Iso9660 uint rootDirExtentLocation, uint rootDirDataLength, DateTime buildTime, - Encoding enc, - int sectorSize) - : base(type, version, sectorSize) + Encoding enc) + : base(type, version) { CharacterEncoding = enc; @@ -108,7 +107,7 @@ namespace DiscUtils.Iso9660 VolumeSpaceSize = volumeSpaceSize; VolumeSetSize = 1; VolumeSequenceNumber = 1; - LogicalBlockSize = (ushort)sectorSize; + LogicalBlockSize = IsoUtilities.SectorSize; PathTableSize = pathTableSize; TypeLPathTableLocation = typeLPathTableLocation; ////OptionalTypeLPathTableLocation = 0; diff --git a/DiscUtils/Iso9660/DiscUtils.Iso9660.csproj b/DiscUtils/Iso9660/DiscUtils.Iso9660.csproj deleted file mode 100644 index e9968cf..0000000 --- a/DiscUtils/Iso9660/DiscUtils.Iso9660.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - - DiscUtils Iso9660 - Kenneth Bell;LordMike - DiscUtils;Optical;Iso9660 - - - - - - - diff --git a/DiscUtils/Iso9660/ExtentStream.cs b/DiscUtils/Iso9660/ExtentStream.cs index 13ec3c2..e7d5aa4 100644 --- a/DiscUtils/Iso9660/ExtentStream.cs +++ b/DiscUtils/Iso9660/ExtentStream.cs @@ -35,17 +35,15 @@ namespace DiscUtils.Iso9660 private long _position; private readonly uint _startBlock; - private readonly int _sectorSize; public ExtentStream(Stream isoStream, uint startBlock, uint dataLength, byte fileUnitSize, - byte interleaveGapSize, int sectorSize) + byte interleaveGapSize) { _isoStream = isoStream; _startBlock = startBlock; _dataLength = dataLength; _fileUnitSize = fileUnitSize; _interleaveGapSize = interleaveGapSize; - _sectorSize = sectorSize; if (_fileUnitSize != 0 || _interleaveGapSize != 0) { @@ -90,7 +88,7 @@ namespace DiscUtils.Iso9660 int toRead = (int)Math.Min((uint)count, _dataLength - _position); - _isoStream.Position = _position + _startBlock * (long)_sectorSize; + _isoStream.Position = _position + _startBlock * (long)IsoUtilities.SectorSize; int numRead = _isoStream.Read(buffer, offset, toRead); _position += numRead; return numRead; diff --git a/DiscUtils/Iso9660/File.cs b/DiscUtils/Iso9660/File.cs index 81bbad4..0ca134a 100644 --- a/DiscUtils/Iso9660/File.cs +++ b/DiscUtils/Iso9660/File.cs @@ -111,7 +111,7 @@ namespace DiscUtils.Iso9660 get { ExtentStream es = new ExtentStream(_context.DataStream, _dirEntry.Record.LocationOfExtent, - _dirEntry.Record.DataLength, _dirEntry.Record.FileUnitSize, _dirEntry.Record.InterleaveGapSize, _context.SectorSize); + _dirEntry.Record.DataLength, _dirEntry.Record.FileUnitSize, _dirEntry.Record.InterleaveGapSize); return new StreamBuffer(es, Ownership.Dispose); } } diff --git a/DiscUtils/Iso9660/IsoContext.cs b/DiscUtils/Iso9660/IsoContext.cs index 5232f62..5ff6396 100644 --- a/DiscUtils/Iso9660/IsoContext.cs +++ b/DiscUtils/Iso9660/IsoContext.cs @@ -28,19 +28,12 @@ namespace DiscUtils.Iso9660 { internal class IsoContext : VfsContext { - public IsoContext(int sectorSize) - { - SectorSize = sectorSize; - } - public Stream DataStream { get; set; } public string RockRidgeIdentifier { get; set; } public bool SuspDetected { get; set; } - public int SectorSize { get; set; } - public List SuspExtensions { get; set; } public int SuspSkipBytes { get; set; } diff --git a/DiscUtils/Iso9660/IsoUtilities.cs b/DiscUtils/Iso9660/IsoUtilities.cs index 4327450..ac6e552 100644 --- a/DiscUtils/Iso9660/IsoUtilities.cs +++ b/DiscUtils/Iso9660/IsoUtilities.cs @@ -30,7 +30,7 @@ namespace DiscUtils.Iso9660 { internal static class IsoUtilities { - //public const int SectorSize = 2048; + public const int SectorSize = 2048; public static uint ToUInt32FromBoth(byte[] data, int offset) { diff --git a/DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs b/DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs index 565c2ea..1d3199d 100644 --- a/DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs +++ b/DiscUtils/Iso9660/PrimaryVolumeDescriptor.cs @@ -38,11 +38,10 @@ namespace DiscUtils.Iso9660 uint typeMPathTableLocation, uint rootDirExtentLocation, uint rootDirDataLength, - DateTime buildTime, - int sectorSize) + DateTime buildTime) : base( VolumeDescriptorType.Primary, 1, volumeSpaceSize, pathTableSize, typeLPathTableLocation, - typeMPathTableLocation, rootDirExtentLocation, rootDirDataLength, buildTime, Encoding.ASCII, sectorSize) {} + typeMPathTableLocation, rootDirExtentLocation, rootDirDataLength, buildTime, Encoding.ASCII) {} internal override void WriteTo(byte[] buffer, int offset) { diff --git a/DiscUtils/Iso9660/PrimaryVolumeDescriptorRegion.cs b/DiscUtils/Iso9660/PrimaryVolumeDescriptorRegion.cs index dcc2125..0f0ae5d 100644 --- a/DiscUtils/Iso9660/PrimaryVolumeDescriptorRegion.cs +++ b/DiscUtils/Iso9660/PrimaryVolumeDescriptorRegion.cs @@ -26,15 +26,15 @@ namespace DiscUtils.Iso9660 { private readonly PrimaryVolumeDescriptor _descriptor; - public PrimaryVolumeDescriptorRegion(PrimaryVolumeDescriptor descriptor, long start, int sectorSize) - : base(start, sectorSize) + public PrimaryVolumeDescriptorRegion(PrimaryVolumeDescriptor descriptor, long start) + : base(start) { _descriptor = descriptor; } protected override byte[] GetBlockData() { - byte[] buffer = new byte[Length]; + byte[] buffer = new byte[IsoUtilities.SectorSize]; _descriptor.WriteTo(buffer, 0); return buffer; } diff --git a/DiscUtils/Iso9660/ReaderDirectory.cs b/DiscUtils/Iso9660/ReaderDirectory.cs index 003436b..d88d312 100644 --- a/DiscUtils/Iso9660/ReaderDirectory.cs +++ b/DiscUtils/Iso9660/ReaderDirectory.cs @@ -37,8 +37,8 @@ namespace DiscUtils.Iso9660 public ReaderDirectory(IsoContext context, ReaderDirEntry dirEntry) : base(context, dirEntry) { - byte[] buffer = new byte[context.SectorSize]; - Stream extent = new ExtentStream(_context.DataStream, dirEntry.Record.LocationOfExtent, uint.MaxValue, 0, 0, context.SectorSize); + byte[] buffer = new byte[IsoUtilities.SectorSize]; + Stream extent = new ExtentStream(_context.DataStream, dirEntry.Record.LocationOfExtent, uint.MaxValue, 0, 0); _records = new List(); @@ -48,8 +48,6 @@ namespace DiscUtils.Iso9660 { int bytesRead = (int)Math.Min(buffer.Length, totalLength - totalRead); - extent.Seek(24, SeekOrigin.Current); - StreamUtilities.ReadExact(extent, buffer, 0, bytesRead); totalRead += (uint)bytesRead; diff --git a/DiscUtils/Iso9660/SupplementaryVolumeDescriptor.cs b/DiscUtils/Iso9660/SupplementaryVolumeDescriptor.cs index bb7c6ba..dfe5ebb 100644 --- a/DiscUtils/Iso9660/SupplementaryVolumeDescriptor.cs +++ b/DiscUtils/Iso9660/SupplementaryVolumeDescriptor.cs @@ -39,11 +39,10 @@ namespace DiscUtils.Iso9660 uint rootDirExtentLocation, uint rootDirDataLength, DateTime buildTime, - Encoding enc, - int sectorSize) + Encoding enc) : base( VolumeDescriptorType.Supplementary, 1, volumeSpaceSize, pathTableSize, typeLPathTableLocation, - typeMPathTableLocation, rootDirExtentLocation, rootDirDataLength, buildTime, enc, sectorSize) {} + typeMPathTableLocation, rootDirExtentLocation, rootDirDataLength, buildTime, enc) {} internal override void WriteTo(byte[] buffer, int offset) { diff --git a/DiscUtils/Iso9660/SupplementaryVolumeDescriptorRegion.cs b/DiscUtils/Iso9660/SupplementaryVolumeDescriptorRegion.cs index 07e756a..317ea7f 100644 --- a/DiscUtils/Iso9660/SupplementaryVolumeDescriptorRegion.cs +++ b/DiscUtils/Iso9660/SupplementaryVolumeDescriptorRegion.cs @@ -26,15 +26,15 @@ namespace DiscUtils.Iso9660 { private readonly SupplementaryVolumeDescriptor _descriptor; - public SupplementaryVolumeDescriptorRegion(SupplementaryVolumeDescriptor descriptor, long start, int sectorSize) - : base(start, sectorSize) + public SupplementaryVolumeDescriptorRegion(SupplementaryVolumeDescriptor descriptor, long start) + : base(start) { _descriptor = descriptor; } protected override byte[] GetBlockData() { - byte[] buffer = new byte[Length]; + byte[] buffer = new byte[IsoUtilities.SectorSize]; _descriptor.WriteTo(buffer, 0); return buffer; } diff --git a/DiscUtils/Iso9660/VfsCDReader.cs b/DiscUtils/Iso9660/VfsCDReader.cs index f9207ed..9c628f2 100644 --- a/DiscUtils/Iso9660/VfsCDReader.cs +++ b/DiscUtils/Iso9660/VfsCDReader.cs @@ -40,7 +40,6 @@ namespace DiscUtils.Iso9660 private readonly Stream _data; private readonly bool _hideVersions; - protected readonly int _sectorSize; /// /// Initializes a new instance of the VfsCDReader class. @@ -48,9 +47,8 @@ namespace DiscUtils.Iso9660 /// The stream to read the ISO image from. /// Whether to read Joliet extensions. /// Hides version numbers (e.g. ";1") from the end of files. - public VfsCDReader(Stream data, bool joliet, bool hideVersions, int sectorSize) - : this(data, joliet ? DefaultVariantsWithJoliet : DefaultVariantsNoJoliet, hideVersions, sectorSize) { - } + public VfsCDReader(Stream data, bool joliet, bool hideVersions) + : this(data, joliet ? DefaultVariantsWithJoliet : DefaultVariantsNoJoliet, hideVersions) {} /// /// Initializes a new instance of the VfsCDReader class. @@ -70,16 +68,15 @@ namespace DiscUtils.Iso9660 /// The Iso9660 variant should normally be specified as the final entry in the list. Placing it earlier /// in the list will effectively mask later items and not including it may prevent some ISOs from being read. /// - public VfsCDReader(Stream data, Iso9660Variant[] variantPriorities, bool hideVersions, int sectorSize) + public VfsCDReader(Stream data, Iso9660Variant[] variantPriorities, bool hideVersions) : base(new DiscFileSystemOptions()) { _data = data; - _sectorSize = sectorSize; _hideVersions = hideVersions; - long vdpos = _sectorSize * 16; // Skip lead-in + long vdpos = 0x8000; // Skip lead-in - byte[] buffer = new byte[_sectorSize]; + byte[] buffer = new byte[IsoUtilities.SectorSize]; long pvdPos = 0; long svdPos = 0; @@ -88,15 +85,13 @@ namespace DiscUtils.Iso9660 do { data.Position = vdpos; - int numRead = data.Read(buffer, 0, _sectorSize); - if (numRead != _sectorSize) + int numRead = data.Read(buffer, 0, IsoUtilities.SectorSize); + if (numRead != IsoUtilities.SectorSize) { break; } - var offset = 24; - - bvd = new BaseVolumeDescriptor(buffer, offset); + bvd = new BaseVolumeDescriptor(buffer, 0); if (bvd.StandardIdentifier != BaseVolumeDescriptor.Iso9660StandardIdentifier) { @@ -106,7 +101,7 @@ namespace DiscUtils.Iso9660 switch (bvd.VolumeDescriptorType) { case VolumeDescriptorType.Boot: - _bootVolDesc = new BootVolumeDescriptor(buffer, offset); + _bootVolDesc = new BootVolumeDescriptor(buffer, 0); if (_bootVolDesc.SystemId != BootVolumeDescriptor.ElToritoSystemIdentifier) { _bootVolDesc = null; @@ -128,7 +123,7 @@ namespace DiscUtils.Iso9660 break; } - vdpos += _sectorSize; + vdpos += IsoUtilities.SectorSize; } while (bvd.VolumeDescriptorType != VolumeDescriptorType.SetTerminator); ActiveVariant = Iso9660Variant.None; @@ -140,10 +135,10 @@ namespace DiscUtils.Iso9660 if (svdPos != 0) { data.Position = svdPos; - data.Read(buffer, 0, _sectorSize); + data.Read(buffer, 0, IsoUtilities.SectorSize); SupplementaryVolumeDescriptor volDesc = new SupplementaryVolumeDescriptor(buffer, 0); - Context = new IsoContext(_sectorSize) { VolumeDescriptor = volDesc, DataStream = _data }; + Context = new IsoContext { VolumeDescriptor = volDesc, DataStream = _data }; RootDirectory = new ReaderDirectory(Context, new ReaderDirEntry(Context, volDesc.RootDirectory)); ActiveVariant = Iso9660Variant.Iso9660; @@ -155,13 +150,11 @@ namespace DiscUtils.Iso9660 case Iso9660Variant.Iso9660: if (pvdPos != 0) { - data.Position = pvdPos + 24; - data.Read(buffer, 0, _sectorSize); + data.Position = pvdPos; + data.Read(buffer, 0, IsoUtilities.SectorSize); PrimaryVolumeDescriptor volDesc = new PrimaryVolumeDescriptor(buffer, 0); - volDesc.LogicalBlockSize = 2352; - - IsoContext context = new IsoContext(_sectorSize) { VolumeDescriptor = volDesc, DataStream = _data }; + IsoContext context = new IsoContext { VolumeDescriptor = volDesc, DataStream = _data }; DirectoryRecord rootSelfRecord = ReadRootSelfRecord(context); InitializeSusp(context, rootSelfRecord); @@ -215,7 +208,7 @@ namespace DiscUtils.Iso9660 BootInitialEntry initialEntry = GetBootInitialEntry(); if (initialEntry != null) { - return initialEntry.ImageStart * _sectorSize; + return initialEntry.ImageStart * IsoUtilities.SectorSize; } return 0; } @@ -273,7 +266,7 @@ namespace DiscUtils.Iso9660 public long ClusterSize { - get { return _sectorSize; } + get { return IsoUtilities.SectorSize; } } public long TotalClusters @@ -331,7 +324,7 @@ namespace DiscUtils.Iso9660 return new[] { new Range(entry.Record.LocationOfExtent, - MathUtilities.Ceil(entry.Record.DataLength, _sectorSize)) + MathUtilities.Ceil(entry.Record.DataLength, IsoUtilities.SectorSize)) }; } @@ -349,7 +342,7 @@ namespace DiscUtils.Iso9660 } return new[] - { new StreamExtent(entry.Record.LocationOfExtent * _sectorSize, entry.Record.DataLength) }; + { new StreamExtent(entry.Record.LocationOfExtent * IsoUtilities.SectorSize, entry.Record.DataLength) }; } public ClusterMap BuildClusterMap() @@ -386,7 +379,7 @@ namespace DiscUtils.Iso9660 throw new NotSupportedException("Non-contiguous extents not supported"); } - long clusters = MathUtilities.Ceil(entry.Record.DataLength, _sectorSize); + long clusters = MathUtilities.Ceil(entry.Record.DataLength, IsoUtilities.SectorSize); for (long i = 0; i < clusters; ++i) { clusterToRole[i + entry.Record.LocationOfExtent] = ClusterRoles.DataFile; @@ -408,7 +401,7 @@ namespace DiscUtils.Iso9660 BootInitialEntry initialEntry = GetBootInitialEntry(); if (initialEntry != null) { - return new SubStream(_data, initialEntry.ImageStart * _sectorSize, + return new SubStream(_data, initialEntry.ImageStart * IsoUtilities.SectorSize, initialEntry.SectorCount * Sizes.Sector); } throw new InvalidOperationException("No valid boot image"); @@ -491,7 +484,7 @@ namespace DiscUtils.Iso9660 private static DirectoryRecord ReadRootSelfRecord(IsoContext context) { context.DataStream.Position = context.VolumeDescriptor.RootDirectory.LocationOfExtent * - context.VolumeDescriptor.LogicalBlockSize + 24; + context.VolumeDescriptor.LogicalBlockSize; byte[] firstSector = StreamUtilities.ReadExact(context.DataStream, context.VolumeDescriptor.LogicalBlockSize); DirectoryRecord rootSelfRecord; @@ -520,8 +513,8 @@ namespace DiscUtils.Iso9660 { if (_bootCatalog == null && _bootVolDesc != null) { - _data.Position = _bootVolDesc.CatalogSector * _sectorSize; - _bootCatalog = StreamUtilities.ReadExact(_data, _sectorSize); + _data.Position = _bootVolDesc.CatalogSector * IsoUtilities.SectorSize; + _bootCatalog = StreamUtilities.ReadExact(_data, IsoUtilities.SectorSize); } return _bootCatalog; diff --git a/DiscUtils/Iso9660/VolumeDescriptorDiskRegion.cs b/DiscUtils/Iso9660/VolumeDescriptorDiskRegion.cs index ad89652..11546f6 100644 --- a/DiscUtils/Iso9660/VolumeDescriptorDiskRegion.cs +++ b/DiscUtils/Iso9660/VolumeDescriptorDiskRegion.cs @@ -29,8 +29,8 @@ namespace DiscUtils.Iso9660 { private byte[] _readCache; - public VolumeDescriptorDiskRegion(long start, int sectorSize) - : base(start, sectorSize) {} + public VolumeDescriptorDiskRegion(long start) + : base(start, IsoUtilities.SectorSize) {} public override void Dispose() {} diff --git a/DiscUtils/Iso9660/VolumeDescriptorSetTerminator.cs b/DiscUtils/Iso9660/VolumeDescriptorSetTerminator.cs index fb247d2..f7035ff 100644 --- a/DiscUtils/Iso9660/VolumeDescriptorSetTerminator.cs +++ b/DiscUtils/Iso9660/VolumeDescriptorSetTerminator.cs @@ -24,7 +24,7 @@ namespace DiscUtils.Iso9660 { internal class VolumeDescriptorSetTerminator : BaseVolumeDescriptor { - public VolumeDescriptorSetTerminator(int sectorSize) - : base(VolumeDescriptorType.SetTerminator, 1, sectorSize) {} + public VolumeDescriptorSetTerminator() + : base(VolumeDescriptorType.SetTerminator, 1) {} } } \ No newline at end of file diff --git a/DiscUtils/Iso9660/VolumeDescriptorSetTerminatorRegion.cs b/DiscUtils/Iso9660/VolumeDescriptorSetTerminatorRegion.cs index 743bfc4..f1e98d6 100644 --- a/DiscUtils/Iso9660/VolumeDescriptorSetTerminatorRegion.cs +++ b/DiscUtils/Iso9660/VolumeDescriptorSetTerminatorRegion.cs @@ -26,15 +26,15 @@ namespace DiscUtils.Iso9660 { private readonly VolumeDescriptorSetTerminator _descriptor; - public VolumeDescriptorSetTerminatorRegion(VolumeDescriptorSetTerminator descriptor, long start, int sectorSize) - : base(start, sectorSize) + public VolumeDescriptorSetTerminatorRegion(VolumeDescriptorSetTerminator descriptor, long start) + : base(start) { _descriptor = descriptor; } protected override byte[] GetBlockData() { - byte[] buffer = new byte[Length]; + byte[] buffer = new byte[IsoUtilities.SectorSize]; _descriptor.WriteTo(buffer, 0); return buffer; } diff --git a/DiscUtils/Iso9660Ps1/BaseVolumeDescriptor.cs b/DiscUtils/Iso9660Ps1/BaseVolumeDescriptor.cs new file mode 100644 index 0000000..f1f4feb --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BaseVolumeDescriptor.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class BaseVolumeDescriptor + { + public const string Iso9660StandardIdentifier = "CD001"; + + public readonly string StandardIdentifier; + public readonly VolumeDescriptorType VolumeDescriptorType; + public readonly byte VolumeDescriptorVersion; + private int SectorSize; + + public BaseVolumeDescriptor(VolumeDescriptorType type, byte version, int sectorSize) + { + VolumeDescriptorType = type; + StandardIdentifier = "CD001"; + VolumeDescriptorVersion = version; + SectorSize = sectorSize; + } + + public BaseVolumeDescriptor(byte[] src, int offset) + { + VolumeDescriptorType = (VolumeDescriptorType)src[offset + 0]; + StandardIdentifier = Encoding.ASCII.GetString(src, offset + 1, 5); + VolumeDescriptorVersion = src[offset + 6]; + } + + internal virtual void WriteTo(byte[] buffer, int offset) + { + Array.Clear(buffer, offset, SectorSize); + buffer[offset] = (byte)VolumeDescriptorType; + IsoUtilities.WriteAChars(buffer, offset + 1, 5, StandardIdentifier); + buffer[offset + 6] = VolumeDescriptorVersion; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/BootDeviceEmulation.cs b/DiscUtils/Iso9660Ps1/BootDeviceEmulation.cs new file mode 100644 index 0000000..553e468 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BootDeviceEmulation.cs @@ -0,0 +1,55 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + /// + /// Enumeration of boot device emulation modes. + /// + public enum BootDeviceEmulation : byte + { + /// + /// No emulation, the boot image is just loaded and executed. + /// + NoEmulation = 0x0, + + /// + /// Emulates 1.2MB diskette image as drive A. + /// + Diskette1200KiB = 0x1, + + /// + /// Emulates 1.44MB diskette image as drive A. + /// + Diskette1440KiB = 0x2, + + /// + /// Emulates 2.88MB diskette image as drive A. + /// + Diskette2880KiB = 0x3, + + /// + /// Emulates hard disk image as drive C. + /// + HardDisk = 0x4 + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/BootInitialEntry.cs b/DiscUtils/Iso9660Ps1/BootInitialEntry.cs new file mode 100644 index 0000000..0d09fba --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BootInitialEntry.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class BootInitialEntry + { + public byte BootIndicator; + public BootDeviceEmulation BootMediaType; + public uint ImageStart; + public ushort LoadSegment; + public ushort SectorCount; + public byte SystemType; + + public BootInitialEntry() {} + + public BootInitialEntry(byte[] buffer, int offset) + { + BootIndicator = buffer[offset + 0x00]; + BootMediaType = (BootDeviceEmulation)buffer[offset + 0x01]; + LoadSegment = EndianUtilities.ToUInt16LittleEndian(buffer, offset + 0x02); + SystemType = buffer[offset + 0x04]; + SectorCount = EndianUtilities.ToUInt16LittleEndian(buffer, offset + 0x06); + ImageStart = EndianUtilities.ToUInt32LittleEndian(buffer, offset + 0x08); + } + + internal void WriteTo(byte[] buffer, int offset) + { + Array.Clear(buffer, offset, 0x20); + buffer[offset + 0x00] = BootIndicator; + buffer[offset + 0x01] = (byte)BootMediaType; + EndianUtilities.WriteBytesLittleEndian(LoadSegment, buffer, offset + 0x02); + buffer[offset + 0x04] = SystemType; + EndianUtilities.WriteBytesLittleEndian(SectorCount, buffer, offset + 0x06); + EndianUtilities.WriteBytesLittleEndian(ImageStart, buffer, offset + 0x08); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/BootValidationEntry.cs b/DiscUtils/Iso9660Ps1/BootValidationEntry.cs new file mode 100644 index 0000000..505c6b4 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BootValidationEntry.cs @@ -0,0 +1,88 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class BootValidationEntry + { + private readonly byte[] _data; + public byte HeaderId; + public string ManfId; + public byte PlatformId; + + public BootValidationEntry() + { + HeaderId = 1; + PlatformId = 0; + ManfId = ".Net DiscUtils"; + } + + public BootValidationEntry(byte[] src, int offset) + { + _data = new byte[32]; + Array.Copy(src, offset, _data, 0, 32); + + HeaderId = _data[0]; + PlatformId = _data[1]; + ManfId = EndianUtilities.BytesToString(_data, 4, 24).TrimEnd('\0').TrimEnd(' '); + } + + public bool ChecksumValid + { + get + { + ushort total = 0; + for (int i = 0; i < 16; ++i) + { + total += EndianUtilities.ToUInt16LittleEndian(_data, i * 2); + } + + return total == 0; + } + } + + internal void WriteTo(byte[] buffer, int offset) + { + Array.Clear(buffer, offset, 0x20); + buffer[offset + 0x00] = HeaderId; + buffer[offset + 0x01] = PlatformId; + EndianUtilities.StringToBytes(ManfId, buffer, offset + 0x04, 24); + buffer[offset + 0x1E] = 0x55; + buffer[offset + 0x1F] = 0xAA; + EndianUtilities.WriteBytesLittleEndian(CalcChecksum(buffer, offset), buffer, offset + 0x1C); + } + + private static ushort CalcChecksum(byte[] buffer, int offset) + { + ushort total = 0; + for (int i = 0; i < 16; ++i) + { + total += EndianUtilities.ToUInt16LittleEndian(buffer, offset + i * 2); + } + + return (ushort)(0 - total); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/BootVolumeDescriptor.cs b/DiscUtils/Iso9660Ps1/BootVolumeDescriptor.cs new file mode 100644 index 0000000..87f786a --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BootVolumeDescriptor.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class BootVolumeDescriptor : BaseVolumeDescriptor + { + public const string ElToritoSystemIdentifier = "EL TORITO SPECIFICATION"; + + public BootVolumeDescriptor(uint catalogSector, int sectorSize) + : base(VolumeDescriptorType.Boot, 1, sectorSize) + { + CatalogSector = catalogSector; + } + + public BootVolumeDescriptor(byte[] src, int offset) + : base(src, offset) + { + SystemId = EndianUtilities.BytesToString(src, offset + 0x7, 0x20).TrimEnd('\0'); + CatalogSector = EndianUtilities.ToUInt32LittleEndian(src, offset + 0x47); + } + + public uint CatalogSector { get; } + + public string SystemId { get; } + + internal override void WriteTo(byte[] buffer, int offset) + { + base.WriteTo(buffer, offset); + + EndianUtilities.StringToBytes(ElToritoSystemIdentifier, buffer, offset + 7, 0x20); + EndianUtilities.WriteBytesLittleEndian(CatalogSector, buffer, offset + 0x47); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/BootVolumeDescriptorRegion.cs b/DiscUtils/Iso9660Ps1/BootVolumeDescriptorRegion.cs new file mode 100644 index 0000000..61e1670 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BootVolumeDescriptorRegion.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal class BootVolumeDescriptorRegion : VolumeDescriptorDiskRegion + { + private readonly BootVolumeDescriptor _descriptor; + + public BootVolumeDescriptorRegion(BootVolumeDescriptor descriptor, long start, int sectorSize) + : base(start, sectorSize) + { + _descriptor = descriptor; + } + + protected override byte[] GetBlockData() + { + byte[] buffer = new byte[Length]; + _descriptor.WriteTo(buffer, 0); + return buffer; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/BuildDirectoryInfo.cs b/DiscUtils/Iso9660Ps1/BuildDirectoryInfo.cs new file mode 100644 index 0000000..f021e83 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BuildDirectoryInfo.cs @@ -0,0 +1,219 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.Text; +using DiscUtils.CoreCompat; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + /// + /// Represents a directory that will be built into the ISO image. + /// + public sealed class BuildDirectoryInfo : BuildDirectoryMember + { + internal static readonly Comparer PathTableSortComparison = new PathTableComparison(); + private readonly Dictionary _members; + + private readonly BuildDirectoryInfo _parent; + private List _sortedMembers; + private int _sectorSize; + + internal BuildDirectoryInfo(string name, BuildDirectoryInfo parent, int sectorSize) + : base(name, MakeShortDirName(name, parent)) + { + _parent = parent == null ? this : parent; + HierarchyDepth = parent == null ? 0 : parent.HierarchyDepth + 1; + _members = new Dictionary(); + _sectorSize = sectorSize; + } + + internal int HierarchyDepth { get; } + + /// + /// The parent directory, or null if none. + /// + public override BuildDirectoryInfo Parent + { + get { return _parent; } + } + + /// + /// Gets the specified child directory or file. + /// + /// The name of the file or directory to get. + /// The member found (or null). + /// true if the specified member was found. + internal bool TryGetMember(string name, out BuildDirectoryMember member) + { + return _members.TryGetValue(name, out member); + } + + internal void Add(BuildDirectoryMember member) + { + _members.Add(member.Name, member); + _sortedMembers = null; + } + + internal override long GetDataSize(Encoding enc) + { + List sorted = GetSortedMembers(); + + long total = 34 * 2; // Two pseudo entries (self & parent) + + foreach (BuildDirectoryMember m in sorted) + { + uint recordSize = m.GetDirectoryRecordSize(enc); + + // If this record would span a sector boundary, then the current sector is + // zero-padded, and the record goes at the start of the next sector. + if (total % _sectorSize + recordSize > _sectorSize) + { + long padLength = _sectorSize - total % _sectorSize; + total += padLength; + } + + total += recordSize; + } + + return MathUtilities.RoundUp(total, _sectorSize); + } + + internal uint GetPathTableEntrySize(Encoding enc) + { + int nameBytes = enc.GetByteCount(PickName(null, enc)); + + return (uint)(8 + nameBytes + ((nameBytes & 0x1) == 1 ? 1 : 0)); + } + + internal int Write(byte[] buffer, int offset, Dictionary locationTable, Encoding enc) + { + int pos = 0; + + List sorted = GetSortedMembers(); + + // Two pseudo entries, effectively '.' and '..' + pos += WriteMember(this, "\0", Encoding.ASCII, buffer, offset + pos, locationTable, enc, _sectorSize); + pos += WriteMember(_parent, "\x01", Encoding.ASCII, buffer, offset + pos, locationTable, enc, _sectorSize); + + foreach (BuildDirectoryMember m in sorted) + { + uint recordSize = m.GetDirectoryRecordSize(enc); + + if (pos % _sectorSize + recordSize > _sectorSize) + { + int padLength = _sectorSize - pos % _sectorSize; + Array.Clear(buffer, offset + pos, padLength); + pos += padLength; + } + + pos += WriteMember(m, null, enc, buffer, offset + pos, locationTable, enc, _sectorSize); + } + + // Ensure final padding data is zero'd + int finalPadLength = MathUtilities.RoundUp(pos, _sectorSize) - pos; + Array.Clear(buffer, offset + pos, finalPadLength); + + return pos + finalPadLength; + } + + private static int WriteMember(BuildDirectoryMember m, string nameOverride, Encoding nameEnc, byte[] buffer, int offset, Dictionary locationTable, Encoding dataEnc, int sectorSize) + { + DirectoryRecord dr = new DirectoryRecord(); + dr.FileIdentifier = m.PickName(nameOverride, nameEnc); + dr.LocationOfExtent = locationTable[m]; + dr.DataLength = (uint)m.GetDataSize(dataEnc); + dr.RecordingDateAndTime = m.CreationTime; + dr.Flags = m is BuildDirectoryInfo ? FileFlags.Directory : FileFlags.None; + return dr.WriteTo(buffer, offset, nameEnc); + } + + private static string MakeShortDirName(string longName, BuildDirectoryInfo dir) + { + if (IsoUtilities.IsValidDirectoryName(longName)) + { + return longName; + } + + char[] shortNameChars = longName.ToUpper(CultureInfo.InvariantCulture).ToCharArray(); + for (int i = 0; i < shortNameChars.Length; ++i) + { + if (!IsoUtilities.IsValidDChar(shortNameChars[i]) && shortNameChars[i] != '.' && shortNameChars[i] != ';') + { + shortNameChars[i] = '_'; + } + } + + return new string(shortNameChars); + } + + private List GetSortedMembers() + { + if (_sortedMembers == null) + { + List sorted = new List(_members.Values); + sorted.Sort(SortedComparison); + _sortedMembers = sorted; + } + + return _sortedMembers; + } + + private class PathTableComparison : Comparer + { + public override int Compare(BuildDirectoryInfo x, BuildDirectoryInfo y) + { + if (x.HierarchyDepth != y.HierarchyDepth) + { + return x.HierarchyDepth - y.HierarchyDepth; + } + + if (x.Parent != y.Parent) + { + return Compare(x.Parent, y.Parent); + } + + return CompareNames(x.Name, y.Name, ' '); + } + + private static int CompareNames(string x, string y, char padChar) + { + int max = Math.Max(x.Length, y.Length); + for (int i = 0; i < max; ++i) + { + char xChar = i < x.Length ? x[i] : padChar; + char yChar = i < y.Length ? y[i] : padChar; + + if (xChar != yChar) + { + return xChar - yChar; + } + } + + return 0; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/BuildDirectoryMember.cs b/DiscUtils/Iso9660Ps1/BuildDirectoryMember.cs new file mode 100644 index 0000000..fdf135b --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BuildDirectoryMember.cs @@ -0,0 +1,155 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660Ps1 +{ + /// + /// Provides the base class for and + /// objects that will be built into an + /// ISO image. + /// + /// Instances of this class have two names, a , + /// which is the full-length Joliet name and a , + /// which is the strictly compliant ISO 9660 name. + public abstract class BuildDirectoryMember + { + internal static readonly Comparer SortedComparison = new DirectorySortedComparison(); + + /// + /// Initializes a new instance of the BuildDirectoryMember class. + /// + /// The Joliet compliant name of the file or directory. + /// The ISO 9660 compliant name of the file or directory. + protected BuildDirectoryMember(string name, string shortName) + { + Name = name; + ShortName = shortName; + CreationTime = DateTime.UtcNow; + } + + /// + /// Gets or sets the creation date for the file or directory, in UTC. + /// + public DateTime CreationTime { get; set; } + + /// + /// Gets the Joliet compliant name of the file or directory. + /// + public string Name { get; } + + /// + /// Gets the parent directory, or null if this is the root directory. + /// + public abstract BuildDirectoryInfo Parent { get; } + + /// + /// Gets the ISO 9660 compliant name of the file or directory. + /// + public string ShortName { get; } + + internal string PickName(string nameOverride, Encoding enc) + { + if (nameOverride != null) + { + return nameOverride; + } + return enc == Encoding.ASCII ? ShortName : Name; + } + + internal abstract long GetDataSize(Encoding enc); + + internal uint GetDirectoryRecordSize(Encoding enc) + { + return DirectoryRecord.CalcLength(PickName(null, enc), enc); + } + + private class DirectorySortedComparison : Comparer + { + public override int Compare(BuildDirectoryMember x, BuildDirectoryMember y) + { + string[] xParts = x.Name.Split('.', ';'); + string[] yParts = y.Name.Split('.', ';'); + + string xPart; + string yPart; + + for (int i = 0; i < 2; ++i) + { + xPart = xParts.Length > i ? xParts[i] : string.Empty; + yPart = yParts.Length > i ? yParts[i] : string.Empty; + int val = ComparePart(xPart, yPart, ' '); + if (val != 0) + { + return val; + } + } + + xPart = xParts.Length > 2 ? xParts[2] : string.Empty; + yPart = yParts.Length > 2 ? yParts[2] : string.Empty; + return ComparePartBackwards(xPart, yPart, '0'); + } + + private static int ComparePart(string x, string y, char padChar) + { + int max = Math.Max(x.Length, y.Length); + for (int i = 0; i < max; ++i) + { + char xChar = i < x.Length ? x[i] : padChar; + char yChar = i < y.Length ? y[i] : padChar; + + if (xChar != yChar) + { + return xChar - yChar; + } + } + + return 0; + } + + private static int ComparePartBackwards(string x, string y, char padChar) + { + int max = Math.Max(x.Length, y.Length); + + int xPad = max - x.Length; + int yPad = max - y.Length; + + for (int i = 0; i < max; ++i) + { + char xChar = i >= xPad ? x[i - xPad] : padChar; + char yChar = i >= yPad ? y[i - yPad] : padChar; + + if (xChar != yChar) + { + // Note: Version numbers are in DESCENDING order! + return yChar - xChar; + } + } + + return 0; + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/BuildFileInfo.cs b/DiscUtils/Iso9660Ps1/BuildFileInfo.cs new file mode 100644 index 0000000..a826c2a --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BuildFileInfo.cs @@ -0,0 +1,136 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using System.Text; +using DiscUtils.CoreCompat; +using DiscUtils.Internal; + +namespace DiscUtils.Iso9660Ps1 +{ + /// + /// Represents a file that will be built into the ISO image. + /// + public sealed class BuildFileInfo : BuildDirectoryMember + { + private readonly byte[] _contentData; + private readonly string _contentPath; + private readonly long _contentSize; + private readonly Stream _contentStream; + + internal BuildFileInfo(string name, BuildDirectoryInfo parent, byte[] content) + : base(IsoUtilities.NormalizeFileName(name), MakeShortFileName(name, parent)) + { + Parent = parent; + _contentData = content; + _contentSize = content.Length; + } + + internal BuildFileInfo(string name, BuildDirectoryInfo parent, string content) + : base(IsoUtilities.NormalizeFileName(name), MakeShortFileName(name, parent)) + { + Parent = parent; + _contentPath = content; + _contentSize = new FileInfo(_contentPath).Length; + + CreationTime = new FileInfo(_contentPath).LastWriteTimeUtc; + } + + internal BuildFileInfo(string name, BuildDirectoryInfo parent, Stream source) + : base(IsoUtilities.NormalizeFileName(name), MakeShortFileName(name, parent)) + { + Parent = parent; + _contentStream = source; + _contentSize = _contentStream.Length; + } + + /// + /// The parent directory, or null if none. + /// + public override BuildDirectoryInfo Parent { get; } + + internal override long GetDataSize(Encoding enc) + { + return _contentSize; + } + + internal Stream OpenStream() + { + if (_contentData != null) + { + return new MemoryStream(_contentData, false); + } + if (_contentPath != null) + { + var locator = new LocalFileLocator(string.Empty); + return locator.Open(_contentPath, FileMode.Open, FileAccess.Read, FileShare.Read); + } + return _contentStream; + } + + internal void CloseStream(Stream s) + { + // Close and dispose the stream, unless it's one we were given to stream in + // from (we might need it again). + if (_contentStream != s) + { + s.Dispose(); + } + } + + private static string MakeShortFileName(string longName, BuildDirectoryInfo dir) + { + if (IsoUtilities.IsValidFileName(longName)) + { + return longName; + } + + char[] shortNameChars = longName.ToUpper(CultureInfo.InvariantCulture).ToCharArray(); + for (int i = 0; i < shortNameChars.Length; ++i) + { + if (!IsoUtilities.IsValidDChar(shortNameChars[i]) && shortNameChars[i] != '.' && shortNameChars[i] != ';') + { + shortNameChars[i] = '_'; + } + } + + string[] parts = IsoUtilities.SplitFileName(new string(shortNameChars)); + + if (parts[0].Length + parts[1].Length > 30) + { + parts[1] = parts[1].Substring(0, Math.Min(parts[1].Length, 3)); + } + + if (parts[0].Length + parts[1].Length > 30) + { + parts[0] = parts[0].Substring(0, 30 - parts[1].Length); + } + + string candidate = parts[0] + '.' + parts[1] + ';' + parts[2]; + + // TODO: Make unique + return candidate; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/BuildParameters.cs b/DiscUtils/Iso9660Ps1/BuildParameters.cs new file mode 100644 index 0000000..7f44cb7 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/BuildParameters.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal class BuildParameters + { + public BuildParameters() + { + VolumeIdentifier = string.Empty; + UseJoliet = true; + SectorSize = 2048; + } + + public int SectorSize { get; set; } + + public bool UseJoliet { get; set; } + + public string VolumeIdentifier { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/CDBuilder.cs b/DiscUtils/Iso9660Ps1/CDBuilder.cs new file mode 100644 index 0000000..b363661 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/CDBuilder.cs @@ -0,0 +1,518 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + /// + /// Class that creates ISO images. + /// + /// + /// + /// CDBuilder builder = new CDBuilder(); + /// builder.VolumeIdentifier = "MYISO"; + /// builder.UseJoliet = true; + /// builder.AddFile("Hello.txt", Encoding.ASCII.GetBytes("hello world!")); + /// builder.Build(@"C:\TEMP\myiso.iso"); + /// + /// + public sealed class CDBuilder : StreamBuilder + { + private const long DiskStart = 0x8000; + private BootInitialEntry _bootEntry; + private Stream _bootImage; + + private readonly BuildParameters _buildParams; + private readonly List _dirs; + + private readonly List _files; + private readonly BuildDirectoryInfo _rootDirectory; + private readonly int _sectorSize; + + /// + /// Initializes a new instance of the CDBuilder class. + /// + public CDBuilder() + { + _files = new List(); + _dirs = new List(); + _sectorSize = 2048; + _rootDirectory = new BuildDirectoryInfo("\0", null, _sectorSize); + _dirs.Add(_rootDirectory); + + _buildParams = new BuildParameters(); + _buildParams.UseJoliet = true; + } + + + /// + /// Initializes a new instance of the CDBuilder class. + /// + public CDBuilder(int sectorSize) + { + _files = new List(); + _dirs = new List(); + _sectorSize = sectorSize; + _rootDirectory = new BuildDirectoryInfo("\0", null, _sectorSize); + _dirs.Add(_rootDirectory); + + _buildParams = new BuildParameters(); + _buildParams.UseJoliet = true; + } + + /// + /// Gets or sets a value indicating whether to update the ISOLINUX info table at the + /// start of the boot image. Use with ISOLINUX only. + /// + /// + /// ISOLINUX has an 'information table' at the start of the boot loader that verifies + /// the CD has been loaded correctly by the BIOS. This table needs to be updated + /// to match the actual ISO. + /// + public bool UpdateIsolinuxBootTable { get; set; } + + /// + /// Gets or sets a value indicating whether Joliet file-system extensions should be used. + /// + public bool UseJoliet + { + get { return _buildParams.UseJoliet; } + set { _buildParams.UseJoliet = value; } + } + + /// + /// Gets or sets the Volume Identifier for the ISO file. + /// + /// + /// Must be a valid identifier, i.e. max 32 characters in the range A-Z, 0-9 or _. + /// Lower-case characters are not permitted. + /// + public string VolumeIdentifier + { + get { return _buildParams.VolumeIdentifier; } + + set + { + if (value.Length > 32) + { + throw new ArgumentException("Not a valid volume identifier"); + } + _buildParams.VolumeIdentifier = value; + } + } + + /// + /// Sets the boot image for the ISO image. + /// + /// Stream containing the boot image. + /// The type of emulation requested of the BIOS. + /// The memory segment to load the image to (0 for default). + public void SetBootImage(Stream image, BootDeviceEmulation emulation, int loadSegment) + { + if (_bootEntry != null) + { + throw new InvalidOperationException("Boot image already set"); + } + + _bootEntry = new BootInitialEntry(); + _bootEntry.BootIndicator = 0x88; + _bootEntry.BootMediaType = emulation; + _bootEntry.LoadSegment = (ushort)loadSegment; + _bootEntry.SystemType = 0; + _bootImage = image; + } + + /// + /// Adds a directory to the ISO image. + /// + /// The name of the directory on the ISO image. + /// The object representing this directory. + /// + /// The name is the full path to the directory, for example: + /// + /// builder.AddDirectory(@"DIRA\DIRB\DIRC"); + /// + /// + public BuildDirectoryInfo AddDirectory(string name) + { + string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + return GetDirectory(nameElements, nameElements.Length, true); + } + + /// + /// Adds a byte array to the ISO image as a file. + /// + /// The name of the file on the ISO image. + /// The contents of the file. + /// The object representing this file. + /// + /// The name is the full path to the file, for example: + /// + /// builder.AddFile(@"DIRA\DIRB\FILE.TXT;1", new byte[]{0,1,2}); + /// + /// Note the version number at the end of the file name is optional, if not + /// specified the default of 1 will be used. + /// + public BuildFileInfo AddFile(string name, byte[] content) + { + string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + BuildDirectoryInfo dir = GetDirectory(nameElements, nameElements.Length - 1, true); + + BuildDirectoryMember existing; + if (dir.TryGetMember(nameElements[nameElements.Length - 1], out existing)) + { + throw new IOException("File already exists"); + } + BuildFileInfo fi = new BuildFileInfo(nameElements[nameElements.Length - 1], dir, content); + _files.Add(fi); + dir.Add(fi); + return fi; + } + + /// + /// Adds a disk file to the ISO image as a file. + /// + /// The name of the file on the ISO image. + /// The name of the file on disk. + /// The object representing this file. + /// + /// The name is the full path to the file, for example: + /// + /// builder.AddFile(@"DIRA\DIRB\FILE.TXT;1", @"C:\temp\tempfile.bin"); + /// + /// Note the version number at the end of the file name is optional, if not + /// specified the default of 1 will be used. + /// + public BuildFileInfo AddFile(string name, string sourcePath) + { + string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + BuildDirectoryInfo dir = GetDirectory(nameElements, nameElements.Length - 1, true); + + BuildDirectoryMember existing; + if (dir.TryGetMember(nameElements[nameElements.Length - 1], out existing)) + { + throw new IOException("File already exists"); + } + BuildFileInfo fi = new BuildFileInfo(nameElements[nameElements.Length - 1], dir, sourcePath); + _files.Add(fi); + dir.Add(fi); + return fi; + } + + /// + /// Adds a stream to the ISO image as a file. + /// + /// The name of the file on the ISO image. + /// The contents of the file. + /// The object representing this file. + /// + /// The name is the full path to the file, for example: + /// + /// builder.AddFile(@"DIRA\DIRB\FILE.TXT;1", stream); + /// + /// Note the version number at the end of the file name is optional, if not + /// specified the default of 1 will be used. + /// + public BuildFileInfo AddFile(string name, Stream source) + { + if (!source.CanSeek) + { + throw new ArgumentException("source doesn't support seeking", nameof(source)); + } + + string[] nameElements = name.Split(new[] { '\\' }, StringSplitOptions.RemoveEmptyEntries); + BuildDirectoryInfo dir = GetDirectory(nameElements, nameElements.Length - 1, true); + + BuildDirectoryMember existing; + if (dir.TryGetMember(nameElements[nameElements.Length - 1], out existing)) + { + throw new IOException("File already exists"); + } + BuildFileInfo fi = new BuildFileInfo(nameElements[nameElements.Length - 1], dir, source); + _files.Add(fi); + dir.Add(fi); + return fi; + } + + protected override List FixExtents(out long totalLength) + { + List fixedRegions = new List(); + + DateTime buildTime = DateTime.UtcNow; + + Encoding suppEncoding = _buildParams.UseJoliet ? Encoding.BigEndianUnicode : Encoding.ASCII; + + Dictionary primaryLocationTable = new Dictionary(); + Dictionary supplementaryLocationTable = + new Dictionary(); + + long focus = DiskStart + 3 * _buildParams.SectorSize; // Primary, Supplementary, End (fixed at end...) + if (_bootEntry != null) + { + focus += _buildParams.SectorSize; + } + + // #################################################################### + // # 0. Fix boot image location + // #################################################################### + long bootCatalogPos = 0; + if (_bootEntry != null) + { + long bootImagePos = focus; + Stream realBootImage = PatchBootImage(_bootImage, (uint)(DiskStart / _buildParams.SectorSize), + (uint)(bootImagePos / _buildParams.SectorSize)); + BuilderStreamExtent bootImageExtent = new BuilderStreamExtent(focus, realBootImage); + fixedRegions.Add(bootImageExtent); + focus += MathUtilities.RoundUp(bootImageExtent.Length, _buildParams.SectorSize); + + bootCatalogPos = focus; + byte[] bootCatalog = new byte[_buildParams.SectorSize]; + BootValidationEntry bve = new BootValidationEntry(); + bve.WriteTo(bootCatalog, 0x00); + _bootEntry.ImageStart = (uint)MathUtilities.Ceil(bootImagePos, _buildParams.SectorSize); + _bootEntry.SectorCount = (ushort)MathUtilities.Ceil(_bootImage.Length, Sizes.Sector); + _bootEntry.WriteTo(bootCatalog, 0x20); + fixedRegions.Add(new BuilderBufferExtent(bootCatalogPos, bootCatalog)); + focus += _buildParams.SectorSize; + } + + // #################################################################### + // # 1. Fix file locations + // #################################################################### + + // Find end of the file data, fixing the files in place as we go + foreach (BuildFileInfo fi in _files) + { + primaryLocationTable.Add(fi, (uint)(focus / _buildParams.SectorSize)); + supplementaryLocationTable.Add(fi, (uint)(focus / _buildParams.SectorSize)); + FileExtent extent = new FileExtent(fi, focus); + + // Only remember files of non-zero length (otherwise we'll stomp on a valid file) + if (extent.Length != 0) + { + fixedRegions.Add(extent); + } + + focus += MathUtilities.RoundUp(extent.Length, _buildParams.SectorSize); + } + + // #################################################################### + // # 2. Fix directory locations + // #################################################################### + + // There are two directory tables + // 1. Primary (std ISO9660) + // 2. Supplementary (Joliet) + + // Find start of the second set of directory data, fixing ASCII directories in place. + long startOfFirstDirData = focus; + foreach (BuildDirectoryInfo di in _dirs) + { + primaryLocationTable.Add(di, (uint)(focus / _buildParams.SectorSize)); + DirectoryExtent extent = new DirectoryExtent(di, primaryLocationTable, Encoding.ASCII, focus); + fixedRegions.Add(extent); + focus += MathUtilities.RoundUp(extent.Length, _buildParams.SectorSize); + } + + // Find end of the second directory table, fixing supplementary directories in place. + long startOfSecondDirData = focus; + foreach (BuildDirectoryInfo di in _dirs) + { + supplementaryLocationTable.Add(di, (uint)(focus / _buildParams.SectorSize)); + DirectoryExtent extent = new DirectoryExtent(di, supplementaryLocationTable, suppEncoding, focus); + fixedRegions.Add(extent); + focus += MathUtilities.RoundUp(extent.Length, _buildParams.SectorSize); + } + + // #################################################################### + // # 3. Fix path tables + // #################################################################### + + // There are four path tables: + // 1. LE, ASCII + // 2. BE, ASCII + // 3. LE, Supp Encoding (Joliet) + // 4. BE, Supp Encoding (Joliet) + + // Find end of the path table + long startOfFirstPathTable = focus; + PathTable pathTable = new PathTable(false, Encoding.ASCII, _dirs, primaryLocationTable, focus); + fixedRegions.Add(pathTable); + focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + long primaryPathTableLength = pathTable.Length; + + long startOfSecondPathTable = focus; + pathTable = new PathTable(true, Encoding.ASCII, _dirs, primaryLocationTable, focus); + fixedRegions.Add(pathTable); + focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + + long startOfThirdPathTable = focus; + pathTable = new PathTable(false, suppEncoding, _dirs, supplementaryLocationTable, focus); + fixedRegions.Add(pathTable); + focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + long supplementaryPathTableLength = pathTable.Length; + + long startOfFourthPathTable = focus; + pathTable = new PathTable(true, suppEncoding, _dirs, supplementaryLocationTable, focus); + fixedRegions.Add(pathTable); + focus += MathUtilities.RoundUp(pathTable.Length, _buildParams.SectorSize); + + // Find the end of the disk + totalLength = focus; + + // #################################################################### + // # 4. Prepare volume descriptors now other structures are fixed + // #################################################################### + int regionIdx = 0; + focus = DiskStart; + PrimaryVolumeDescriptor pvDesc = new PrimaryVolumeDescriptor( + (uint)(totalLength / _buildParams.SectorSize), // VolumeSpaceSize + (uint)primaryPathTableLength, // PathTableSize + (uint)(startOfFirstPathTable / _buildParams.SectorSize), // TypeLPathTableLocation + (uint)(startOfSecondPathTable / _buildParams.SectorSize), // TypeMPathTableLocation + (uint)(startOfFirstDirData / _buildParams.SectorSize), // RootDirectory.LocationOfExtent + (uint)_rootDirectory.GetDataSize(Encoding.ASCII), // RootDirectory.DataLength + buildTime, + _sectorSize); + pvDesc.VolumeIdentifier = _buildParams.VolumeIdentifier; + PrimaryVolumeDescriptorRegion pvdr = new PrimaryVolumeDescriptorRegion(pvDesc, focus, _sectorSize); + fixedRegions.Insert(regionIdx++, pvdr); + focus += _buildParams.SectorSize; + + if (_bootEntry != null) + { + BootVolumeDescriptor bvDesc = new BootVolumeDescriptor( + (uint)(bootCatalogPos / _buildParams.SectorSize), _buildParams.SectorSize); + BootVolumeDescriptorRegion bvdr = new BootVolumeDescriptorRegion(bvDesc, focus, _buildParams.SectorSize); + fixedRegions.Insert(regionIdx++, bvdr); + focus += _buildParams.SectorSize; + } + + SupplementaryVolumeDescriptor svDesc = new SupplementaryVolumeDescriptor( + (uint)(totalLength / _buildParams.SectorSize), // VolumeSpaceSize + (uint)supplementaryPathTableLength, // PathTableSize + (uint)(startOfThirdPathTable / _buildParams.SectorSize), // TypeLPathTableLocation + (uint)(startOfFourthPathTable / _buildParams.SectorSize), // TypeMPathTableLocation + (uint)(startOfSecondDirData / _buildParams.SectorSize), // RootDirectory.LocationOfExtent + (uint)_rootDirectory.GetDataSize(suppEncoding), // RootDirectory.DataLength + buildTime, + suppEncoding, + _sectorSize); + svDesc.VolumeIdentifier = _buildParams.VolumeIdentifier; + SupplementaryVolumeDescriptorRegion svdr = new SupplementaryVolumeDescriptorRegion(svDesc, focus, _sectorSize); + fixedRegions.Insert(regionIdx++, svdr); + focus += _buildParams.SectorSize; + + VolumeDescriptorSetTerminator evDesc = new VolumeDescriptorSetTerminator(_sectorSize); + VolumeDescriptorSetTerminatorRegion evdr = new VolumeDescriptorSetTerminatorRegion(evDesc, focus, _sectorSize); + fixedRegions.Insert(regionIdx++, evdr); + + return fixedRegions; + } + + /// + /// Patches a boot image (esp. for ISOLINUX) before it is written to the disk. + /// + /// The original (master) boot image. + /// The logical block address of the primary volume descriptor. + /// The logical block address of the boot image itself. + /// A stream containing the patched boot image - does not need to be disposed. + private Stream PatchBootImage(Stream bootImage, uint pvdLba, uint bootImageLba) + { + // Early-exit if no patching to do... + if (!UpdateIsolinuxBootTable) + { + return bootImage; + } + + byte[] bootData = StreamUtilities.ReadExact(bootImage, (int)bootImage.Length); + + Array.Clear(bootData, 8, 56); + + uint checkSum = 0; + for (int i = 64; i < bootData.Length; i += 4) + { + checkSum += EndianUtilities.ToUInt32LittleEndian(bootData, i); + } + + EndianUtilities.WriteBytesLittleEndian(pvdLba, bootData, 8); + EndianUtilities.WriteBytesLittleEndian(bootImageLba, bootData, 12); + EndianUtilities.WriteBytesLittleEndian(bootData.Length, bootData, 16); + EndianUtilities.WriteBytesLittleEndian(checkSum, bootData, 20); + + return new MemoryStream(bootData, false); + } + + private BuildDirectoryInfo GetDirectory(string[] path, int pathLength, bool createMissing) + { + BuildDirectoryInfo di = TryGetDirectory(path, pathLength, createMissing); + + if (di == null) + { + throw new DirectoryNotFoundException("Directory not found"); + } + + return di; + } + + private BuildDirectoryInfo TryGetDirectory(string[] path, int pathLength, bool createMissing) + { + BuildDirectoryInfo focus = _rootDirectory; + + for (int i = 0; i < pathLength; ++i) + { + BuildDirectoryMember next; + if (!focus.TryGetMember(path[i], out next)) + { + if (createMissing) + { + // This directory doesn't exist, create it... + BuildDirectoryInfo di = new BuildDirectoryInfo(path[i], focus, _sectorSize); + focus.Add(di); + _dirs.Add(di); + focus = di; + } + else + { + return null; + } + } + else + { + BuildDirectoryInfo nextAsBuildDirectoryInfo = next as BuildDirectoryInfo; + if (nextAsBuildDirectoryInfo == null) + { + throw new IOException("File with conflicting name exists"); + } + focus = nextAsBuildDirectoryInfo; + } + } + + return focus; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/CDReader.cs b/DiscUtils/Iso9660Ps1/CDReader.cs new file mode 100644 index 0000000..033b3b6 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/CDReader.cs @@ -0,0 +1,229 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Streams; +using DiscUtils.Vfs; + +namespace DiscUtils.Iso9660Ps1 +{ + /// + /// Class for reading existing ISO images. + /// + public class CDReader : VfsFileSystemFacade, IClusterBasedFileSystem, IUnixFileSystem + { + /// + /// Initializes a new instance of the CDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + public CDReader(Stream data, bool joliet) + : base(new VfsCDReader(data, joliet, false, 2048)) + { + } + + /// + /// Initializes a new instance of the CDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + /// Hides version numbers (e.g. ";1") from the end of files. + public CDReader(Stream data, bool joliet, bool hideVersions) + : base(new VfsCDReader(data, joliet, hideVersions, 2048)) { } + + + + /// + /// Initializes a new instance of the CDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + public CDReader(Stream data, bool joliet, int sectorSize) + : base(new VfsCDReader(data, joliet, false, sectorSize)) { + } + + /// + /// Initializes a new instance of the CDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + /// Hides version numbers (e.g. ";1") from the end of files. + public CDReader(Stream data, bool joliet, bool hideVersions, int sectorSize) + : base(new VfsCDReader(data, joliet, hideVersions, sectorSize)) {} + + /// + /// Gets which of the Iso9660 variants is being used. + /// + public Iso9660Variant ActiveVariant + { + get { return GetRealFileSystem().ActiveVariant; } + } + + /// + /// Gets the emulation requested of BIOS when the image is loaded. + /// + public BootDeviceEmulation BootEmulation + { + get { return GetRealFileSystem().BootEmulation; } + } + + /// + /// Gets the absolute start position (in bytes) of the boot image, or zero if not found. + /// + public long BootImageStart + { + get { return GetRealFileSystem().BootImageStart; } + } + + /// + /// Gets the memory segment the image should be loaded into (0 for default). + /// + public int BootLoadSegment + { + get { return GetRealFileSystem().BootLoadSegment; } + } + + /// + /// Gets a value indicating whether a boot image is present. + /// + public bool HasBootImage + { + get { return GetRealFileSystem().HasBootImage; } + } + + /// + /// Gets the size (in bytes) of each cluster. + /// + public long ClusterSize + { + get { return GetRealFileSystem().ClusterSize; } + } + + /// + /// Gets the total number of clusters managed by the file system. + /// + public long TotalClusters + { + get { return GetRealFileSystem().TotalClusters; } + } + + /// + /// Converts a cluster (index) into an absolute byte position in the underlying stream. + /// + /// The cluster to convert. + /// The corresponding absolute byte position. + public long ClusterToOffset(long cluster) + { + return GetRealFileSystem().ClusterToOffset(cluster); + } + + /// + /// Converts an absolute byte position in the underlying stream to a cluster (index). + /// + /// The byte position to convert. + /// The cluster containing the specified byte. + public long OffsetToCluster(long offset) + { + return GetRealFileSystem().OffsetToCluster(offset); + } + + /// + /// Converts a file name to the list of clusters occupied by the file's data. + /// + /// The path to inspect. + /// The clusters. + /// Note that in some file systems, small files may not have dedicated + /// clusters. Only dedicated clusters will be returned. + public Range[] PathToClusters(string path) + { + return GetRealFileSystem().PathToClusters(path); + } + + /// + /// Converts a file name to the extents containing its data. + /// + /// The path to inspect. + /// The file extents, as absolute byte positions in the underlying stream. + /// Use this method with caution - not all file systems will store all bytes + /// directly in extents. Files may be compressed, sparse or encrypted. This method + /// merely indicates where file data is stored, not what's stored. + public StreamExtent[] PathToExtents(string path) + { + return GetRealFileSystem().PathToExtents(path); + } + + /// + /// Gets an object that can convert between clusters and files. + /// + /// The cluster map. + public ClusterMap BuildClusterMap() + { + return GetRealFileSystem().BuildClusterMap(); + } + + /// + /// Retrieves Unix-specific information about a file or directory. + /// + /// Path to the file or directory. + /// Information about the owner, group, permissions and type of the + /// file or directory. + public UnixFileSystemInfo GetUnixFileInfo(string path) + { + return GetRealFileSystem().GetUnixFileInfo(path); + } + + /// + /// Detects if a stream contains a valid ISO file system. + /// + /// The stream to inspect. + /// true if the stream contains an ISO file system, else false. + public static bool Detect(Stream data, int sectorSize) + { + byte[] buffer = new byte[sectorSize]; + + if (data.Length < 0x8000 + sectorSize) + { + return false; + } + + data.Position = 0x8000; + int numRead = StreamUtilities.ReadMaximum(data, buffer, 0, sectorSize); + if (numRead != sectorSize) + { + return false; + } + + BaseVolumeDescriptor bvd = new BaseVolumeDescriptor(buffer, 0); + + return bvd.StandardIdentifier == BaseVolumeDescriptor.Iso9660StandardIdentifier; + } + + /// + /// Opens a stream containing the boot image. + /// + /// The boot image as a stream. + public Stream OpenBootImage() + { + return GetRealFileSystem().OpenBootImage(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/CommonVolumeDescriptor.cs b/DiscUtils/Iso9660Ps1/CommonVolumeDescriptor.cs new file mode 100644 index 0000000..fb7a571 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/CommonVolumeDescriptor.cs @@ -0,0 +1,141 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Internal; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class CommonVolumeDescriptor : BaseVolumeDescriptor + { + public string AbstractFileIdentifier; + public string ApplicationIdentifier; + public string BibliographicFileIdentifier; + public Encoding CharacterEncoding; + public string CopyrightFileIdentifier; + public DateTime CreationDateAndTime; + public string DataPreparerIdentifier; + public DateTime EffectiveDateAndTime; + public DateTime ExpirationDateAndTime; + public byte FileStructureVersion; + public ushort LogicalBlockSize; + public DateTime ModificationDateAndTime; + public uint OptionalTypeLPathTableLocation; + public uint OptionalTypeMPathTableLocation; + public uint PathTableSize; + public string PublisherIdentifier; + public DirectoryRecord RootDirectory; + + public string SystemIdentifier; + public uint TypeLPathTableLocation; + public uint TypeMPathTableLocation; + public string VolumeIdentifier; + public ushort VolumeSequenceNumber; + public string VolumeSetIdentifier; + public ushort VolumeSetSize; + public uint VolumeSpaceSize; + + public CommonVolumeDescriptor(byte[] src, int offset, Encoding enc) + : base(src, offset) + { + CharacterEncoding = enc; + + SystemIdentifier = IsoUtilities.ReadChars(src, offset + 8, 32, CharacterEncoding); + VolumeIdentifier = IsoUtilities.ReadChars(src, offset + 40, 32, CharacterEncoding); + VolumeSpaceSize = IsoUtilities.ToUInt32FromBoth(src, offset + 80); + VolumeSetSize = IsoUtilities.ToUInt16FromBoth(src, offset + 120); + VolumeSequenceNumber = IsoUtilities.ToUInt16FromBoth(src, offset + 124); + LogicalBlockSize = IsoUtilities.ToUInt16FromBoth(src, offset + 128); + PathTableSize = IsoUtilities.ToUInt32FromBoth(src, offset + 132); + TypeLPathTableLocation = EndianUtilities.ToUInt32LittleEndian(src, offset + 140); + OptionalTypeLPathTableLocation = EndianUtilities.ToUInt32LittleEndian(src, offset + 144); + TypeMPathTableLocation = Utilities.BitSwap(EndianUtilities.ToUInt32LittleEndian(src, offset + 148)); + OptionalTypeMPathTableLocation = Utilities.BitSwap(EndianUtilities.ToUInt32LittleEndian(src, offset + 152)); + DirectoryRecord.ReadFrom(src, offset + 156, CharacterEncoding, out RootDirectory); + VolumeSetIdentifier = IsoUtilities.ReadChars(src, offset + 190, 318 - 190, CharacterEncoding); + PublisherIdentifier = IsoUtilities.ReadChars(src, offset + 318, 446 - 318, CharacterEncoding); + DataPreparerIdentifier = IsoUtilities.ReadChars(src, offset + 446, 574 - 446, CharacterEncoding); + ApplicationIdentifier = IsoUtilities.ReadChars(src, offset + 574, 702 - 574, CharacterEncoding); + CopyrightFileIdentifier = IsoUtilities.ReadChars(src, offset + 702, 739 - 702, CharacterEncoding); + AbstractFileIdentifier = IsoUtilities.ReadChars(src, offset + 739, 776 - 739, CharacterEncoding); + BibliographicFileIdentifier = IsoUtilities.ReadChars(src, offset + 776, 813 - 776, CharacterEncoding); + CreationDateAndTime = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(src, offset + 813); + ModificationDateAndTime = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(src, offset + 830); + ExpirationDateAndTime = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(src, offset + 847); + EffectiveDateAndTime = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(src, offset + 864); + FileStructureVersion = src[offset + 881]; + } + + public CommonVolumeDescriptor( + VolumeDescriptorType type, + byte version, + uint volumeSpaceSize, + uint pathTableSize, + uint typeLPathTableLocation, + uint typeMPathTableLocation, + uint rootDirExtentLocation, + uint rootDirDataLength, + DateTime buildTime, + Encoding enc, + int sectorSize) + : base(type, version, sectorSize) + { + CharacterEncoding = enc; + + SystemIdentifier = string.Empty; + VolumeIdentifier = string.Empty; + VolumeSpaceSize = volumeSpaceSize; + VolumeSetSize = 1; + VolumeSequenceNumber = 1; + LogicalBlockSize = (ushort)sectorSize; + PathTableSize = pathTableSize; + TypeLPathTableLocation = typeLPathTableLocation; + ////OptionalTypeLPathTableLocation = 0; + TypeMPathTableLocation = typeMPathTableLocation; + ////OptionalTypeMPathTableLocation = 0; + RootDirectory = new DirectoryRecord(); + RootDirectory.ExtendedAttributeRecordLength = 0; + RootDirectory.LocationOfExtent = rootDirExtentLocation; + RootDirectory.DataLength = rootDirDataLength; + RootDirectory.RecordingDateAndTime = buildTime; + RootDirectory.Flags = FileFlags.Directory; + RootDirectory.FileUnitSize = 0; + RootDirectory.InterleaveGapSize = 0; + RootDirectory.VolumeSequenceNumber = 1; + RootDirectory.FileIdentifier = "\0"; + VolumeSetIdentifier = string.Empty; + PublisherIdentifier = string.Empty; + DataPreparerIdentifier = string.Empty; + ApplicationIdentifier = string.Empty; + CopyrightFileIdentifier = string.Empty; + AbstractFileIdentifier = string.Empty; + BibliographicFileIdentifier = string.Empty; + CreationDateAndTime = buildTime; + ModificationDateAndTime = buildTime; + ExpirationDateAndTime = DateTime.MinValue; + EffectiveDateAndTime = buildTime; + FileStructureVersion = 1; // V1 + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/DirectoryExtent.cs b/DiscUtils/Iso9660Ps1/DirectoryExtent.cs new file mode 100644 index 0000000..3c3b1f2 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/DirectoryExtent.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class DirectoryExtent : BuilderExtent + { + private readonly BuildDirectoryInfo _dirInfo; + private readonly Encoding _enc; + private readonly Dictionary _locationTable; + + private byte[] _readCache; + + public DirectoryExtent(BuildDirectoryInfo dirInfo, Dictionary locationTable, + Encoding enc, long start) + : base(start, dirInfo.GetDataSize(enc)) + { + _dirInfo = dirInfo; + _locationTable = locationTable; + _enc = enc; + } + + public override void Dispose() {} + + public override void PrepareForRead() + { + _readCache = new byte[Length]; + _dirInfo.Write(_readCache, 0, _locationTable, _enc); + } + + public override int Read(long diskOffset, byte[] buffer, int offset, int count) + { + long relPos = diskOffset - Start; + + int numRead = (int)Math.Min(count, _readCache.Length - relPos); + + Array.Copy(_readCache, (int)relPos, buffer, offset, numRead); + + return numRead; + } + + public override void DisposeReadState() + { + _readCache = null; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/DirectoryRecord.cs b/DiscUtils/Iso9660Ps1/DirectoryRecord.cs new file mode 100644 index 0000000..fb6c9ad --- /dev/null +++ b/DiscUtils/Iso9660Ps1/DirectoryRecord.cs @@ -0,0 +1,114 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class DirectoryRecord + { + public uint DataLength; + public byte ExtendedAttributeRecordLength; + public string FileIdentifier; + public byte FileUnitSize; + public FileFlags Flags; + public byte InterleaveGapSize; + public uint LocationOfExtent; + public DateTime RecordingDateAndTime; + public byte[] SystemUseData; + public ushort VolumeSequenceNumber; + + public static int ReadFrom(byte[] src, int offset, Encoding enc, out DirectoryRecord record) + { + int length = src[offset + 0]; + + record = new DirectoryRecord(); + record.ExtendedAttributeRecordLength = src[offset + 1]; + record.LocationOfExtent = IsoUtilities.ToUInt32FromBoth(src, offset + 2); + record.DataLength = IsoUtilities.ToUInt32FromBoth(src, offset + 10); + record.RecordingDateAndTime = IsoUtilities.ToUTCDateTimeFromDirectoryTime(src, offset + 18); + record.Flags = (FileFlags)src[offset + 25]; + record.FileUnitSize = src[offset + 26]; + record.InterleaveGapSize = src[offset + 27]; + record.VolumeSequenceNumber = IsoUtilities.ToUInt16FromBoth(src, offset + 28); + byte lengthOfFileIdentifier = src[offset + 32]; + record.FileIdentifier = IsoUtilities.ReadChars(src, offset + 33, lengthOfFileIdentifier, enc); + + int padding = (lengthOfFileIdentifier & 1) == 0 ? 1 : 0; + int startSystemArea = lengthOfFileIdentifier + padding + 33; + int lenSystemArea = length - startSystemArea; + if (lenSystemArea > 0) + { + record.SystemUseData = new byte[lenSystemArea]; + Array.Copy(src, offset + startSystemArea, record.SystemUseData, 0, lenSystemArea); + } + + return length; + } + + public static uint CalcLength(string name, Encoding enc) + { + int nameBytes; + if (name.Length == 1 && name[0] <= 1) + { + nameBytes = 1; + } + else + { + nameBytes = enc.GetByteCount(name); + } + + return (uint)(33 + nameBytes + ((nameBytes & 0x1) == 0 ? 1 : 0)); + } + + internal int WriteTo(byte[] buffer, int offset, Encoding enc) + { + uint length = CalcLength(FileIdentifier, enc); + buffer[offset] = (byte)length; + buffer[offset + 1] = ExtendedAttributeRecordLength; + IsoUtilities.ToBothFromUInt32(buffer, offset + 2, LocationOfExtent); + IsoUtilities.ToBothFromUInt32(buffer, offset + 10, DataLength); + IsoUtilities.ToDirectoryTimeFromUTC(buffer, offset + 18, RecordingDateAndTime); + buffer[offset + 25] = (byte)Flags; + buffer[offset + 26] = FileUnitSize; + buffer[offset + 27] = InterleaveGapSize; + IsoUtilities.ToBothFromUInt16(buffer, offset + 28, VolumeSequenceNumber); + byte lengthOfFileIdentifier; + + if (FileIdentifier.Length == 1 && FileIdentifier[0] <= 1) + { + buffer[offset + 33] = (byte)FileIdentifier[0]; + lengthOfFileIdentifier = 1; + } + else + { + lengthOfFileIdentifier = + (byte) + IsoUtilities.WriteString(buffer, offset + 33, (int)(length - 33), false, FileIdentifier, enc); + } + + buffer[offset + 32] = lengthOfFileIdentifier; + return (int)length; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/ExtentStream.cs b/DiscUtils/Iso9660Ps1/ExtentStream.cs new file mode 100644 index 0000000..80e4880 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/ExtentStream.cs @@ -0,0 +1,125 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class ExtentStream : Stream + { + private readonly uint _dataLength; + private readonly byte _fileUnitSize; + private readonly byte _interleaveGapSize; + + private readonly Stream _isoStream; + private long _position; + + private readonly uint _startBlock; + private readonly int _sectorSize; + + public ExtentStream(Stream isoStream, uint startBlock, uint dataLength, byte fileUnitSize, + byte interleaveGapSize, int sectorSize) + { + _isoStream = isoStream; + _startBlock = startBlock; + _dataLength = dataLength; + _fileUnitSize = fileUnitSize; + _interleaveGapSize = interleaveGapSize; + _sectorSize = sectorSize; + + if (_fileUnitSize != 0 || _interleaveGapSize != 0) + { + throw new NotSupportedException("Non-contiguous extents not supported"); + } + } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Length + { + get { return _dataLength; } + } + + public override long Position + { + get { return _position; } + set { _position = value; } + } + + public override void Flush() {} + + public override int Read(byte[] buffer, int offset, int count) + { + if (_position > _dataLength) + { + return 0; + } + + int toRead = (int)Math.Min((uint)count, _dataLength - _position); + + _isoStream.Position = _position + _startBlock * (long)_sectorSize; + int numRead = _isoStream.Read(buffer, offset, toRead); + _position += numRead; + return numRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + long newPos = offset; + if (origin == SeekOrigin.Current) + { + newPos += _position; + } + else if (origin == SeekOrigin.End) + { + newPos += _dataLength; + } + + _position = newPos; + return newPos; + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/File.cs b/DiscUtils/Iso9660Ps1/File.cs new file mode 100644 index 0000000..32514fe --- /dev/null +++ b/DiscUtils/Iso9660Ps1/File.cs @@ -0,0 +1,119 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Vfs; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class File : IVfsFile + { + protected IsoContext _context; + protected ReaderDirEntry _dirEntry; + + public File(IsoContext context, ReaderDirEntry dirEntry) + { + _context = context; + _dirEntry = dirEntry; + } + + public virtual byte[] SystemUseData + { + get { return _dirEntry.Record.SystemUseData; } + } + + public UnixFileSystemInfo UnixFileInfo + { + get + { + if (!_context.SuspDetected || string.IsNullOrEmpty(_context.RockRidgeIdentifier)) + { + throw new InvalidOperationException("No RockRidge file information available"); + } + + SuspRecords suspRecords = new SuspRecords(_context, SystemUseData, 0); + + PosixFileInfoSystemUseEntry pfi = + suspRecords.GetEntry(_context.RockRidgeIdentifier, "PX"); + if (pfi != null) + { + return new UnixFileSystemInfo + { + FileType = (UnixFileType)((pfi.FileMode >> 12) & 0xff), + Permissions = (UnixFilePermissions)(pfi.FileMode & 0xfff), + UserId = (int)pfi.UserId, + GroupId = (int)pfi.GroupId, + Inode = pfi.Inode, + LinkCount = (int)pfi.NumLinks + }; + } + + throw new InvalidOperationException("No RockRidge file information available for this file"); + } + } + + public DateTime LastAccessTimeUtc + { + get { return _dirEntry.LastAccessTimeUtc; } + + set { throw new NotSupportedException(); } + } + + public DateTime LastWriteTimeUtc + { + get { return _dirEntry.LastWriteTimeUtc; } + + set { throw new NotSupportedException(); } + } + + public DateTime CreationTimeUtc + { + get { return _dirEntry.CreationTimeUtc; } + + set { throw new NotSupportedException(); } + } + + public FileAttributes FileAttributes + { + get { return _dirEntry.FileAttributes; } + + set { throw new NotSupportedException(); } + } + + public long FileLength + { + get { return _dirEntry.Record.DataLength; } + } + + public IBuffer FileContent + { + get + { + ExtentStream es = new ExtentStream(_context.DataStream, _dirEntry.Record.LocationOfExtent, + _dirEntry.Record.DataLength, _dirEntry.Record.FileUnitSize, _dirEntry.Record.InterleaveGapSize, _context.SectorSize); + return new StreamBuffer(es, Ownership.Dispose); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/FileExtent.cs b/DiscUtils/Iso9660Ps1/FileExtent.cs new file mode 100644 index 0000000..dbff57a --- /dev/null +++ b/DiscUtils/Iso9660Ps1/FileExtent.cs @@ -0,0 +1,85 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class FileExtent : BuilderExtent + { + private readonly BuildFileInfo _fileInfo; + + private Stream _readStream; + + public FileExtent(BuildFileInfo fileInfo, long start) + : base(start, fileInfo.GetDataSize(Encoding.ASCII)) + { + _fileInfo = fileInfo; + } + + public override void Dispose() + { + if (_readStream != null) + { + _fileInfo.CloseStream(_readStream); + _readStream = null; + } + } + + public override void PrepareForRead() + { + _readStream = _fileInfo.OpenStream(); + } + + public override int Read(long diskOffset, byte[] block, int offset, int count) + { + long relPos = diskOffset - Start; + int totalRead = 0; + + // Don't arbitrarily set position, just in case stream implementation is + // non-seeking, and we're doing sequential reads + if (_readStream.Position != relPos) + { + _readStream.Position = relPos; + } + + // Read up to EOF + int numRead = _readStream.Read(block, offset, count); + totalRead += numRead; + while (numRead > 0 && totalRead < count) + { + numRead = _readStream.Read(block, offset + totalRead, count - totalRead); + totalRead += numRead; + } + + return totalRead; + } + + public override void DisposeReadState() + { + _fileInfo.CloseStream(_readStream); + _readStream = null; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/FileFlags.cs b/DiscUtils/Iso9660Ps1/FileFlags.cs new file mode 100644 index 0000000..b350c27 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/FileFlags.cs @@ -0,0 +1,16 @@ +using System; + +namespace DiscUtils.Iso9660Ps1 +{ + [Flags] + internal enum FileFlags : byte + { + None = 0x00, + Hidden = 0x01, + Directory = 0x02, + AssociatedFile = 0x04, + Record = 0x08, + Protection = 0x10, + MultiExtent = 0x80 + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Iso9660Variant.cs b/DiscUtils/Iso9660Ps1/Iso9660Variant.cs new file mode 100644 index 0000000..fd76c2f --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Iso9660Variant.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + /// + /// Enumeration of known file system variants. + /// + /// + /// ISO9660 has a number of significant limitations, and over time + /// multiple schemes have been devised for extending the standard + /// to support the richer file system semantics typical of most modern + /// operating systems. These variants differ functionally and (in the + /// case of RockRidge) may represent a logically different directory + /// hierarchy to that encoded in the vanilla iso9660 standard. + /// Use this enum to control which variants to honour / prefer + /// when accessing an ISO image. + /// + public enum Iso9660Variant + { + /// + /// No known variant. + /// + None, + + /// + /// Vanilla ISO9660. + /// + Iso9660, + + /// + /// Joliet file system (Windows). + /// + Joliet, + + /// + /// Rock Ridge (Unix). + /// + RockRidge + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/IsoContext.cs b/DiscUtils/Iso9660Ps1/IsoContext.cs new file mode 100644 index 0000000..318f207 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/IsoContext.cs @@ -0,0 +1,49 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using System.IO; +using DiscUtils.Vfs; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class IsoContext : VfsContext + { + public IsoContext(int sectorSize) + { + SectorSize = sectorSize; + } + + public Stream DataStream { get; set; } + + public string RockRidgeIdentifier { get; set; } + + public bool SuspDetected { get; set; } + + public int SectorSize { get; set; } + + public List SuspExtensions { get; set; } + + public int SuspSkipBytes { get; set; } + public CommonVolumeDescriptor VolumeDescriptor { get; set; } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/IsoUtilities.cs b/DiscUtils/Iso9660Ps1/IsoUtilities.cs new file mode 100644 index 0000000..05e4398 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/IsoUtilities.cs @@ -0,0 +1,467 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Globalization; +using System.IO; +using System.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal static class IsoUtilities + { + //public const int SectorSize = 2048; + + public static uint ToUInt32FromBoth(byte[] data, int offset) + { + return EndianUtilities.ToUInt32LittleEndian(data, offset); + } + + public static ushort ToUInt16FromBoth(byte[] data, int offset) + { + return EndianUtilities.ToUInt16LittleEndian(data, offset); + } + + internal static void ToBothFromUInt32(byte[] buffer, int offset, uint value) + { + EndianUtilities.WriteBytesLittleEndian(value, buffer, offset); + EndianUtilities.WriteBytesBigEndian(value, buffer, offset + 4); + } + + internal static void ToBothFromUInt16(byte[] buffer, int offset, ushort value) + { + EndianUtilities.WriteBytesLittleEndian(value, buffer, offset); + EndianUtilities.WriteBytesBigEndian(value, buffer, offset + 2); + } + + internal static void ToBytesFromUInt32(byte[] buffer, int offset, uint value) + { + EndianUtilities.WriteBytesLittleEndian(value, buffer, offset); + } + + internal static void ToBytesFromUInt16(byte[] buffer, int offset, ushort value) + { + EndianUtilities.WriteBytesLittleEndian(value, buffer, offset); + } + + internal static void WriteAChars(byte[] buffer, int offset, int numBytes, string str) + { + // Validate string + if (!IsValidAString(str)) + { + throw new IOException("Attempt to write string with invalid a-characters"); + } + + ////WriteASCII(buffer, offset, numBytes, true, str); + WriteString(buffer, offset, numBytes, true, str, Encoding.ASCII); + } + + internal static void WriteDChars(byte[] buffer, int offset, int numBytes, string str) + { + // Validate string + if (!IsValidDString(str)) + { + throw new IOException("Attempt to write string with invalid d-characters"); + } + + ////WriteASCII(buffer, offset, numBytes, true, str); + WriteString(buffer, offset, numBytes, true, str, Encoding.ASCII); + } + + internal static void WriteA1Chars(byte[] buffer, int offset, int numBytes, string str, Encoding enc) + { + // Validate string + if (!IsValidAString(str)) + { + throw new IOException("Attempt to write string with invalid a-characters"); + } + + WriteString(buffer, offset, numBytes, true, str, enc); + } + + internal static void WriteD1Chars(byte[] buffer, int offset, int numBytes, string str, Encoding enc) + { + // Validate string + if (!IsValidDString(str)) + { + throw new IOException("Attempt to write string with invalid d-characters"); + } + + WriteString(buffer, offset, numBytes, true, str, enc); + } + + internal static string ReadChars(byte[] buffer, int offset, int numBytes, Encoding enc) + { + char[] chars; + + // Special handling for 'magic' names '\x00' and '\x01', which indicate root and parent, respectively + if (numBytes == 1) + { + chars = new char[1]; + chars[0] = (char)buffer[offset]; + } + else + { + Decoder decoder = enc.GetDecoder(); + chars = new char[decoder.GetCharCount(buffer, offset, numBytes, false)]; + decoder.GetChars(buffer, offset, numBytes, chars, 0, false); + } + + return new string(chars).TrimEnd(' '); + } + +#if false + public static byte WriteFileName(byte[] buffer, int offset, int numBytes, string str, Encoding enc) + { + if (numBytes > 255 || numBytes < 0) + { + throw new ArgumentOutOfRangeException("numBytes", "Attempt to write overlength or underlength file name"); + } + + // Validate string + if (!isValidFileName(str)) + { + throw new IOException("Attempt to write string with invalid file name characters"); + } + + return (byte)WriteString(buffer, offset, numBytes, false, str, enc); + } + + public static byte WriteDirectoryName(byte[] buffer, int offset, int numBytes, string str, Encoding enc) + { + if (numBytes > 255 || numBytes < 0) + { + throw new ArgumentOutOfRangeException("numBytes", "Attempt to write overlength or underlength directory name"); + } + + // Validate string + if (!isValidDirectoryName(str)) + { + throw new IOException("Attempt to write string with invalid directory name characters"); + } + + return (byte)WriteString(buffer, offset, numBytes, false, str, enc); + } +#endif + + internal static int WriteString(byte[] buffer, int offset, int numBytes, bool pad, string str, Encoding enc) + { + return WriteString(buffer, offset, numBytes, pad, str, enc, false); + } + + internal static int WriteString(byte[] buffer, int offset, int numBytes, bool pad, string str, Encoding enc, + bool canTruncate) + { + Encoder encoder = enc.GetEncoder(); + + string paddedString = pad ? str + new string(' ', numBytes) : str; + + // Assumption: never less than one byte per character + + int charsUsed; + int bytesUsed; + bool completed; + encoder.Convert(paddedString.ToCharArray(), 0, paddedString.Length, buffer, offset, numBytes, false, + out charsUsed, out bytesUsed, out completed); + + if (!canTruncate && charsUsed < str.Length) + { + throw new IOException("Failed to write entire string"); + } + + return bytesUsed; + } + + internal static bool IsValidAString(string str) + { + for (int i = 0; i < str.Length; ++i) + { + if (!( + (str[i] >= ' ' && str[i] <= '\"') + || (str[i] >= '%' && str[i] <= '/') + || (str[i] >= ':' && str[i] <= '?') + || (str[i] >= '0' && str[i] <= '9') + || (str[i] >= 'A' && str[i] <= 'Z') + || (str[i] == '_'))) + { + return false; + } + } + + return true; + } + + internal static bool IsValidDString(string str) + { + for (int i = 0; i < str.Length; ++i) + { + if (!IsValidDChar(str[i])) + { + return false; + } + } + + return true; + } + + internal static bool IsValidDChar(char ch) + { + return (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch == '_'); + } + + internal static bool IsValidFileName(string str) + { + for (int i = 0; i < str.Length; ++i) + { + if ( + !((str[i] >= '0' && str[i] <= '9') || (str[i] >= 'A' && str[i] <= 'Z') || (str[i] == '_') || + (str[i] == '.') || (str[i] == ';'))) + { + return false; + } + } + + return true; + } + + internal static bool IsValidDirectoryName(string str) + { + if (str.Length == 1 && (str[0] == 0 || str[0] == 1)) + { + return true; + } + return IsValidDString(str); + } + + internal static string NormalizeFileName(string name) + { + string[] parts = SplitFileName(name); + return parts[0] + '.' + parts[1] + ';' + parts[2]; + } + + internal static string[] SplitFileName(string name) + { + string[] parts = { name, string.Empty, "1" }; + + if (name.Contains(".")) + { + int endOfFilePart = name.IndexOf('.'); + parts[0] = name.Substring(0, endOfFilePart); + if (name.Contains(";")) + { + int verSep = name.IndexOf(';', endOfFilePart + 1); + parts[1] = name.Substring(endOfFilePart + 1, verSep - (endOfFilePart + 1)); + parts[2] = name.Substring(verSep + 1); + } + else + { + parts[1] = name.Substring(endOfFilePart + 1); + } + } + else + { + if (name.Contains(";")) + { + int verSep = name.IndexOf(';'); + parts[0] = name.Substring(0, verSep); + parts[2] = name.Substring(verSep + 1); + } + } + + ushort ver; + if (!ushort.TryParse(parts[2], out ver) || ver > 32767 || ver < 1) + { + ver = 1; + } + + parts[2] = string.Format(CultureInfo.InvariantCulture, "{0}", ver); + + return parts; + } + + /// + /// Converts a DirectoryRecord time to UTC. + /// + /// Buffer containing the time data. + /// Offset in buffer of the time data. + /// The time in UTC. + internal static DateTime ToUTCDateTimeFromDirectoryTime(byte[] data, int offset) + { + try + { + DateTime relTime = new DateTime( + 1900 + data[offset], + data[offset + 1], + data[offset + 2], + data[offset + 3], + data[offset + 4], + data[offset + 5], + DateTimeKind.Utc); + return relTime - TimeSpan.FromMinutes(15 * (sbyte)data[offset + 6]); + } + catch (ArgumentOutOfRangeException) + { + // In case the ISO has a bad date encoded, we'll just fall back to using a fixed date + return DateTime.MinValue; + } + } + + internal static void ToDirectoryTimeFromUTC(byte[] data, int offset, DateTime dateTime) + { + if (dateTime == DateTime.MinValue) + { + Array.Clear(data, offset, 7); + } + else + { + if (dateTime.Year < 1900) + { + throw new IOException("Year is out of range"); + } + + data[offset] = (byte)(dateTime.Year - 1900); + data[offset + 1] = (byte)dateTime.Month; + data[offset + 2] = (byte)dateTime.Day; + data[offset + 3] = (byte)dateTime.Hour; + data[offset + 4] = (byte)dateTime.Minute; + data[offset + 5] = (byte)dateTime.Second; + data[offset + 6] = 0; + } + } + + internal static DateTime ToDateTimeFromVolumeDescriptorTime(byte[] data, int offset) + { + bool allNull = true; + for (int i = 0; i < 16; ++i) + { + if (data[offset + i] != (byte)'0' && data[offset + i] != 0) + { + allNull = false; + break; + } + } + + if (allNull) + { + return DateTime.MinValue; + } + + string strForm = Encoding.ASCII.GetString(data, offset, 16); + + // Work around bugs in burning software that may use zero bytes (rather than '0' characters) + strForm = strForm.Replace('\0', '0'); + + int year = SafeParseInt(1, 9999, strForm.Substring(0, 4)); + int month = SafeParseInt(1, 12, strForm.Substring(4, 2)); + int day = SafeParseInt(1, 31, strForm.Substring(6, 2)); + int hour = SafeParseInt(0, 23, strForm.Substring(8, 2)); + int min = SafeParseInt(0, 59, strForm.Substring(10, 2)); + int sec = SafeParseInt(0, 59, strForm.Substring(12, 2)); + int hundredths = SafeParseInt(0, 99, strForm.Substring(14, 2)); + + try + { + DateTime time = new DateTime(year, month, day, hour, min, sec, hundredths * 10, DateTimeKind.Utc); + return time - TimeSpan.FromMinutes(15 * (sbyte)data[offset + 16]); + } + catch (ArgumentOutOfRangeException) + { + return DateTime.MinValue; + } + } + + internal static void ToVolumeDescriptorTimeFromUTC(byte[] buffer, int offset, DateTime dateTime) + { + if (dateTime == DateTime.MinValue) + { + for (int i = offset; i < offset + 16; ++i) + { + buffer[i] = (byte)'0'; + } + + buffer[offset + 16] = 0; + return; + } + + string strForm = dateTime.ToString("yyyyMMddHHmmssff", CultureInfo.InvariantCulture); + EndianUtilities.StringToBytes(strForm, buffer, offset, 16); + buffer[offset + 16] = 0; + } + + internal static void EncodingToBytes(Encoding enc, byte[] data, int offset) + { + Array.Clear(data, offset, 32); + if (enc == Encoding.ASCII) + { + // Nothing to do + } + else if (enc == Encoding.BigEndianUnicode) + { + data[offset + 0] = 0x25; + data[offset + 1] = 0x2F; + data[offset + 2] = 0x45; + } + else + { + throw new ArgumentException("Unrecognized character encoding"); + } + } + + internal static Encoding EncodingFromBytes(byte[] data, int offset) + { + Encoding enc = Encoding.ASCII; + if (data[offset + 0] == 0x25 && data[offset + 1] == 0x2F + && (data[offset + 2] == 0x40 || data[offset + 2] == 0x43 || data[offset + 2] == 0x45)) + { + // I.e. this is a joliet disc! + enc = Encoding.BigEndianUnicode; + } + + return enc; + } + + internal static bool IsSpecialDirectory(DirectoryRecord r) + { + return r.FileIdentifier == "\0" || r.FileIdentifier == "\x01"; + } + + private static int SafeParseInt(int minVal, int maxVal, string str) + { + int val; + if (!int.TryParse(str, out val)) + { + return minVal; + } + + if (val < minVal) + { + return minVal; + } + if (val > maxVal) + { + return maxVal; + } + return val; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/PathTable.cs b/DiscUtils/Iso9660Ps1/PathTable.cs new file mode 100644 index 0000000..e314cd4 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/PathTable.cs @@ -0,0 +1,100 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class PathTable : BuilderExtent + { + private readonly bool _byteSwap; + private readonly List _dirs; + private readonly Encoding _enc; + private readonly Dictionary _locations; + + private byte[] _readCache; + + public PathTable(bool byteSwap, Encoding enc, List dirs, + Dictionary locations, long start) + : base(start, CalcLength(enc, dirs)) + { + _byteSwap = byteSwap; + _enc = enc; + _dirs = dirs; + _locations = locations; + } + + public override void Dispose() {} + + public override void PrepareForRead() + { + _readCache = new byte[Length]; + int pos = 0; + + List sortedList = new List(_dirs); + sortedList.Sort(BuildDirectoryInfo.PathTableSortComparison); + + Dictionary dirNumbers = new Dictionary(_dirs.Count); + ushort i = 1; + foreach (BuildDirectoryInfo di in sortedList) + { + dirNumbers[di] = i++; + PathTableRecord ptr = new PathTableRecord(); + ptr.DirectoryIdentifier = di.PickName(null, _enc); + ptr.LocationOfExtent = _locations[di]; + ptr.ParentDirectoryNumber = dirNumbers[di.Parent]; + + pos += ptr.Write(_byteSwap, _enc, _readCache, pos); + } + } + + public override int Read(long diskOffset, byte[] buffer, int offset, int count) + { + long relPos = diskOffset - Start; + + int numRead = (int)Math.Min(count, _readCache.Length - relPos); + + Array.Copy(_readCache, (int)relPos, buffer, offset, numRead); + + return numRead; + } + + public override void DisposeReadState() + { + _readCache = null; + } + + private static uint CalcLength(Encoding enc, List dirs) + { + uint length = 0; + foreach (BuildDirectoryInfo di in dirs) + { + length += di.GetPathTableEntrySize(enc); + } + + return length; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/PathTableRecord.cs b/DiscUtils/Iso9660Ps1/PathTableRecord.cs new file mode 100644 index 0000000..0480e18 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/PathTableRecord.cs @@ -0,0 +1,71 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Internal; + +namespace DiscUtils.Iso9660Ps1 +{ + internal struct PathTableRecord + { + ////public byte ExtendedAttributeRecordLength; + public uint LocationOfExtent; + public ushort ParentDirectoryNumber; + public string DirectoryIdentifier; + + ////public static int ReadFrom(byte[] src, int offset, bool byteSwap, Encoding enc, out PathTableRecord record) + ////{ + //// byte directoryIdentifierLength = src[offset + 0]; + //// record.ExtendedAttributeRecordLength = src[offset + 1]; + //// record.LocationOfExtent = EndianUtilities.ToUInt32LittleEndian(src, offset + 2); + //// record.ParentDirectoryNumber = EndianUtilities.ToUInt16LittleEndian(src, offset + 6); + //// record.DirectoryIdentifier = IsoUtilities.ReadChars(src, offset + 8, directoryIdentifierLength, enc); + //// + //// if (byteSwap) + //// { + //// record.LocationOfExtent = Utilities.BitSwap(record.LocationOfExtent); + //// record.ParentDirectoryNumber = Utilities.BitSwap(record.ParentDirectoryNumber); + //// } + //// + //// return directoryIdentifierLength + 8 + (((directoryIdentifierLength & 1) == 1) ? 1 : 0); + ////} + + internal int Write(bool byteSwap, Encoding enc, byte[] buffer, int offset) + { + int nameBytes = enc.GetByteCount(DirectoryIdentifier); + + buffer[offset + 0] = (byte)nameBytes; + buffer[offset + 1] = 0; // ExtendedAttributeRecordLength; + IsoUtilities.ToBytesFromUInt32(buffer, offset + 2, + byteSwap ? Utilities.BitSwap(LocationOfExtent) : LocationOfExtent); + IsoUtilities.ToBytesFromUInt16(buffer, offset + 6, + byteSwap ? Utilities.BitSwap(ParentDirectoryNumber) : ParentDirectoryNumber); + IsoUtilities.WriteString(buffer, offset + 8, nameBytes, false, DirectoryIdentifier, enc); + if ((nameBytes & 1) == 1) + { + buffer[offset + 8 + nameBytes] = 0; + } + + return 8 + nameBytes + ((nameBytes & 0x1) == 1 ? 1 : 0); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/PrimaryVolumeDescriptor.cs b/DiscUtils/Iso9660Ps1/PrimaryVolumeDescriptor.cs new file mode 100644 index 0000000..75be21b --- /dev/null +++ b/DiscUtils/Iso9660Ps1/PrimaryVolumeDescriptor.cs @@ -0,0 +1,76 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Internal; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class PrimaryVolumeDescriptor : CommonVolumeDescriptor + { + public PrimaryVolumeDescriptor(byte[] src, int offset) + : base(src, offset, Encoding.ASCII) {} + + public PrimaryVolumeDescriptor( + uint volumeSpaceSize, + uint pathTableSize, + uint typeLPathTableLocation, + uint typeMPathTableLocation, + uint rootDirExtentLocation, + uint rootDirDataLength, + DateTime buildTime, + int sectorSize) + : base( + VolumeDescriptorType.Primary, 1, volumeSpaceSize, pathTableSize, typeLPathTableLocation, + typeMPathTableLocation, rootDirExtentLocation, rootDirDataLength, buildTime, Encoding.ASCII, sectorSize) {} + + internal override void WriteTo(byte[] buffer, int offset) + { + base.WriteTo(buffer, offset); + IsoUtilities.WriteAChars(buffer, offset + 8, 32, SystemIdentifier); + IsoUtilities.WriteString(buffer, offset + 40, 32, true, VolumeIdentifier, Encoding.ASCII, true); + IsoUtilities.ToBothFromUInt32(buffer, offset + 80, VolumeSpaceSize); + IsoUtilities.ToBothFromUInt16(buffer, offset + 120, VolumeSetSize); + IsoUtilities.ToBothFromUInt16(buffer, offset + 124, VolumeSequenceNumber); + IsoUtilities.ToBothFromUInt16(buffer, offset + 128, LogicalBlockSize); + IsoUtilities.ToBothFromUInt32(buffer, offset + 132, PathTableSize); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 140, TypeLPathTableLocation); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 144, OptionalTypeLPathTableLocation); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 148, Utilities.BitSwap(TypeMPathTableLocation)); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 152, Utilities.BitSwap(OptionalTypeMPathTableLocation)); + RootDirectory.WriteTo(buffer, offset + 156, Encoding.ASCII); + IsoUtilities.WriteDChars(buffer, offset + 190, 129, VolumeSetIdentifier); + IsoUtilities.WriteAChars(buffer, offset + 318, 129, PublisherIdentifier); + IsoUtilities.WriteAChars(buffer, offset + 446, 129, DataPreparerIdentifier); + IsoUtilities.WriteAChars(buffer, offset + 574, 129, ApplicationIdentifier); + IsoUtilities.WriteDChars(buffer, offset + 702, 37, CopyrightFileIdentifier); // FIXME!! + IsoUtilities.WriteDChars(buffer, offset + 739, 37, AbstractFileIdentifier); // FIXME!! + IsoUtilities.WriteDChars(buffer, offset + 776, 37, BibliographicFileIdentifier); // FIXME!! + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 813, CreationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 830, ModificationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 847, ExpirationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 864, EffectiveDateAndTime); + buffer[offset + 881] = FileStructureVersion; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/PrimaryVolumeDescriptorRegion.cs b/DiscUtils/Iso9660Ps1/PrimaryVolumeDescriptorRegion.cs new file mode 100644 index 0000000..d327ae3 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/PrimaryVolumeDescriptorRegion.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal class PrimaryVolumeDescriptorRegion : VolumeDescriptorDiskRegion + { + private readonly PrimaryVolumeDescriptor _descriptor; + + public PrimaryVolumeDescriptorRegion(PrimaryVolumeDescriptor descriptor, long start, int sectorSize) + : base(start, sectorSize) + { + _descriptor = descriptor; + } + + protected override byte[] GetBlockData() + { + byte[] buffer = new byte[Length]; + _descriptor.WriteTo(buffer, 0); + return buffer; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/ReaderDirEntry.cs b/DiscUtils/Iso9660Ps1/ReaderDirEntry.cs new file mode 100644 index 0000000..3678b95 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/ReaderDirEntry.cs @@ -0,0 +1,195 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Text; +using DiscUtils.Internal; +using DiscUtils.Streams; +using DiscUtils.Vfs; + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class ReaderDirEntry : VfsDirEntry + { + private readonly IsoContext _context; + private readonly string _fileName; + private readonly DirectoryRecord _record; + + public ReaderDirEntry(IsoContext context, DirectoryRecord dirRecord) + { + _context = context; + _record = dirRecord; + _fileName = _record.FileIdentifier; + + bool rockRidge = !string.IsNullOrEmpty(_context.RockRidgeIdentifier); + + if (context.SuspDetected && _record.SystemUseData != null) + { + SuspRecords = new SuspRecords(_context, _record.SystemUseData, 0); + } + + if (rockRidge && SuspRecords != null) + { + // The full name is taken from this record, even if it's a child-link record + List nameEntries = SuspRecords.GetEntries(_context.RockRidgeIdentifier, "NM"); + StringBuilder rrName = new StringBuilder(); + if (nameEntries != null && nameEntries.Count > 0) + { + foreach (PosixNameSystemUseEntry nameEntry in nameEntries) + { + rrName.Append(nameEntry.NameData); + } + + _fileName = rrName.ToString(); + } + + // If this is a Rock Ridge child link, replace the dir record with that from the 'self' record + // in the child directory. + ChildLinkSystemUseEntry clEntry = + SuspRecords.GetEntry(_context.RockRidgeIdentifier, "CL"); + if (clEntry != null) + { + _context.DataStream.Position = clEntry.ChildDirLocation * _context.VolumeDescriptor.LogicalBlockSize; + byte[] firstSector = StreamUtilities.ReadExact(_context.DataStream, + _context.VolumeDescriptor.LogicalBlockSize); + + DirectoryRecord.ReadFrom(firstSector, 0, _context.VolumeDescriptor.CharacterEncoding, out _record); + if (_record.SystemUseData != null) + { + SuspRecords = new SuspRecords(_context, _record.SystemUseData, 0); + } + } + } + + LastAccessTimeUtc = _record.RecordingDateAndTime; + LastWriteTimeUtc = _record.RecordingDateAndTime; + CreationTimeUtc = _record.RecordingDateAndTime; + + if (rockRidge && SuspRecords != null) + { + FileTimeSystemUseEntry tfEntry = + SuspRecords.GetEntry(_context.RockRidgeIdentifier, "TF"); + + if (tfEntry != null) + { + if ((tfEntry.TimestampsPresent & FileTimeSystemUseEntry.Timestamps.Access) != 0) + { + LastAccessTimeUtc = tfEntry.AccessTime; + } + + if ((tfEntry.TimestampsPresent & FileTimeSystemUseEntry.Timestamps.Modify) != 0) + { + LastWriteTimeUtc = tfEntry.ModifyTime; + } + + if ((tfEntry.TimestampsPresent & FileTimeSystemUseEntry.Timestamps.Creation) != 0) + { + CreationTimeUtc = tfEntry.CreationTime; + } + } + } + } + + public override DateTime CreationTimeUtc { get; } + + public override FileAttributes FileAttributes + { + get + { + FileAttributes attrs = 0; + + if (!string.IsNullOrEmpty(_context.RockRidgeIdentifier)) + { + // If Rock Ridge PX info is present, derive the attributes from the RR info. + PosixFileInfoSystemUseEntry pfi = + SuspRecords.GetEntry(_context.RockRidgeIdentifier, "PX"); + if (pfi != null) + { + attrs = Utilities.FileAttributesFromUnixFileType((UnixFileType)((pfi.FileMode >> 12) & 0xF)); + } + + if (_fileName.StartsWith(".", StringComparison.Ordinal)) + { + attrs |= FileAttributes.Hidden; + } + } + + attrs |= FileAttributes.ReadOnly; + + if ((_record.Flags & FileFlags.Directory) != 0) + { + attrs |= FileAttributes.Directory; + } + + if ((_record.Flags & FileFlags.Hidden) != 0) + { + attrs |= FileAttributes.Hidden; + } + + return attrs; + } + } + + public override string FileName + { + get { return _fileName; } + } + + public override bool HasVfsFileAttributes + { + get { return true; } + } + + public override bool HasVfsTimeInfo + { + get { return true; } + } + + public override bool IsDirectory + { + get { return (_record.Flags & FileFlags.Directory) != 0; } + } + + public override bool IsSymlink + { + get { return false; } + } + + public override DateTime LastAccessTimeUtc { get; } + + public override DateTime LastWriteTimeUtc { get; } + + public DirectoryRecord Record + { + get { return _record; } + } + + public SuspRecords SuspRecords { get; } + + public override long UniqueCacheId + { + get { return ((long)_record.LocationOfExtent << 32) | _record.DataLength; } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/ReaderDirectory.cs b/DiscUtils/Iso9660Ps1/ReaderDirectory.cs new file mode 100644 index 0000000..1e43f49 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/ReaderDirectory.cs @@ -0,0 +1,130 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.CoreCompat; +using DiscUtils.Streams; +using DiscUtils.Vfs; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class ReaderDirectory : File, IVfsDirectory + { + private readonly List _records; + + public ReaderDirectory(IsoContext context, ReaderDirEntry dirEntry) + : base(context, dirEntry) + { + byte[] buffer = new byte[context.SectorSize]; + Stream extent = new ExtentStream(_context.DataStream, dirEntry.Record.LocationOfExtent, uint.MaxValue, 0, 0, context.SectorSize); + + _records = new List(); + + uint totalLength = dirEntry.Record.DataLength; + uint totalRead = 0; + while (totalRead < totalLength) + { + int bytesRead = (int)Math.Min(buffer.Length, totalLength - totalRead); + + extent.Seek(24, SeekOrigin.Current); + + StreamUtilities.ReadExact(extent, buffer, 0, bytesRead); + totalRead += (uint)bytesRead; + + uint pos = 0; + while (pos < bytesRead && buffer[pos] != 0) + { + DirectoryRecord dr; + uint length = (uint)DirectoryRecord.ReadFrom(buffer, (int)pos, context.VolumeDescriptor.CharacterEncoding, out dr); + + if (!IsoUtilities.IsSpecialDirectory(dr)) + { + ReaderDirEntry childDirEntry = new ReaderDirEntry(_context, dr); + + if (context.SuspDetected && !string.IsNullOrEmpty(context.RockRidgeIdentifier)) + { + if (childDirEntry.SuspRecords == null || !childDirEntry.SuspRecords.HasEntry(context.RockRidgeIdentifier, "RE")) + { + _records.Add(childDirEntry); + } + } + else + { + _records.Add(childDirEntry); + } + } + else if (dr.FileIdentifier == "\0") + { + Self = new ReaderDirEntry(_context, dr); + } + + pos += length; + } + } + } + + public override byte[] SystemUseData + { + get { return Self.Record.SystemUseData; } + } + + public ICollection AllEntries + { + get { return _records; } + } + + public ReaderDirEntry Self { get; } + + public ReaderDirEntry GetEntryByName(string name) + { + bool anyVerMatch = name.IndexOf(';') < 0; + string normName = IsoUtilities.NormalizeFileName(name).ToUpper(CultureInfo.InvariantCulture); + if (anyVerMatch) + { + normName = normName.Substring(0, normName.LastIndexOf(';') + 1); + } + + foreach (ReaderDirEntry r in _records) + { + string toComp = IsoUtilities.NormalizeFileName(r.FileName).ToUpper(CultureInfo.InvariantCulture); + if (!anyVerMatch && toComp == normName) + { + return r; + } + if (anyVerMatch && toComp.StartsWith(normName, StringComparison.Ordinal)) + { + return r; + } + } + + return null; + } + + public ReaderDirEntry CreateNewFile(string name) + { + throw new NotSupportedException(); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/RockRidge/ChildLinkSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/RockRidge/ChildLinkSystemUseEntry.cs new file mode 100644 index 0000000..5806eee --- /dev/null +++ b/DiscUtils/Iso9660Ps1/RockRidge/ChildLinkSystemUseEntry.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class ChildLinkSystemUseEntry : SystemUseEntry + { + public uint ChildDirLocation; + + public ChildLinkSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 12, 1); + + ChildDirLocation = IsoUtilities.ToUInt32FromBoth(data, offset + 4); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/RockRidge/FileTimeSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/RockRidge/FileTimeSystemUseEntry.cs new file mode 100644 index 0000000..d8e9780 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/RockRidge/FileTimeSystemUseEntry.cs @@ -0,0 +1,94 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class FileTimeSystemUseEntry : SystemUseEntry + { + [Flags] + public enum Timestamps : byte + { + None = 0x00, + Creation = 0x01, + Modify = 0x02, + Access = 0x04, + Attributes = 0x08, + Backup = 0x10, + Expiration = 0x20, + Effective = 0x40 + } + + public DateTime AccessTime; + public DateTime AttributesTime; + public DateTime BackupTime; + public DateTime CreationTime; + public DateTime EffectiveTime; + public DateTime ExpirationTime; + public DateTime ModifyTime; + public Timestamps TimestampsPresent = Timestamps.None; + + public FileTimeSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 5, 1); + + byte flags = data[offset + 4]; + + bool longForm = (flags & 0x80) != 0; + int fieldLen = longForm ? 17 : 7; + + TimestampsPresent = (Timestamps)(flags & 0x7F); + + int pos = offset + 5; + + CreationTime = ReadTimestamp(Timestamps.Creation, data, longForm, ref pos); + ModifyTime = ReadTimestamp(Timestamps.Modify, data, longForm, ref pos); + AccessTime = ReadTimestamp(Timestamps.Access, data, longForm, ref pos); + AttributesTime = ReadTimestamp(Timestamps.Attributes, data, longForm, ref pos); + BackupTime = ReadTimestamp(Timestamps.Backup, data, longForm, ref pos); + ExpirationTime = ReadTimestamp(Timestamps.Expiration, data, longForm, ref pos); + EffectiveTime = ReadTimestamp(Timestamps.Effective, data, longForm, ref pos); + } + + private DateTime ReadTimestamp(Timestamps timestamp, byte[] data, bool longForm, ref int pos) + { + DateTime result = DateTime.MinValue; + + if ((TimestampsPresent & timestamp) != 0) + { + if (longForm) + { + result = IsoUtilities.ToDateTimeFromVolumeDescriptorTime(data, pos); + pos += 17; + } + else + { + result = IsoUtilities.ToUTCDateTimeFromDirectoryTime(data, pos); + pos += 7; + } + } + + return result; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/RockRidge/PosixFileInfoSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/RockRidge/PosixFileInfoSystemUseEntry.cs new file mode 100644 index 0000000..aa9cbd4 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/RockRidge/PosixFileInfoSystemUseEntry.cs @@ -0,0 +1,48 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class PosixFileInfoSystemUseEntry : SystemUseEntry + { + public uint FileMode; + public uint GroupId; + public uint Inode; + public uint NumLinks; + public uint UserId; + + public PosixFileInfoSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 36, 1); + + FileMode = IsoUtilities.ToUInt32FromBoth(data, offset + 4); + NumLinks = IsoUtilities.ToUInt32FromBoth(data, offset + 12); + UserId = IsoUtilities.ToUInt32FromBoth(data, offset + 20); + GroupId = IsoUtilities.ToUInt32FromBoth(data, offset + 28); + Inode = 0; + if (length >= 44) + { + Inode = IsoUtilities.ToUInt32FromBoth(data, offset + 36); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/RockRidge/PosixNameSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/RockRidge/PosixNameSystemUseEntry.cs new file mode 100644 index 0000000..ba32771 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/RockRidge/PosixNameSystemUseEntry.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class PosixNameSystemUseEntry : SystemUseEntry + { + public byte Flags; + public string NameData; + + public PosixNameSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 5, 1); + + Flags = data[offset + 4]; + NameData = EndianUtilities.BytesToString(data, offset + 5, length - 5); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/RockRidge/RockRidgeExtension.cs b/DiscUtils/Iso9660Ps1/RockRidge/RockRidgeExtension.cs new file mode 100644 index 0000000..9c2f422 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/RockRidge/RockRidgeExtension.cs @@ -0,0 +1,57 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class RockRidgeExtension : SuspExtension + { + public RockRidgeExtension(string identifier) + { + Identifier = identifier; + } + + public override string Identifier { get; } + + public override SystemUseEntry Parse(string name, byte length, byte version, byte[] data, int offset, Encoding encoding) + { + switch (name) + { + case "PX": + return new PosixFileInfoSystemUseEntry(name, length, version, data, offset); + + case "NM": + return new PosixNameSystemUseEntry(name, length, version, data, offset); + + case "CL": + return new ChildLinkSystemUseEntry(name, length, version, data, offset); + + case "TF": + return new FileTimeSystemUseEntry(name, length, version, data, offset); + + default: + return new GenericSystemUseEntry(name, length, version, data, offset); + } + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/SupplementaryVolumeDescriptor.cs b/DiscUtils/Iso9660Ps1/SupplementaryVolumeDescriptor.cs new file mode 100644 index 0000000..719b70b --- /dev/null +++ b/DiscUtils/Iso9660Ps1/SupplementaryVolumeDescriptor.cs @@ -0,0 +1,80 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Internal; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class SupplementaryVolumeDescriptor : CommonVolumeDescriptor + { + public SupplementaryVolumeDescriptor(byte[] src, int offset) + : base(src, offset, IsoUtilities.EncodingFromBytes(src, offset + 88)) {} + + public SupplementaryVolumeDescriptor( + uint volumeSpaceSize, + uint pathTableSize, + uint typeLPathTableLocation, + uint typeMPathTableLocation, + uint rootDirExtentLocation, + uint rootDirDataLength, + DateTime buildTime, + Encoding enc, + int sectorSize) + : base( + VolumeDescriptorType.Supplementary, 1, volumeSpaceSize, pathTableSize, typeLPathTableLocation, + typeMPathTableLocation, rootDirExtentLocation, rootDirDataLength, buildTime, enc, sectorSize) {} + + internal override void WriteTo(byte[] buffer, int offset) + { + base.WriteTo(buffer, offset); + IsoUtilities.WriteA1Chars(buffer, offset + 8, 32, SystemIdentifier, CharacterEncoding); + IsoUtilities.WriteString(buffer, offset + 40, 32, true, VolumeIdentifier, CharacterEncoding, true); + IsoUtilities.ToBothFromUInt32(buffer, offset + 80, VolumeSpaceSize); + IsoUtilities.EncodingToBytes(CharacterEncoding, buffer, offset + 88); + IsoUtilities.ToBothFromUInt16(buffer, offset + 120, VolumeSetSize); + IsoUtilities.ToBothFromUInt16(buffer, offset + 124, VolumeSequenceNumber); + IsoUtilities.ToBothFromUInt16(buffer, offset + 128, LogicalBlockSize); + IsoUtilities.ToBothFromUInt32(buffer, offset + 132, PathTableSize); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 140, TypeLPathTableLocation); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 144, OptionalTypeLPathTableLocation); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 148, Utilities.BitSwap(TypeMPathTableLocation)); + IsoUtilities.ToBytesFromUInt32(buffer, offset + 152, Utilities.BitSwap(OptionalTypeMPathTableLocation)); + RootDirectory.WriteTo(buffer, offset + 156, CharacterEncoding); + IsoUtilities.WriteD1Chars(buffer, offset + 190, 129, VolumeSetIdentifier, CharacterEncoding); + IsoUtilities.WriteA1Chars(buffer, offset + 318, 129, PublisherIdentifier, CharacterEncoding); + IsoUtilities.WriteA1Chars(buffer, offset + 446, 129, DataPreparerIdentifier, CharacterEncoding); + IsoUtilities.WriteA1Chars(buffer, offset + 574, 129, ApplicationIdentifier, CharacterEncoding); + IsoUtilities.WriteD1Chars(buffer, offset + 702, 37, CopyrightFileIdentifier, CharacterEncoding); // FIXME!! + IsoUtilities.WriteD1Chars(buffer, offset + 739, 37, AbstractFileIdentifier, CharacterEncoding); // FIXME!! + IsoUtilities.WriteD1Chars(buffer, offset + 776, 37, BibliographicFileIdentifier, CharacterEncoding); + + // FIXME!! + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 813, CreationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 830, ModificationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 847, ExpirationDateAndTime); + IsoUtilities.ToVolumeDescriptorTimeFromUTC(buffer, offset + 864, EffectiveDateAndTime); + buffer[offset + 881] = FileStructureVersion; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/SupplementaryVolumeDescriptorRegion.cs b/DiscUtils/Iso9660Ps1/SupplementaryVolumeDescriptorRegion.cs new file mode 100644 index 0000000..329f0f5 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/SupplementaryVolumeDescriptorRegion.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal class SupplementaryVolumeDescriptorRegion : VolumeDescriptorDiskRegion + { + private readonly SupplementaryVolumeDescriptor _descriptor; + + public SupplementaryVolumeDescriptorRegion(SupplementaryVolumeDescriptor descriptor, long start, int sectorSize) + : base(start, sectorSize) + { + _descriptor = descriptor; + } + + protected override byte[] GetBlockData() + { + byte[] buffer = new byte[Length]; + _descriptor.WriteTo(buffer, 0); + return buffer; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/ContinuationSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/Susp/ContinuationSystemUseEntry.cs new file mode 100644 index 0000000..23ea48f --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/ContinuationSystemUseEntry.cs @@ -0,0 +1,40 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class ContinuationSystemUseEntry : SystemUseEntry + { + public uint Block; + public uint BlockOffset; + public uint Length; + + public ContinuationSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 28, 1); + + Block = IsoUtilities.ToUInt32FromBoth(data, offset + 4); + BlockOffset = IsoUtilities.ToUInt32FromBoth(data, offset + 12); + Length = IsoUtilities.ToUInt32FromBoth(data, offset + 20); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/ExtensionSelectSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/Susp/ExtensionSelectSystemUseEntry.cs new file mode 100644 index 0000000..9001da4 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/ExtensionSelectSystemUseEntry.cs @@ -0,0 +1,36 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class ExtensionSelectSystemUseEntry : SystemUseEntry + { + public byte SelectedExtension; + + public ExtensionSelectSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 5, 1); + + SelectedExtension = data[offset + 4]; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/ExtensionSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/Susp/ExtensionSystemUseEntry.cs new file mode 100644 index 0000000..dc6d382 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/ExtensionSystemUseEntry.cs @@ -0,0 +1,56 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using System.Text; + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class ExtensionSystemUseEntry : SystemUseEntry + { + public string ExtensionDescriptor; + public string ExtensionIdentifier; + public string ExtensionSource; + public byte ExtensionVersion; + + public ExtensionSystemUseEntry(string name, byte length, byte version, byte[] data, int offset, Encoding encoding) + { + CheckAndSetCommonProperties(name, length, version, 8, 1); + + int lenId = data[offset + 4]; + int lenDescriptor = data[offset + 5]; + int lenSource = data[offset + 6]; + + ExtensionVersion = data[offset + 7]; + + if (length < 8 + lenId + lenDescriptor + lenSource) + { + throw new InvalidDataException("Invalid SUSP ER entry - too short, only " + length + " bytes - expected: " + + (8 + lenId + lenDescriptor + lenSource)); + } + + ExtensionIdentifier = IsoUtilities.ReadChars(data, offset + 8, lenId, encoding); + ExtensionDescriptor = IsoUtilities.ReadChars(data, offset + 8 + lenId, lenDescriptor, encoding); + ExtensionSource = IsoUtilities.ReadChars(data, offset + 8 + lenId + lenDescriptor, lenSource, encoding); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/GenericSuspExtension.cs b/DiscUtils/Iso9660Ps1/Susp/GenericSuspExtension.cs new file mode 100644 index 0000000..0b2af9c --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/GenericSuspExtension.cs @@ -0,0 +1,41 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class GenericSuspExtension : SuspExtension + { + public GenericSuspExtension(string identifier) + { + Identifier = identifier; + } + + public override string Identifier { get; } + + public override SystemUseEntry Parse(string name, byte length, byte version, byte[] data, int offset, Encoding encoding) + { + return new GenericSystemUseEntry(name, length, version, data, offset); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/GenericSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/Susp/GenericSystemUseEntry.cs new file mode 100644 index 0000000..e1d95b5 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/GenericSystemUseEntry.cs @@ -0,0 +1,39 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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; + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class GenericSystemUseEntry : SystemUseEntry + { + public byte[] Data; + + public GenericSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 4, 0xFF); + + Data = new byte[length - 4]; + Array.Copy(data, offset + 4, Data, 0, length - 4); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/PaddingSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/Susp/PaddingSystemUseEntry.cs new file mode 100644 index 0000000..49c2134 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/PaddingSystemUseEntry.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class PaddingSystemUseEntry : SystemUseEntry + { + public PaddingSystemUseEntry(string name, byte length, byte version) + { + CheckAndSetCommonProperties(name, length, version, 4, 1); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/SharingProtocolSystemUseEntry.cs b/DiscUtils/Iso9660Ps1/Susp/SharingProtocolSystemUseEntry.cs new file mode 100644 index 0000000..b35520d --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/SharingProtocolSystemUseEntry.cs @@ -0,0 +1,43 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class SharingProtocolSystemUseEntry : SystemUseEntry + { + public byte SystemAreaSkip; + + public SharingProtocolSystemUseEntry(string name, byte length, byte version, byte[] data, int offset) + { + CheckAndSetCommonProperties(name, length, version, 7, 1); + + if (data[offset + 4] != 0xBE || data[offset + 5] != 0xEF) + { + throw new InvalidDataException("Invalid SUSP SP entry - invalid checksum bytes"); + } + + SystemAreaSkip = data[offset + 6]; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/SuspExtension.cs b/DiscUtils/Iso9660Ps1/Susp/SuspExtension.cs new file mode 100644 index 0000000..fd220b2 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/SuspExtension.cs @@ -0,0 +1,33 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; + +namespace DiscUtils.Iso9660Ps1 +{ + internal abstract class SuspExtension + { + public abstract string Identifier { get; } + + public abstract SystemUseEntry Parse(string name, byte length, byte version, byte[] data, int offset, Encoding encoding); + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/SuspRecords.cs b/DiscUtils/Iso9660Ps1/Susp/SuspRecords.cs new file mode 100644 index 0000000..174ee40 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/SuspRecords.cs @@ -0,0 +1,182 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Collections.Generic; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal sealed class SuspRecords + { + private readonly Dictionary>> _records; + + public SuspRecords(IsoContext context, byte[] data, int offset) + { + _records = new Dictionary>>(); + + ContinuationSystemUseEntry contEntry = Parse(context, data, offset + context.SuspSkipBytes); + while (contEntry != null) + { + context.DataStream.Position = contEntry.Block * (long)context.VolumeDescriptor.LogicalBlockSize + + contEntry.BlockOffset; + byte[] contData = StreamUtilities.ReadExact(context.DataStream, (int)contEntry.Length); + + contEntry = Parse(context, contData, 0); + } + } + + public static bool DetectSharingProtocol(byte[] data, int offset) + { + if (data == null || data.Length - offset < 7) + { + return false; + } + + return data[offset] == 83 + && data[offset + 1] == 80 + && data[offset + 2] == 7 + && data[offset + 3] == 1 + && data[offset + 4] == 0xBE + && data[offset + 5] == 0xEF; + } + + public List GetEntries(string extension, string name) + { + if (string.IsNullOrEmpty(extension)) + { + extension = string.Empty; + } + + Dictionary> extensionData; + if (!_records.TryGetValue(extension, out extensionData)) + { + return null; + } + + List result; + if (extensionData.TryGetValue(name, out result)) + { + return result; + } + + return null; + } + + public T GetEntry(string extension, string name) + where T : SystemUseEntry + { + List entries = GetEntries(extension, name); + if (entries == null) + { + return null; + } + + foreach (T entry in entries) + { + return entry; + } + + return null; + } + + public bool HasEntry(string extension, string name) + { + List entries = GetEntries(extension, name); + return entries != null && entries.Count != 0; + } + + private ContinuationSystemUseEntry Parse(IsoContext context, byte[] data, int offset) + { + ContinuationSystemUseEntry contEntry = null; + SuspExtension extension = null; + + if (context.SuspExtensions != null && context.SuspExtensions.Count > 0) + { + extension = context.SuspExtensions[0]; + } + + int pos = offset; + while (data.Length - pos > 4) + { + byte len; + SystemUseEntry entry = SystemUseEntry.Parse(data, pos, context.VolumeDescriptor.CharacterEncoding, + extension, out len); + pos += len; + + if (entry == null) + { + // A null entry indicates SUSP parsing must terminate. + // This will occur if a termination record is found, + // or if there is a problem with the SUSP data. + return contEntry; + } + + switch (entry.Name) + { + case "CE": + contEntry = (ContinuationSystemUseEntry)entry; + break; + + case "ES": + ExtensionSelectSystemUseEntry esEntry = (ExtensionSelectSystemUseEntry)entry; + extension = context.SuspExtensions[esEntry.SelectedExtension]; + break; + + case "PD": + break; + + case "SP": + case "ER": + StoreEntry(null, entry); + break; + + default: + StoreEntry(extension, entry); + break; + } + } + + return contEntry; + } + + private void StoreEntry(SuspExtension extension, SystemUseEntry entry) + { + string extensionId = extension == null ? string.Empty : extension.Identifier; + + Dictionary> extensionEntries; + if (!_records.TryGetValue(extensionId, out extensionEntries)) + { + extensionEntries = new Dictionary>(); + _records.Add(extensionId, extensionEntries); + } + + List entries; + if (!extensionEntries.TryGetValue(entry.Name, out entries)) + { + entries = new List(); + extensionEntries.Add(entry.Name, entries); + } + + entries.Add(entry); + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/Susp/SystemUseEntry.cs b/DiscUtils/Iso9660Ps1/Susp/SystemUseEntry.cs new file mode 100644 index 0000000..91877ca --- /dev/null +++ b/DiscUtils/Iso9660Ps1/Susp/SystemUseEntry.cs @@ -0,0 +1,105 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.Text; +using DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal abstract class SystemUseEntry + { + public string Name; + public byte Version; + + public static SystemUseEntry Parse(byte[] data, int offset, Encoding encoding, SuspExtension extension, + out byte length) + { + if (data[offset] == 0) + { + // A zero-byte here is invalid and indicates an incorrectly written SUSP field. + // Return null to indicate to the caller that SUSP parsing is terminated. + length = 0; + + return null; + } + + string name = EndianUtilities.BytesToString(data, offset, 2); + length = data[offset + 2]; + byte version = data[offset + 3]; + + switch (name) + { + case "CE": + return new ContinuationSystemUseEntry(name, length, version, data, offset); + + case "PD": + return new PaddingSystemUseEntry(name, length, version); + + case "SP": + return new SharingProtocolSystemUseEntry(name, length, version, data, offset); + + case "ST": + // Termination entry. There's no point in storing or validating this one. + // Return null to indicate to the caller that SUSP parsing is terminated. + return null; + + case "ER": + return new ExtensionSystemUseEntry(name, length, version, data, offset, encoding); + + case "ES": + return new ExtensionSelectSystemUseEntry(name, length, version, data, offset); + + case "AA": + case "AB": + case "AS": + // Placeholder support for Apple and Amiga extension records. + return new GenericSystemUseEntry(name, length, version, data, offset); + + default: + if (extension == null) + { + return new GenericSystemUseEntry(name, length, version, data, offset); + } + + return extension.Parse(name, length, version, data, offset, encoding); + } + } + + protected void CheckAndSetCommonProperties(string name, byte length, byte version, byte minLength, byte maxVersion) + { + if (length < minLength) + { + throw new InvalidDataException("Invalid SUSP " + Name + " entry - too short, only " + length + " bytes"); + } + + if (version > maxVersion || version == 0) + { + throw new NotSupportedException("Unknown SUSP " + Name + " entry version: " + version); + } + + Name = name; + Version = version; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/VfsCDReader.cs b/DiscUtils/Iso9660Ps1/VfsCDReader.cs new file mode 100644 index 0000000..c694ec3 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/VfsCDReader.cs @@ -0,0 +1,530 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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.IO; +using DiscUtils.Streams; +using DiscUtils.Vfs; + +namespace DiscUtils.Iso9660Ps1 +{ + internal class VfsCDReader : VfsReadOnlyFileSystem, + IClusterBasedFileSystem, IUnixFileSystem + { + private static readonly Iso9660Variant[] DefaultVariantsNoJoliet = { Iso9660Variant.RockRidge, Iso9660Variant.Iso9660 }; + + private static readonly Iso9660Variant[] DefaultVariantsWithJoliet = { Iso9660Variant.Joliet, Iso9660Variant.RockRidge, Iso9660Variant.Iso9660 }; + + private byte[] _bootCatalog; + private readonly BootVolumeDescriptor _bootVolDesc; + + private readonly Stream _data; + private readonly bool _hideVersions; + protected readonly int _sectorSize; + + /// + /// Initializes a new instance of the VfsCDReader class. + /// + /// The stream to read the ISO image from. + /// Whether to read Joliet extensions. + /// Hides version numbers (e.g. ";1") from the end of files. + public VfsCDReader(Stream data, bool joliet, bool hideVersions, int sectorSize) + : this(data, joliet ? DefaultVariantsWithJoliet : DefaultVariantsNoJoliet, hideVersions, sectorSize) { + } + + /// + /// Initializes a new instance of the VfsCDReader class. + /// + /// The stream to read the ISO image from. + /// Which possible file system variants to use, and with which priority. + /// Hides version numbers (e.g. ";1") from the end of files. + /// + /// + /// The implementation considers each of the file system variants in variantProperties and selects + /// the first which is determined to be present. In this example Joliet, then Rock Ridge, then vanilla + /// Iso9660 will be considered: + /// + /// + /// VfsCDReader(stream, new Iso9660Variant[] {Joliet, RockRidge, Iso9660}, true); + /// + /// The Iso9660 variant should normally be specified as the final entry in the list. Placing it earlier + /// in the list will effectively mask later items and not including it may prevent some ISOs from being read. + /// + public VfsCDReader(Stream data, Iso9660Variant[] variantPriorities, bool hideVersions, int sectorSize) + : base(new DiscFileSystemOptions()) + { + _data = data; + _sectorSize = sectorSize; + _hideVersions = hideVersions; + + long vdpos = _sectorSize * 16; // Skip lead-in + + byte[] buffer = new byte[_sectorSize]; + + long pvdPos = 0; + long svdPos = 0; + + BaseVolumeDescriptor bvd; + do + { + data.Position = vdpos; + int numRead = data.Read(buffer, 0, _sectorSize); + if (numRead != _sectorSize) + { + break; + } + + var offset = 24; + + bvd = new BaseVolumeDescriptor(buffer, offset); + + if (bvd.StandardIdentifier != BaseVolumeDescriptor.Iso9660StandardIdentifier) + { + throw new InvalidFileSystemException("Volume is not ISO-9660"); + } + + switch (bvd.VolumeDescriptorType) + { + case VolumeDescriptorType.Boot: + _bootVolDesc = new BootVolumeDescriptor(buffer, offset); + if (_bootVolDesc.SystemId != BootVolumeDescriptor.ElToritoSystemIdentifier) + { + _bootVolDesc = null; + } + + break; + + case VolumeDescriptorType.Primary: // Primary Vol Descriptor + pvdPos = vdpos; + break; + + case VolumeDescriptorType.Supplementary: // Supplementary Vol Descriptor + svdPos = vdpos; + break; + + case VolumeDescriptorType.Partition: // Volume Partition Descriptor + break; + case VolumeDescriptorType.SetTerminator: // Volume Descriptor Set Terminator + break; + } + + vdpos += _sectorSize; + } while (bvd.VolumeDescriptorType != VolumeDescriptorType.SetTerminator); + + ActiveVariant = Iso9660Variant.None; + foreach (Iso9660Variant variant in variantPriorities) + { + switch (variant) + { + case Iso9660Variant.Joliet: + if (svdPos != 0) + { + data.Position = svdPos; + data.Read(buffer, 0, _sectorSize); + SupplementaryVolumeDescriptor volDesc = new SupplementaryVolumeDescriptor(buffer, 0); + + Context = new IsoContext(_sectorSize) { VolumeDescriptor = volDesc, DataStream = _data }; + RootDirectory = new ReaderDirectory(Context, + new ReaderDirEntry(Context, volDesc.RootDirectory)); + ActiveVariant = Iso9660Variant.Iso9660; + } + + break; + + case Iso9660Variant.RockRidge: + case Iso9660Variant.Iso9660: + if (pvdPos != 0) + { + data.Position = pvdPos + 24; + data.Read(buffer, 0, _sectorSize); + PrimaryVolumeDescriptor volDesc = new PrimaryVolumeDescriptor(buffer, 0); + + volDesc.LogicalBlockSize = 2352; + + IsoContext context = new IsoContext(_sectorSize) { VolumeDescriptor = volDesc, DataStream = _data }; + DirectoryRecord rootSelfRecord = ReadRootSelfRecord(context); + + InitializeSusp(context, rootSelfRecord); + + if (variant == Iso9660Variant.Iso9660 + || + (variant == Iso9660Variant.RockRidge && + !string.IsNullOrEmpty(context.RockRidgeIdentifier))) + { + Context = context; + RootDirectory = new ReaderDirectory(context, new ReaderDirEntry(context, rootSelfRecord)); + ActiveVariant = variant; + } + } + + break; + } + + if (ActiveVariant != Iso9660Variant.None) + { + break; + } + } + + if (ActiveVariant == Iso9660Variant.None) + { + throw new IOException("None of the permitted ISO9660 file system variants was detected"); + } + } + + public Iso9660Variant ActiveVariant { get; } + + public BootDeviceEmulation BootEmulation + { + get + { + BootInitialEntry initialEntry = GetBootInitialEntry(); + if (initialEntry != null) + { + return initialEntry.BootMediaType; + } + + return BootDeviceEmulation.NoEmulation; + } + } + + public long BootImageStart + { + get + { + BootInitialEntry initialEntry = GetBootInitialEntry(); + if (initialEntry != null) + { + return initialEntry.ImageStart * _sectorSize; + } + return 0; + } + } + + public int BootLoadSegment + { + get + { + BootInitialEntry initialEntry = GetBootInitialEntry(); + if (initialEntry != null) + { + return initialEntry.LoadSegment; + } + + return 0; + } + } + + /// + /// Provides the friendly name for the CD filesystem. + /// + public override string FriendlyName + { + get { return "ISO 9660 (CD-ROM)"; } + } + + public bool HasBootImage + { + get + { + if (_bootVolDesc == null) + { + return false; + } + + byte[] bootCatalog = GetBootCatalog(); + if (bootCatalog == null) + { + return false; + } + + BootValidationEntry entry = new BootValidationEntry(bootCatalog, 0); + return entry.ChecksumValid; + } + } + + /// + /// Gets the Volume Identifier. + /// + public override string VolumeLabel + { + get { return Context.VolumeDescriptor.VolumeIdentifier; } + } + + public long ClusterSize + { + get { return _sectorSize; } + } + + public long TotalClusters + { + get { return Context.VolumeDescriptor.VolumeSpaceSize; } + } + + public long ClusterToOffset(long cluster) + { + return cluster * ClusterSize; + } + + public long OffsetToCluster(long offset) + { + return offset / ClusterSize; + } + + /// + /// Size of the Filesystem in bytes + /// + public override long Size + { + get { throw new NotSupportedException("Filesystem size is not (yet) supported"); } + } + + /// + /// Used space of the Filesystem in bytes + /// + public override long UsedSpace + { + get { throw new NotSupportedException("Filesystem size is not (yet) supported"); } + } + + /// + /// Available space of the Filesystem in bytes + /// + public override long AvailableSpace + { + get { throw new NotSupportedException("Filesystem size is not (yet) supported"); } + } + + public Range[] PathToClusters(string path) + { + ReaderDirEntry entry = GetDirectoryEntry(path); + if (entry == null) + { + throw new FileNotFoundException("File not found", path); + } + + if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + { + throw new NotSupportedException("Non-contiguous extents not supported"); + } + + return new[] + { + new Range(entry.Record.LocationOfExtent, + MathUtilities.Ceil(entry.Record.DataLength, _sectorSize)) + }; + } + + public StreamExtent[] PathToExtents(string path) + { + ReaderDirEntry entry = GetDirectoryEntry(path); + if (entry == null) + { + throw new FileNotFoundException("File not found", path); + } + + if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + { + throw new NotSupportedException("Non-contiguous extents not supported"); + } + + return new[] + { new StreamExtent(entry.Record.LocationOfExtent * _sectorSize, entry.Record.DataLength) }; + } + + public ClusterMap BuildClusterMap() + { + long totalClusters = TotalClusters; + ClusterRoles[] clusterToRole = new ClusterRoles[totalClusters]; + object[] clusterToFileId = new object[totalClusters]; + Dictionary fileIdToPaths = new Dictionary(); + + ForAllDirEntries( + string.Empty, + (path, entry) => + { + string[] paths = null; + if (fileIdToPaths.ContainsKey(entry.UniqueCacheId)) + { + paths = fileIdToPaths[entry.UniqueCacheId]; + } + + if (paths == null) + { + fileIdToPaths[entry.UniqueCacheId] = new[] { path }; + } + else + { + string[] newPaths = new string[paths.Length + 1]; + Array.Copy(paths, newPaths, paths.Length); + newPaths[paths.Length] = path; + fileIdToPaths[entry.UniqueCacheId] = newPaths; + } + + if (entry.Record.FileUnitSize != 0 || entry.Record.InterleaveGapSize != 0) + { + throw new NotSupportedException("Non-contiguous extents not supported"); + } + + long clusters = MathUtilities.Ceil(entry.Record.DataLength, _sectorSize); + for (long i = 0; i < clusters; ++i) + { + clusterToRole[i + entry.Record.LocationOfExtent] = ClusterRoles.DataFile; + clusterToFileId[i + entry.Record.LocationOfExtent] = entry.UniqueCacheId; + } + }); + + return new ClusterMap(clusterToRole, clusterToFileId, fileIdToPaths); + } + + public UnixFileSystemInfo GetUnixFileInfo(string path) + { + File file = GetFile(path); + return file.UnixFileInfo; + } + + public Stream OpenBootImage() + { + BootInitialEntry initialEntry = GetBootInitialEntry(); + if (initialEntry != null) + { + return new SubStream(_data, initialEntry.ImageStart * _sectorSize, + initialEntry.SectorCount * Sizes.Sector); + } + throw new InvalidOperationException("No valid boot image"); + } + + protected override File ConvertDirEntryToFile(ReaderDirEntry dirEntry) + { + if (dirEntry.IsDirectory) + { + return new ReaderDirectory(Context, dirEntry); + } + return new File(Context, dirEntry); + } + + protected override string FormatFileName(string name) + { + if (_hideVersions) + { + int pos = name.LastIndexOf(';'); + if (pos > 0) + { + return name.Substring(0, pos); + } + } + + return name; + } + + private static void InitializeSusp(IsoContext context, DirectoryRecord rootSelfRecord) + { + // Stage 1 - SUSP present? + List extensions = new List(); + if (!SuspRecords.DetectSharingProtocol(rootSelfRecord.SystemUseData, 0)) + { + context.SuspExtensions = new List(); + context.SuspDetected = false; + return; + } + context.SuspDetected = true; + + SuspRecords suspRecords = new SuspRecords(context, rootSelfRecord.SystemUseData, 0); + + // Stage 2 - Init general SUSP params + SharingProtocolSystemUseEntry spEntry = + (SharingProtocolSystemUseEntry)suspRecords.GetEntries(null, "SP")[0]; + context.SuspSkipBytes = spEntry.SystemAreaSkip; + + // Stage 3 - Init extensions + List extensionEntries = suspRecords.GetEntries(null, "ER"); + if (extensionEntries != null) + { + foreach (ExtensionSystemUseEntry extension in extensionEntries) + { + switch (extension.ExtensionIdentifier) + { + case "RRIP_1991A": + case "IEEE_P1282": + case "IEEE_1282": + extensions.Add(new RockRidgeExtension(extension.ExtensionIdentifier)); + context.RockRidgeIdentifier = extension.ExtensionIdentifier; + break; + + default: + extensions.Add(new GenericSuspExtension(extension.ExtensionIdentifier)); + break; + } + } + } + else if (suspRecords.GetEntries(null, "RR") != null) + { + // Some ISO creators don't add the 'ER' record for RockRidge, but write the (legacy) + // RR record anyway + extensions.Add(new RockRidgeExtension("RRIP_1991A")); + context.RockRidgeIdentifier = "RRIP_1991A"; + } + + context.SuspExtensions = extensions; + } + + private static DirectoryRecord ReadRootSelfRecord(IsoContext context) + { + context.DataStream.Position = context.VolumeDescriptor.RootDirectory.LocationOfExtent * + context.VolumeDescriptor.LogicalBlockSize + 24; + byte[] firstSector = StreamUtilities.ReadExact(context.DataStream, context.VolumeDescriptor.LogicalBlockSize); + + DirectoryRecord rootSelfRecord; + DirectoryRecord.ReadFrom(firstSector, 0, context.VolumeDescriptor.CharacterEncoding, out rootSelfRecord); + return rootSelfRecord; + } + + private BootInitialEntry GetBootInitialEntry() + { + byte[] bootCatalog = GetBootCatalog(); + if (bootCatalog == null) + { + return null; + } + + BootValidationEntry validationEntry = new BootValidationEntry(bootCatalog, 0); + if (!validationEntry.ChecksumValid) + { + return null; + } + + return new BootInitialEntry(bootCatalog, 0x20); + } + + private byte[] GetBootCatalog() + { + if (_bootCatalog == null && _bootVolDesc != null) + { + _data.Position = _bootVolDesc.CatalogSector * _sectorSize; + _bootCatalog = StreamUtilities.ReadExact(_data, _sectorSize); + } + + return _bootCatalog; + } + } +} diff --git a/DiscUtils/Iso9660Ps1/VolumeDescriptorDiskRegion.cs b/DiscUtils/Iso9660Ps1/VolumeDescriptorDiskRegion.cs new file mode 100644 index 0000000..4be6e93 --- /dev/null +++ b/DiscUtils/Iso9660Ps1/VolumeDescriptorDiskRegion.cs @@ -0,0 +1,60 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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 DiscUtils.Streams; + +namespace DiscUtils.Iso9660Ps1 +{ + internal abstract class VolumeDescriptorDiskRegion : BuilderExtent + { + private byte[] _readCache; + + public VolumeDescriptorDiskRegion(long start, int sectorSize) + : base(start, sectorSize) {} + + public override void Dispose() {} + + public override void PrepareForRead() + { + _readCache = GetBlockData(); + } + + public override int Read(long diskOffset, byte[] buffer, int offset, int count) + { + long relPos = diskOffset - Start; + + int numRead = (int)Math.Min(count, _readCache.Length - relPos); + + Array.Copy(_readCache, (int)relPos, buffer, offset, numRead); + + return numRead; + } + + public override void DisposeReadState() + { + _readCache = null; + } + + protected abstract byte[] GetBlockData(); + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/VolumeDescriptorSetTerminator.cs b/DiscUtils/Iso9660Ps1/VolumeDescriptorSetTerminator.cs new file mode 100644 index 0000000..98ed18e --- /dev/null +++ b/DiscUtils/Iso9660Ps1/VolumeDescriptorSetTerminator.cs @@ -0,0 +1,30 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal class VolumeDescriptorSetTerminator : BaseVolumeDescriptor + { + public VolumeDescriptorSetTerminator(int sectorSize) + : base(VolumeDescriptorType.SetTerminator, 1, sectorSize) {} + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/VolumeDescriptorSetTerminatorRegion.cs b/DiscUtils/Iso9660Ps1/VolumeDescriptorSetTerminatorRegion.cs new file mode 100644 index 0000000..22fdaec --- /dev/null +++ b/DiscUtils/Iso9660Ps1/VolumeDescriptorSetTerminatorRegion.cs @@ -0,0 +1,42 @@ +// +// Copyright (c) 2008-2011, Kenneth Bell +// +// 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. +// + +namespace DiscUtils.Iso9660Ps1 +{ + internal class VolumeDescriptorSetTerminatorRegion : VolumeDescriptorDiskRegion + { + private readonly VolumeDescriptorSetTerminator _descriptor; + + public VolumeDescriptorSetTerminatorRegion(VolumeDescriptorSetTerminator descriptor, long start, int sectorSize) + : base(start, sectorSize) + { + _descriptor = descriptor; + } + + protected override byte[] GetBlockData() + { + byte[] buffer = new byte[Length]; + _descriptor.WriteTo(buffer, 0); + return buffer; + } + } +} \ No newline at end of file diff --git a/DiscUtils/Iso9660Ps1/VolumeDescriptorType.cs b/DiscUtils/Iso9660Ps1/VolumeDescriptorType.cs new file mode 100644 index 0000000..1505f7e --- /dev/null +++ b/DiscUtils/Iso9660Ps1/VolumeDescriptorType.cs @@ -0,0 +1,11 @@ +namespace DiscUtils.Iso9660Ps1 +{ + internal enum VolumeDescriptorType : byte + { + Boot = 0, + Primary = 1, + Supplementary = 2, + Partition = 3, + SetTerminator = 255 + } +} \ No newline at end of file diff --git a/DiscUtils/Core/LogicalDiskManager/ComponentRecord.cs b/DiscUtils/LogicalDiskManager/ComponentRecord.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/ComponentRecord.cs rename to DiscUtils/LogicalDiskManager/ComponentRecord.cs diff --git a/DiscUtils/Core/LogicalDiskManager/Database.cs b/DiscUtils/LogicalDiskManager/Database.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/Database.cs rename to DiscUtils/LogicalDiskManager/Database.cs diff --git a/DiscUtils/Core/LogicalDiskManager/DatabaseHeader.cs b/DiscUtils/LogicalDiskManager/DatabaseHeader.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/DatabaseHeader.cs rename to DiscUtils/LogicalDiskManager/DatabaseHeader.cs diff --git a/DiscUtils/Core/LogicalDiskManager/DatabaseRecord.cs b/DiscUtils/LogicalDiskManager/DatabaseRecord.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/DatabaseRecord.cs rename to DiscUtils/LogicalDiskManager/DatabaseRecord.cs diff --git a/DiscUtils/Core/LogicalDiskManager/DiskGroupRecord.cs b/DiscUtils/LogicalDiskManager/DiskGroupRecord.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/DiskGroupRecord.cs rename to DiscUtils/LogicalDiskManager/DiskGroupRecord.cs diff --git a/DiscUtils/Core/LogicalDiskManager/DiskRecord.cs b/DiscUtils/LogicalDiskManager/DiskRecord.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/DiskRecord.cs rename to DiscUtils/LogicalDiskManager/DiskRecord.cs diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicDisk.cs b/DiscUtils/LogicalDiskManager/DynamicDisk.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/DynamicDisk.cs rename to DiscUtils/LogicalDiskManager/DynamicDisk.cs diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicDiskGroup.cs b/DiscUtils/LogicalDiskManager/DynamicDiskGroup.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/DynamicDiskGroup.cs rename to DiscUtils/LogicalDiskManager/DynamicDiskGroup.cs diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicDiskManager.cs b/DiscUtils/LogicalDiskManager/DynamicDiskManager.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/DynamicDiskManager.cs rename to DiscUtils/LogicalDiskManager/DynamicDiskManager.cs diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicDiskManagerFactory.cs b/DiscUtils/LogicalDiskManager/DynamicDiskManagerFactory.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/DynamicDiskManagerFactory.cs rename to DiscUtils/LogicalDiskManager/DynamicDiskManagerFactory.cs diff --git a/DiscUtils/Core/LogicalDiskManager/DynamicVolume.cs b/DiscUtils/LogicalDiskManager/DynamicVolume.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/DynamicVolume.cs rename to DiscUtils/LogicalDiskManager/DynamicVolume.cs diff --git a/DiscUtils/Core/LogicalDiskManager/ExtentMergeType.cs b/DiscUtils/LogicalDiskManager/ExtentMergeType.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/ExtentMergeType.cs rename to DiscUtils/LogicalDiskManager/ExtentMergeType.cs diff --git a/DiscUtils/Core/LogicalDiskManager/ExtentRecord.cs b/DiscUtils/LogicalDiskManager/ExtentRecord.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/ExtentRecord.cs rename to DiscUtils/LogicalDiskManager/ExtentRecord.cs diff --git a/DiscUtils/Core/LogicalDiskManager/PrivateHeader.cs b/DiscUtils/LogicalDiskManager/PrivateHeader.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/PrivateHeader.cs rename to DiscUtils/LogicalDiskManager/PrivateHeader.cs diff --git a/DiscUtils/Core/LogicalDiskManager/RecordType.cs b/DiscUtils/LogicalDiskManager/RecordType.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/RecordType.cs rename to DiscUtils/LogicalDiskManager/RecordType.cs diff --git a/DiscUtils/Core/LogicalDiskManager/TocBlock.cs b/DiscUtils/LogicalDiskManager/TocBlock.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/TocBlock.cs rename to DiscUtils/LogicalDiskManager/TocBlock.cs diff --git a/DiscUtils/Core/LogicalDiskManager/VolumeRecord.cs b/DiscUtils/LogicalDiskManager/VolumeRecord.cs similarity index 100% rename from DiscUtils/Core/LogicalDiskManager/VolumeRecord.cs rename to DiscUtils/LogicalDiskManager/VolumeRecord.cs diff --git a/DiscUtils/Core/LogicalVolumeInfo.cs b/DiscUtils/LogicalVolumeInfo.cs similarity index 100% rename from DiscUtils/Core/LogicalVolumeInfo.cs rename to DiscUtils/LogicalVolumeInfo.cs diff --git a/DiscUtils/Core/LogicalVolumeStatus.cs b/DiscUtils/LogicalVolumeStatus.cs similarity index 100% rename from DiscUtils/Core/LogicalVolumeStatus.cs rename to DiscUtils/LogicalVolumeStatus.cs diff --git a/DiscUtils/Core/NativeFileSystem.cs b/DiscUtils/NativeFileSystem.cs similarity index 100% rename from DiscUtils/Core/NativeFileSystem.cs rename to DiscUtils/NativeFileSystem.cs diff --git a/DiscUtils/Core/Partitions/BiosExtendedPartitionTable.cs b/DiscUtils/Partitions/BiosExtendedPartitionTable.cs similarity index 100% rename from DiscUtils/Core/Partitions/BiosExtendedPartitionTable.cs rename to DiscUtils/Partitions/BiosExtendedPartitionTable.cs diff --git a/DiscUtils/Core/Partitions/BiosPartitionInfo.cs b/DiscUtils/Partitions/BiosPartitionInfo.cs similarity index 100% rename from DiscUtils/Core/Partitions/BiosPartitionInfo.cs rename to DiscUtils/Partitions/BiosPartitionInfo.cs diff --git a/DiscUtils/Core/Partitions/BiosPartitionRecord.cs b/DiscUtils/Partitions/BiosPartitionRecord.cs similarity index 100% rename from DiscUtils/Core/Partitions/BiosPartitionRecord.cs rename to DiscUtils/Partitions/BiosPartitionRecord.cs diff --git a/DiscUtils/Core/Partitions/BiosPartitionTable.cs b/DiscUtils/Partitions/BiosPartitionTable.cs similarity index 100% rename from DiscUtils/Core/Partitions/BiosPartitionTable.cs rename to DiscUtils/Partitions/BiosPartitionTable.cs diff --git a/DiscUtils/Core/Partitions/BiosPartitionTypes.cs b/DiscUtils/Partitions/BiosPartitionTypes.cs similarity index 100% rename from DiscUtils/Core/Partitions/BiosPartitionTypes.cs rename to DiscUtils/Partitions/BiosPartitionTypes.cs diff --git a/DiscUtils/Core/Partitions/BiosPartitionedDiskBuilder.cs b/DiscUtils/Partitions/BiosPartitionedDiskBuilder.cs similarity index 100% rename from DiscUtils/Core/Partitions/BiosPartitionedDiskBuilder.cs rename to DiscUtils/Partitions/BiosPartitionedDiskBuilder.cs diff --git a/DiscUtils/Core/Partitions/DefaultPartitionTableFactory.cs b/DiscUtils/Partitions/DefaultPartitionTableFactory.cs similarity index 100% rename from DiscUtils/Core/Partitions/DefaultPartitionTableFactory.cs rename to DiscUtils/Partitions/DefaultPartitionTableFactory.cs diff --git a/DiscUtils/Core/Partitions/GptEntry.cs b/DiscUtils/Partitions/GptEntry.cs similarity index 100% rename from DiscUtils/Core/Partitions/GptEntry.cs rename to DiscUtils/Partitions/GptEntry.cs diff --git a/DiscUtils/Core/Partitions/GptHeader.cs b/DiscUtils/Partitions/GptHeader.cs similarity index 100% rename from DiscUtils/Core/Partitions/GptHeader.cs rename to DiscUtils/Partitions/GptHeader.cs diff --git a/DiscUtils/Core/Partitions/GuidPartitionInfo.cs b/DiscUtils/Partitions/GuidPartitionInfo.cs similarity index 100% rename from DiscUtils/Core/Partitions/GuidPartitionInfo.cs rename to DiscUtils/Partitions/GuidPartitionInfo.cs diff --git a/DiscUtils/Core/Partitions/GuidPartitionTable.cs b/DiscUtils/Partitions/GuidPartitionTable.cs similarity index 100% rename from DiscUtils/Core/Partitions/GuidPartitionTable.cs rename to DiscUtils/Partitions/GuidPartitionTable.cs diff --git a/DiscUtils/Core/Partitions/GuidPartitionTypes.cs b/DiscUtils/Partitions/GuidPartitionTypes.cs similarity index 100% rename from DiscUtils/Core/Partitions/GuidPartitionTypes.cs rename to DiscUtils/Partitions/GuidPartitionTypes.cs diff --git a/DiscUtils/Core/Partitions/PartitionInfo.cs b/DiscUtils/Partitions/PartitionInfo.cs similarity index 100% rename from DiscUtils/Core/Partitions/PartitionInfo.cs rename to DiscUtils/Partitions/PartitionInfo.cs diff --git a/DiscUtils/Core/Partitions/PartitionTable.cs b/DiscUtils/Partitions/PartitionTable.cs similarity index 100% rename from DiscUtils/Core/Partitions/PartitionTable.cs rename to DiscUtils/Partitions/PartitionTable.cs diff --git a/DiscUtils/Core/Partitions/PartitionTableFactory.cs b/DiscUtils/Partitions/PartitionTableFactory.cs similarity index 100% rename from DiscUtils/Core/Partitions/PartitionTableFactory.cs rename to DiscUtils/Partitions/PartitionTableFactory.cs diff --git a/DiscUtils/Core/Partitions/PartitionTableFactoryAttribute.cs b/DiscUtils/Partitions/PartitionTableFactoryAttribute.cs similarity index 100% rename from DiscUtils/Core/Partitions/PartitionTableFactoryAttribute.cs rename to DiscUtils/Partitions/PartitionTableFactoryAttribute.cs diff --git a/DiscUtils/Core/Partitions/WellKnownPartitionType.cs b/DiscUtils/Partitions/WellKnownPartitionType.cs similarity index 100% rename from DiscUtils/Core/Partitions/WellKnownPartitionType.cs rename to DiscUtils/Partitions/WellKnownPartitionType.cs diff --git a/DiscUtils/Core/PhysicalVolumeInfo.cs b/DiscUtils/PhysicalVolumeInfo.cs similarity index 100% rename from DiscUtils/Core/PhysicalVolumeInfo.cs rename to DiscUtils/PhysicalVolumeInfo.cs diff --git a/DiscUtils/Core/PhysicalVolumeType.cs b/DiscUtils/PhysicalVolumeType.cs similarity index 100% rename from DiscUtils/Core/PhysicalVolumeType.cs rename to DiscUtils/PhysicalVolumeType.cs diff --git a/DiscUtils/Core/Plist.cs b/DiscUtils/Plist.cs similarity index 100% rename from DiscUtils/Core/Plist.cs rename to DiscUtils/Plist.cs diff --git a/DiscUtils/Core/Raw/Disk.cs b/DiscUtils/Raw/Disk.cs similarity index 100% rename from DiscUtils/Core/Raw/Disk.cs rename to DiscUtils/Raw/Disk.cs diff --git a/DiscUtils/Core/Raw/DiskFactory.cs b/DiscUtils/Raw/DiskFactory.cs similarity index 100% rename from DiscUtils/Core/Raw/DiskFactory.cs rename to DiscUtils/Raw/DiskFactory.cs diff --git a/DiscUtils/Core/Raw/DiskImageFile.cs b/DiscUtils/Raw/DiskImageFile.cs similarity index 100% rename from DiscUtils/Core/Raw/DiskImageFile.cs rename to DiscUtils/Raw/DiskImageFile.cs diff --git a/DiscUtils/Core/ReadOnlyDiscFileSystem.cs b/DiscUtils/ReadOnlyDiscFileSystem.cs similarity index 100% rename from DiscUtils/Core/ReadOnlyDiscFileSystem.cs rename to DiscUtils/ReadOnlyDiscFileSystem.cs diff --git a/DiscUtils/Core/ReparsePoint.cs b/DiscUtils/ReparsePoint.cs similarity index 100% rename from DiscUtils/Core/ReparsePoint.cs rename to DiscUtils/ReparsePoint.cs diff --git a/DiscUtils/Core/ReportLevels.cs b/DiscUtils/ReportLevels.cs similarity index 100% rename from DiscUtils/Core/ReportLevels.cs rename to DiscUtils/ReportLevels.cs diff --git a/DiscUtils/Core/Setup/FileOpenEventArgs.cs b/DiscUtils/Setup/FileOpenEventArgs.cs similarity index 100% rename from DiscUtils/Core/Setup/FileOpenEventArgs.cs rename to DiscUtils/Setup/FileOpenEventArgs.cs diff --git a/DiscUtils/Core/Setup/SetupHelper.cs b/DiscUtils/Setup/SetupHelper.cs similarity index 100% rename from DiscUtils/Core/Setup/SetupHelper.cs rename to DiscUtils/Setup/SetupHelper.cs diff --git a/DiscUtils/Streams/DiscUtils.Streams.csproj b/DiscUtils/Streams/DiscUtils.Streams.csproj deleted file mode 100644 index 0083bf2..0000000 --- a/DiscUtils/Streams/DiscUtils.Streams.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - - DiscUtils Streams - Kenneth Bell;LordMike;Bianco Veigel - DiscUtils;Streams - - - diff --git a/DiscUtils/Core/System/DateTimeOffsetExtensions.cs b/DiscUtils/System/DateTimeOffsetExtensions.cs similarity index 100% rename from DiscUtils/Core/System/DateTimeOffsetExtensions.cs rename to DiscUtils/System/DateTimeOffsetExtensions.cs diff --git a/DiscUtils/Core/System/ExtensionAttribute.cs b/DiscUtils/System/ExtensionAttribute.cs similarity index 100% rename from DiscUtils/Core/System/ExtensionAttribute.cs rename to DiscUtils/System/ExtensionAttribute.cs diff --git a/DiscUtils/Core/System/HashSet.cs b/DiscUtils/System/HashSet.cs similarity index 100% rename from DiscUtils/Core/System/HashSet.cs rename to DiscUtils/System/HashSet.cs diff --git a/DiscUtils/Core/System/Tuple.cs b/DiscUtils/System/Tuple.cs similarity index 100% rename from DiscUtils/Core/System/Tuple.cs rename to DiscUtils/System/Tuple.cs diff --git a/DiscUtils/Core/TimeConverter.cs b/DiscUtils/TimeConverter.cs similarity index 100% rename from DiscUtils/Core/TimeConverter.cs rename to DiscUtils/TimeConverter.cs diff --git a/DiscUtils/Core/UnixFilePermissions.cs b/DiscUtils/UnixFilePermissions.cs similarity index 100% rename from DiscUtils/Core/UnixFilePermissions.cs rename to DiscUtils/UnixFilePermissions.cs diff --git a/DiscUtils/Core/UnixFileSystemInfo.cs b/DiscUtils/UnixFileSystemInfo.cs similarity index 100% rename from DiscUtils/Core/UnixFileSystemInfo.cs rename to DiscUtils/UnixFileSystemInfo.cs diff --git a/DiscUtils/Core/UnixFileType.cs b/DiscUtils/UnixFileType.cs similarity index 100% rename from DiscUtils/Core/UnixFileType.cs rename to DiscUtils/UnixFileType.cs diff --git a/DiscUtils/Core/Vfs/IVfsDirectory.cs b/DiscUtils/Vfs/IVfsDirectory.cs similarity index 100% rename from DiscUtils/Core/Vfs/IVfsDirectory.cs rename to DiscUtils/Vfs/IVfsDirectory.cs diff --git a/DiscUtils/Core/Vfs/IVfsFile.cs b/DiscUtils/Vfs/IVfsFile.cs similarity index 100% rename from DiscUtils/Core/Vfs/IVfsFile.cs rename to DiscUtils/Vfs/IVfsFile.cs diff --git a/DiscUtils/Core/Vfs/IVfsFileWithStreams.cs b/DiscUtils/Vfs/IVfsFileWithStreams.cs similarity index 100% rename from DiscUtils/Core/Vfs/IVfsFileWithStreams.cs rename to DiscUtils/Vfs/IVfsFileWithStreams.cs diff --git a/DiscUtils/Core/Vfs/IVfsSymlink.cs b/DiscUtils/Vfs/IVfsSymlink.cs similarity index 100% rename from DiscUtils/Core/Vfs/IVfsSymlink.cs rename to DiscUtils/Vfs/IVfsSymlink.cs diff --git a/DiscUtils/Core/Vfs/VfsContext.cs b/DiscUtils/Vfs/VfsContext.cs similarity index 100% rename from DiscUtils/Core/Vfs/VfsContext.cs rename to DiscUtils/Vfs/VfsContext.cs diff --git a/DiscUtils/Core/Vfs/VfsDirEntry.cs b/DiscUtils/Vfs/VfsDirEntry.cs similarity index 100% rename from DiscUtils/Core/Vfs/VfsDirEntry.cs rename to DiscUtils/Vfs/VfsDirEntry.cs diff --git a/DiscUtils/Core/Vfs/VfsFileSystem.cs b/DiscUtils/Vfs/VfsFileSystem.cs similarity index 100% rename from DiscUtils/Core/Vfs/VfsFileSystem.cs rename to DiscUtils/Vfs/VfsFileSystem.cs diff --git a/DiscUtils/Core/Vfs/VfsFileSystemFacade.cs b/DiscUtils/Vfs/VfsFileSystemFacade.cs similarity index 100% rename from DiscUtils/Core/Vfs/VfsFileSystemFacade.cs rename to DiscUtils/Vfs/VfsFileSystemFacade.cs diff --git a/DiscUtils/Core/Vfs/VfsFileSystemFactory.cs b/DiscUtils/Vfs/VfsFileSystemFactory.cs similarity index 100% rename from DiscUtils/Core/Vfs/VfsFileSystemFactory.cs rename to DiscUtils/Vfs/VfsFileSystemFactory.cs diff --git a/DiscUtils/Core/Vfs/VfsFileSystemFactoryAttribute.cs b/DiscUtils/Vfs/VfsFileSystemFactoryAttribute.cs similarity index 100% rename from DiscUtils/Core/Vfs/VfsFileSystemFactoryAttribute.cs rename to DiscUtils/Vfs/VfsFileSystemFactoryAttribute.cs diff --git a/DiscUtils/Core/Vfs/VfsFileSystemInfo.cs b/DiscUtils/Vfs/VfsFileSystemInfo.cs similarity index 100% rename from DiscUtils/Core/Vfs/VfsFileSystemInfo.cs rename to DiscUtils/Vfs/VfsFileSystemInfo.cs diff --git a/DiscUtils/Core/Vfs/VfsFileSystemOpener.cs b/DiscUtils/Vfs/VfsFileSystemOpener.cs similarity index 100% rename from DiscUtils/Core/Vfs/VfsFileSystemOpener.cs rename to DiscUtils/Vfs/VfsFileSystemOpener.cs diff --git a/DiscUtils/Core/Vfs/VfsReadOnlyFileSystem.cs b/DiscUtils/Vfs/VfsReadOnlyFileSystem.cs similarity index 100% rename from DiscUtils/Core/Vfs/VfsReadOnlyFileSystem.cs rename to DiscUtils/Vfs/VfsReadOnlyFileSystem.cs diff --git a/DiscUtils/Core/VirtualDisk.cs b/DiscUtils/VirtualDisk.cs similarity index 100% rename from DiscUtils/Core/VirtualDisk.cs rename to DiscUtils/VirtualDisk.cs diff --git a/DiscUtils/Core/VirtualDiskClass.cs b/DiscUtils/VirtualDiskClass.cs similarity index 100% rename from DiscUtils/Core/VirtualDiskClass.cs rename to DiscUtils/VirtualDiskClass.cs diff --git a/DiscUtils/Core/VirtualDiskExtent.cs b/DiscUtils/VirtualDiskExtent.cs similarity index 100% rename from DiscUtils/Core/VirtualDiskExtent.cs rename to DiscUtils/VirtualDiskExtent.cs diff --git a/DiscUtils/Core/VirtualDiskLayer.cs b/DiscUtils/VirtualDiskLayer.cs similarity index 100% rename from DiscUtils/Core/VirtualDiskLayer.cs rename to DiscUtils/VirtualDiskLayer.cs diff --git a/DiscUtils/Core/VirtualDiskManager.cs b/DiscUtils/VirtualDiskManager.cs similarity index 100% rename from DiscUtils/Core/VirtualDiskManager.cs rename to DiscUtils/VirtualDiskManager.cs diff --git a/DiscUtils/Core/VirtualDiskParameters.cs b/DiscUtils/VirtualDiskParameters.cs similarity index 100% rename from DiscUtils/Core/VirtualDiskParameters.cs rename to DiscUtils/VirtualDiskParameters.cs diff --git a/DiscUtils/Core/VirtualDiskTypeInfo.cs b/DiscUtils/VirtualDiskTypeInfo.cs similarity index 100% rename from DiscUtils/Core/VirtualDiskTypeInfo.cs rename to DiscUtils/VirtualDiskTypeInfo.cs diff --git a/DiscUtils/Core/VolumeInfo.cs b/DiscUtils/VolumeInfo.cs similarity index 100% rename from DiscUtils/Core/VolumeInfo.cs rename to DiscUtils/VolumeInfo.cs diff --git a/DiscUtils/Core/VolumeManager.cs b/DiscUtils/VolumeManager.cs similarity index 100% rename from DiscUtils/Core/VolumeManager.cs rename to DiscUtils/VolumeManager.cs diff --git a/DiscUtils/Core/WindowsFileInformation.cs b/DiscUtils/WindowsFileInformation.cs similarity index 100% rename from DiscUtils/Core/WindowsFileInformation.cs rename to DiscUtils/WindowsFileInformation.cs diff --git a/PopsBuilder/Pops/DiscInfo.cs b/PopsBuilder/Pops/DiscInfo.cs index 84b601f..ab8233b 100644 --- a/PopsBuilder/Pops/DiscInfo.cs +++ b/PopsBuilder/Pops/DiscInfo.cs @@ -1,4 +1,4 @@ -using DiscUtils.Iso9660; +using DiscUtils.Iso9660Ps1; using DiscUtils.Streams; using GameBuilder.Cue; using System; diff --git a/PopsBuilder/Pops/PsIsoImg.cs b/PopsBuilder/Pops/PsIsoImg.cs index 740a3c7..a80c0a9 100644 --- a/PopsBuilder/Pops/PsIsoImg.cs +++ b/PopsBuilder/Pops/PsIsoImg.cs @@ -1,12 +1,9 @@ -using Org.BouncyCastle.Crypto.Paddings; -using GameBuilder.Atrac3; +using GameBuilder.Atrac3; using GameBuilder.Cue; using GameBuilder.Psp; using PspCrypto; using System; using System.Net; -using System.Runtime.InteropServices; -using System.Security.Cryptography.X509Certificates; using GameBuilder.Progress; namespace GameBuilder.Pops diff --git a/PopsBuilder/Psp/UmdInfo.cs b/PopsBuilder/Psp/UmdInfo.cs index 012a2da..2962b0b 100644 --- a/PopsBuilder/Psp/UmdInfo.cs +++ b/PopsBuilder/Psp/UmdInfo.cs @@ -25,7 +25,7 @@ namespace GameBuilder.Psp { this.IsoFile = isoFile; this.IsoStream = File.OpenRead(isoFile); - using (CDReader cdReader = new CDReader(this.IsoStream, true, true, 2048)) + using (CDReader cdReader = new CDReader(this.IsoStream, true, true)) { foreach (string file in filesList) { From f14b166dec174cf2e87511910d9cdc28e6384beb Mon Sep 17 00:00:00 2001 From: Li Date: Tue, 18 Apr 2023 02:41:55 +1200 Subject: [PATCH 13/31] Fix key obtain (eboot.pbp) --- ChovySign-CLI/Program.cs | 2 +- PbpResign/Program.cs | 3 +++ PopsBuilder/VersionKey/EbootPbpMethod.cs | 18 +++++++++--------- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/ChovySign-CLI/Program.cs b/ChovySign-CLI/Program.cs index cecdf8e..c7541fd 100644 --- a/ChovySign-CLI/Program.cs +++ b/ChovySign-CLI/Program.cs @@ -180,7 +180,7 @@ namespace ChovySign_CLI if (drmInfo is null) return Error("no versionkey was found, exiting", 6); - Console.WriteLine("Version Key: " + BitConverter.ToString(drmInfo.VersionKey).Replace("-", "")); + Console.WriteLine("Version Key: " + BitConverter.ToString(drmInfo.VersionKey).Replace("-", "") + ", " + drmInfo.KeyIndex); if (pbpMode is null) return Error("no pbp mode was set, exiting", 7); diff --git a/PbpResign/Program.cs b/PbpResign/Program.cs index 47f1ca0..cecc27f 100644 --- a/PbpResign/Program.cs +++ b/PbpResign/Program.cs @@ -362,6 +362,9 @@ namespace PbpResign return false; } + File.WriteAllBytes("PSAR.BUFF", psarBuff.ToArray()); + File.WriteAllBytes("HDRHASH.BUFF", npHdr.HeaderHash.ToArray()); + var vkey = new byte[0x10]; Span mkey = stackalloc byte[Marshal.SizeOf()]; AMCTRL.sceDrmBBMacInit(mkey, 3); diff --git a/PopsBuilder/VersionKey/EbootPbpMethod.cs b/PopsBuilder/VersionKey/EbootPbpMethod.cs index 6b6477d..21e0802 100644 --- a/PopsBuilder/VersionKey/EbootPbpMethod.cs +++ b/PopsBuilder/VersionKey/EbootPbpMethod.cs @@ -26,25 +26,25 @@ namespace GameBuilder.VersionKey { StreamUtil ebootUtil = new StreamUtil(ebootStream); ebootStream.Seek(0x1, SeekOrigin.Begin); - - if (ebootUtil.ReadCStr() != "PBP") + string pbpMagic = ebootUtil.ReadStrLen(0x3); + if (pbpMagic == "PBP") { int dataPspLocation = ebootUtil.ReadInt32At(0x20); int dataPsarLocation = ebootUtil.ReadInt32At(0x24); ebootStream.Seek(dataPsarLocation, SeekOrigin.Begin); - string magic = ebootUtil.ReadStrLen(8); + string psarMagic = ebootUtil.ReadStrLen(8); - switch (magic) + switch (psarMagic) { case "NPUMDIMG": int keyType = ebootUtil.ReadInt32(); string contentId = ebootUtil.ReadStringAt(dataPsarLocation + 0x10); - byte[] npUmdHdr = ebootUtil.ReadBytesAt(dataPsarLocation, 0x100); - byte[] npUmdBody = ebootUtil.ReadBytesAt(dataPsarLocation + 0xC0, 0x10); + byte[] npUmdHdr = ebootUtil.ReadBytesAt(dataPsarLocation, 0xC0); + byte[] npUmdHeaderHash = ebootUtil.ReadBytesAt(dataPsarLocation + 0xC0, 0x10); - byte[] versionkey = getKey(npUmdHdr, npUmdBody); + byte[] versionkey = getKey(npUmdHeaderHash, npUmdHdr); return new NpDrmInfo(versionkey, contentId, keyType); case "PSISOIMG": @@ -66,13 +66,13 @@ namespace GameBuilder.VersionKey return new NpDrmInfo(versionkey, contentId, keyType); } default: - throw new Exception("Cannot obtain versionkey from this EBOOT.PBP (magic:" + magic + ")"); + throw new Exception("Cannot obtain versionkey from this EBOOT.PBP (magic:" + psarMagic + ")"); } } else { - throw new Exception("Invalid PBP"); + throw new Exception("Invalid PBP (got \"" + pbpMagic + "\", expected \"PBP\")"); } } } From 03be164a0e69b5db1e20c7a624538dc106bd9208 Mon Sep 17 00:00:00 2001 From: Li Date: Tue, 18 Apr 2023 07:00:52 +1200 Subject: [PATCH 14/31] some shit got corrupted, will probably have to revert some stuff --- ChovySign-CLI/Program.cs | 85 ++- .../LiLib.csproj | 3 +- .../MathUtil.cs | 2 +- .../Progress/ProgressInfo.cs | 2 +- .../Progress/ProgressTracker.cs | 6 +- .../StreamUtil.cs | 5 +- PbpResign/PbpResign.csproj | 2 +- PbpResign/Program.cs | Bin 52293 -> 53703 bytes PopsBuilder/Cue/CueReader.cs | 3 +- PopsBuilder/GameBuilder.csproj | 1 + PopsBuilder/Pops/DiscCompressor.cs | 15 +- PopsBuilder/Pops/PopsImg.cs | 7 +- PopsBuilder/Pops/PsIsoImg.cs | 35 +- PopsBuilder/Pops/PsTitleImg.cs | 24 +- PopsBuilder/Psp/NpDrmPsar.cs | 4 +- PopsBuilder/Psp/NpUmdImg.cs | 5 +- PopsBuilder/Psp/PbpBuilder.cs | 199 ++++--- PopsBuilder/Psp/Sfo.cs | 5 +- PopsBuilder/VersionKey/EbootPbpMethod.cs | 1 + PopsBuilder/xXEccD3str0yerXx.cs | 55 -- PspCrypto/PspCrypto.csproj | 2 +- PspCrypto/SceNpDrm.cs | 77 ++- PspTest.sln | 27 +- PsvImage/AesHelper.cs | 42 -- PsvImage/CmaKeys.cs | 38 -- PsvImage/PSVIMGBuilder.cs | 119 ---- PsvImage/PsvImgStream.cs | 43 -- PsvImage/PsvmdBuilder.cs | 22 - PsvImage/Utils.cs | 62 --- PsvImgTools/ContentManager/KeyGenerator.cs | Bin 0 -> 2085 bytes PsvImgTools/PsvImgTools/PSVIMGBuilder.cs | 516 ++++++++++++++++++ PsvImgTools/PsvImgTools/PSVIMGFileStream.cs | 314 +++++++++++ PsvImgTools/PsvImgTools/PSVIMGStream.cs | 367 +++++++++++++ .../PsvImgTools/PSVIMGStructs.cs | 113 +++- PsvImgTools/PsvImgTools/PSVMDBuilder.cs | 208 +++++++ PsvImgTools/Vita.csproj | 18 + 36 files changed, 1831 insertions(+), 596 deletions(-) rename PsvImage/PsvImage.csproj => LiGeneralUtilities/LiLib.csproj (63%) rename {PopsBuilder => LiGeneralUtilities}/MathUtil.cs (96%) rename {PopsBuilder => LiGeneralUtilities}/Progress/ProgressInfo.cs (97%) rename {PopsBuilder => LiGeneralUtilities}/Progress/ProgressTracker.cs (74%) rename {PopsBuilder => LiGeneralUtilities}/StreamUtil.cs (98%) delete mode 100644 PopsBuilder/xXEccD3str0yerXx.cs delete mode 100644 PsvImage/AesHelper.cs delete mode 100644 PsvImage/CmaKeys.cs delete mode 100644 PsvImage/PSVIMGBuilder.cs delete mode 100644 PsvImage/PsvImgStream.cs delete mode 100644 PsvImage/PsvmdBuilder.cs delete mode 100644 PsvImage/Utils.cs create mode 100644 PsvImgTools/ContentManager/KeyGenerator.cs create mode 100644 PsvImgTools/PsvImgTools/PSVIMGBuilder.cs create mode 100644 PsvImgTools/PsvImgTools/PSVIMGFileStream.cs create mode 100644 PsvImgTools/PsvImgTools/PSVIMGStream.cs rename PsvImage/PsvImgStructs.cs => PsvImgTools/PsvImgTools/PSVIMGStructs.cs (63%) create mode 100644 PsvImgTools/PsvImgTools/PSVMDBuilder.cs create mode 100644 PsvImgTools/Vita.csproj diff --git a/ChovySign-CLI/Program.cs b/ChovySign-CLI/Program.cs index c7541fd..6588983 100644 --- a/ChovySign-CLI/Program.cs +++ b/ChovySign-CLI/Program.cs @@ -1,8 +1,10 @@ -using GameBuilder.Pops; -using GameBuilder.Progress; +using Li.Progress; +using GameBuilder.Pops; using GameBuilder.Psp; using GameBuilder.VersionKey; using PspCrypto; +using System.Reflection.PortableExecutable; +using System.IO.Pipes; namespace ChovySign_CLI { @@ -36,7 +38,11 @@ namespace ChovySign_CLI } public static int Error(string errorMsg, int ret) { + ConsoleColor prevColor = Console.ForegroundColor; + Console.ForegroundColor = ConsoleColor.DarkRed; Console.Error.WriteLine("ERROR: "+errorMsg); + Console.ForegroundColor = prevColor; + return ret; } public static byte[] StringToByteArray(string hex) @@ -46,7 +52,12 @@ namespace ChovySign_CLI private static void onProgress(ProgressInfo info) { - Console.Write(info.CurrentProcess + " " + info.ProgressInt.ToString() + "% (" + info.Done + "/" + info.Remain + ") \r"); + string msg = info.CurrentProcess + " " + info.ProgressInt.ToString() + "% (" + info.Done + "/" + info.Remain + ")"; + int spaceLen = (Console.WindowWidth - msg.Length) - 2; + string emptySpace = " "; + for (int i = 0; i < spaceLen; i++) + emptySpace += " "; + Console.Write(msg + emptySpace + "\r"); } private static int complete() @@ -98,7 +109,8 @@ namespace ChovySign_CLI } public static int Main(string[] args) { - if(args.Length == 0) + + if (args.Length == 0) { Console.WriteLine("Chovy-Sign v2 (CLI)"); Console.WriteLine("--pops [disc1.cue] [disc2.cue] [disc3.cue] ... (up to 5)"); @@ -209,49 +221,33 @@ namespace ChovySign_CLI psfo.AddKey("PSP_SYSTEM_VER", "6.60", 8); psfo.AddKey("REGION", 32768, 4); psfo.AddKey("TITLE", popsDiscName, 128); - + byte[] sfo = psfo.WriteSfo(); if (discs.Length == 1) { - using (PsIsoImg psIsoImg = new PsIsoImg(drmInfo, discInfs.First())) - { - psIsoImg.RegisterCallback(onProgress); - psIsoImg.CreatePsar(); - - PbpBuilder.CreatePbp(psfo.WriteSfo(), - File.ReadAllBytes(popsIcon0File), - null, - (popsPic0File is not null) ? File.ReadAllBytes(popsPic0File) : null, - Resources.PIC1, - null, - psIsoImg, - "EBOOT.PBP", - 0); + using (PbpBuilder pbpBuilder = new PbpBuilder(sfo, File.ReadAllBytes(popsIcon0File), null, (popsPic0File is not null) ? File.ReadAllBytes(popsPic0File) : null, Resources.PIC1, null, new PsIsoImg(drmInfo, discInfs.First()), 0)) + { + pbpBuilder.RegisterCallback(onProgress); + pbpBuilder.CreatePsarAndPbp(); + pbpBuilder.PbpStream.Seek(0x00, SeekOrigin.Begin); byte[] ebootsig = new byte[0x200]; - SceNpDrm.KsceNpDrmEbootSigGenPs1("EBOOT.PBP", ebootsig, 0x3600000); + SceNpDrm.KsceNpDrmEbootSigGenPs1(pbpBuilder.PbpStream, ebootsig, 0x3674000); File.WriteAllBytes("__sce_ebootpbp", ebootsig.ToArray()); + pbpBuilder.WritePbpToFile("EBOOT.PBP"); } } else { - using (PsTitleImg psTitleImg = new PsTitleImg(drmInfo, discInfs)) + using (PbpBuilder pbpBuilder = new PbpBuilder(sfo, File.ReadAllBytes(popsIcon0File), null, (popsPic0File is not null) ? File.ReadAllBytes(popsPic0File) : null, Resources.PIC1, null, new PsTitleImg(drmInfo, discInfs), 0)) { - psTitleImg.RegisterCallback(onProgress); - psTitleImg.CreatePsar(); - - PbpBuilder.CreatePbp(psfo.WriteSfo(), - File.ReadAllBytes(popsIcon0File), - null, - (popsPic0File is not null) ? File.ReadAllBytes(popsPic0File) : null, - Resources.PIC1, - null, - psTitleImg, - "EBOOT.PBP", - 0); + pbpBuilder.RegisterCallback(onProgress); + pbpBuilder.CreatePsarAndPbp(); + pbpBuilder.PbpStream.Seek(0x00, SeekOrigin.Begin); byte[] ebootsig = new byte[0x200]; - SceNpDrm.KsceNpDrmEbootSigGenPs1("EBOOT.PBP", ebootsig, 0x3600000); + pbpBuilder.WritePbpToFile("EBOOT.PBP"); + SceNpDrm.KsceNpDrmEbootSigGenPs1(pbpBuilder.PbpStream, ebootsig, 0x3674000); File.WriteAllBytes("__sce_ebootpbp", ebootsig.ToArray()); } } @@ -260,25 +256,16 @@ namespace ChovySign_CLI { using (UmdInfo umd = new UmdInfo(discs.First())) { - using (NpUmdImg npUmd = new NpUmdImg(drmInfo, umd, pspCompress)) + using (PbpBuilder pbpBuilder = new PbpBuilder(umd.DataFiles["PARAM.SFO"], umd.DataFiles["ICON0.PNG"], umd.DataFiles["ICON1.PMF"], umd.DataFiles["PIC0.PNG"], umd.DataFiles["PIC1.PNG"], umd.DataFiles["SND0.AT3"], new NpUmdImg(drmInfo, umd, pspCompress), 1)) { - npUmd.RegisterCallback(onProgress); - npUmd.CreatePsar(); - - PbpBuilder.CreatePbp(umd.DataFiles["PARAM.SFO"], - umd.DataFiles["ICON0.PNG"], - umd.DataFiles["ICON1.PMF"], - umd.DataFiles["PIC0.PNG"], - umd.DataFiles["PIC1.PNG"], - umd.DataFiles["SND0.AT3"], - npUmd, - "EBOOT.PBP", - 1); + pbpBuilder.RegisterCallback(onProgress); + pbpBuilder.CreatePsarAndPbp(); + pbpBuilder.PbpStream.Seek(0x00, SeekOrigin.Begin); byte[] ebootsig = new byte[0x200]; - SceNpDrm.KsceNpDrmEbootSigGenPsp("EBOOT.PBP", ebootsig, 0x3600000); + SceNpDrm.KsceNpDrmEbootSigGenPsp(pbpBuilder.PbpStream, ebootsig, 0x3674000); File.WriteAllBytes("__sce_ebootpbp", ebootsig.ToArray()); - + pbpBuilder.WritePbpToFile("EBOOT.PBP"); } } } diff --git a/PsvImage/PsvImage.csproj b/LiGeneralUtilities/LiLib.csproj similarity index 63% rename from PsvImage/PsvImage.csproj rename to LiGeneralUtilities/LiLib.csproj index 5e9d468..132c02c 100644 --- a/PsvImage/PsvImage.csproj +++ b/LiGeneralUtilities/LiLib.csproj @@ -2,7 +2,8 @@ net6.0 - true + enable + enable diff --git a/PopsBuilder/MathUtil.cs b/LiGeneralUtilities/MathUtil.cs similarity index 96% rename from PopsBuilder/MathUtil.cs rename to LiGeneralUtilities/MathUtil.cs index 5354d7e..bd39a5b 100644 --- a/PopsBuilder/MathUtil.cs +++ b/LiGeneralUtilities/MathUtil.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace GameBuilder +namespace Li.Utilities { public static class MathUtil { diff --git a/PopsBuilder/Progress/ProgressInfo.cs b/LiGeneralUtilities/Progress/ProgressInfo.cs similarity index 97% rename from PopsBuilder/Progress/ProgressInfo.cs rename to LiGeneralUtilities/Progress/ProgressInfo.cs index 2f2597d..74e27c7 100644 --- a/PopsBuilder/Progress/ProgressInfo.cs +++ b/LiGeneralUtilities/Progress/ProgressInfo.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace GameBuilder.Progress +namespace Li.Progress { public class ProgressInfo { diff --git a/PopsBuilder/Progress/ProgressTracker.cs b/LiGeneralUtilities/Progress/ProgressTracker.cs similarity index 74% rename from PopsBuilder/Progress/ProgressTracker.cs rename to LiGeneralUtilities/Progress/ProgressTracker.cs index abfa737..60cf354 100644 --- a/PopsBuilder/Progress/ProgressTracker.cs +++ b/LiGeneralUtilities/Progress/ProgressTracker.cs @@ -4,7 +4,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace GameBuilder.Progress +namespace Li.Progress { public class ProgressTracker { @@ -15,9 +15,9 @@ namespace GameBuilder.Progress progressCallbacks.Add(cb); } - public void UpdateProgress(int total, int remain, string what) + protected void updateProgress(int done, int remain, string what) { - ProgressInfo inf = new ProgressInfo(total, remain, what); + ProgressInfo inf = new ProgressInfo(done, remain, what); foreach (Action cb in progressCallbacks) cb(inf); } diff --git a/PopsBuilder/StreamUtil.cs b/LiGeneralUtilities/StreamUtil.cs similarity index 98% rename from PopsBuilder/StreamUtil.cs rename to LiGeneralUtilities/StreamUtil.cs index 5faeec3..5c9bc4f 100644 --- a/PopsBuilder/StreamUtil.cs +++ b/LiGeneralUtilities/StreamUtil.cs @@ -1,11 +1,10 @@ -using PspCrypto; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; -namespace GameBuilder +namespace Li.Utilities { public class StreamUtil { diff --git a/PbpResign/PbpResign.csproj b/PbpResign/PbpResign.csproj index a45a120..6cca5a8 100644 --- a/PbpResign/PbpResign.csproj +++ b/PbpResign/PbpResign.csproj @@ -13,7 +13,7 @@ - + diff --git a/PbpResign/Program.cs b/PbpResign/Program.cs index cecc27fcf2e4938a340c52b2eff4383683a1d885..c3ad072579276a553bc824f8236e421b3dddb300 100644 GIT binary patch literal 53703 zcmeIufdBvi0K=g9Q(xf#g-~I@fB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA xz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HUu1_sf`00961 literal 52293 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 X;OZacXct?kxmrUjyuSbN{fGYxbwq8^ diff --git a/PopsBuilder/Cue/CueReader.cs b/PopsBuilder/Cue/CueReader.cs index 9ba60f0..ecb3133 100644 --- a/PopsBuilder/Cue/CueReader.cs +++ b/PopsBuilder/Cue/CueReader.cs @@ -1,4 +1,5 @@ -using System; +using Li.Utilities; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; diff --git a/PopsBuilder/GameBuilder.csproj b/PopsBuilder/GameBuilder.csproj index 2a3fceb..1aff664 100644 --- a/PopsBuilder/GameBuilder.csproj +++ b/PopsBuilder/GameBuilder.csproj @@ -8,6 +8,7 @@ + diff --git a/PopsBuilder/Pops/DiscCompressor.cs b/PopsBuilder/Pops/DiscCompressor.cs index 0537a4b..ac385fd 100644 --- a/PopsBuilder/Pops/DiscCompressor.cs +++ b/PopsBuilder/Pops/DiscCompressor.cs @@ -1,6 +1,6 @@ using GameBuilder.Atrac3; +using Li.Progress; using GameBuilder.Cue; -using GameBuilder.Progress; using GameBuilder.Psp; using PspCrypto; using System; @@ -9,10 +9,11 @@ using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using Li.Utilities; namespace GameBuilder.Pops { - public class DiscCompressor : ProgressTracker + public class DiscCompressor : ProgressTracker, IDisposable { const int COMPRESS_BLOCK_SZ = 0x9300; const int DEFAULT_ISO_OFFSET = 0x100000; @@ -103,7 +104,7 @@ namespace GameBuilder.Pops while (eccRem.Position < eccRem.Length) { writeCompressedIsoBlock(eccRem); - UpdateProgress(Convert.ToInt32(eccRem.Position), Convert.ToInt32(eccRem.Length), "Compress & Encrypt Disc"); + updateProgress(Convert.ToInt32(eccRem.Position), Convert.ToInt32(eccRem.Length), "Compress & Encrypt Disc"); } } } @@ -163,7 +164,7 @@ namespace GameBuilder.Pops for (int i = 1; i <= totalTracks; i++) { if (cue.GetTrackNumber(i).TrackType != TrackType.TRACK_CDDA) continue; - UpdateProgress(i, totalTracks, "Convert CD Audio tracks to ATRAC3"); + updateProgress(i, totalTracks, "Convert CD Audio tracks to ATRAC3"); using (CueStream audioStream = cue.OpenTrack(i)) { @@ -219,6 +220,12 @@ namespace GameBuilder.Pops isoHeaderUtil.WriteBytes(cue.CreateToc()); } + public void Dispose() + { + IsoHeader.Dispose(); + CompressedIso.Dispose(); + cue.Dispose(); + } private DiscInfo disc; private CueReader cue; diff --git a/PopsBuilder/Pops/PopsImg.cs b/PopsBuilder/Pops/PopsImg.cs index 6d9d1b6..e2c261e 100644 --- a/PopsBuilder/Pops/PopsImg.cs +++ b/PopsBuilder/Pops/PopsImg.cs @@ -1,5 +1,6 @@ using GameBuilder; using GameBuilder.Psp; +using Li.Utilities; using PspCrypto; using System; using System.Collections.Generic; @@ -10,7 +11,7 @@ using System.Threading.Tasks; namespace GameBuilder.Pops { - public class PopsImg : NpDrmPsar + public abstract class PopsImg : NpDrmPsar { public PopsImg(NpDrmInfo versionKey) : base(versionKey) @@ -28,6 +29,7 @@ namespace GameBuilder.Pops private StreamUtil simpleUtil; public byte[] StartDat; public byte[] SimplePgd; + private void createSimpleDat() { simpleUtil.WriteStr("SIMPLE "); @@ -39,9 +41,6 @@ namespace GameBuilder.Pops simpleUtil.WriteBytes(Resources.SIMPLE); } - - - private byte[] generateSimplePgd() { simple.Seek(0x0, SeekOrigin.Begin); diff --git a/PopsBuilder/Pops/PsIsoImg.cs b/PopsBuilder/Pops/PsIsoImg.cs index a80c0a9..dfdbd49 100644 --- a/PopsBuilder/Pops/PsIsoImg.cs +++ b/PopsBuilder/Pops/PsIsoImg.cs @@ -1,14 +1,14 @@ -using GameBuilder.Atrac3; +using Li.Progress; +using GameBuilder.Atrac3; using GameBuilder.Cue; using GameBuilder.Psp; using PspCrypto; using System; using System.Net; -using GameBuilder.Progress; namespace GameBuilder.Pops { - public class PsIsoImg : PopsImg + public class PsIsoImg : PopsImg, IDisposable { internal PsIsoImg(NpDrmInfo versionKey, DiscCompressor discCompressor) : base(versionKey) { @@ -26,11 +26,9 @@ namespace GameBuilder.Pops this.compressor = new DiscCompressor(this, disc, new Atrac3ToolEncoder()); this.compressor.RegisterCallback(onProgress); } - public void CreatePsar(bool isPartOfMultiDisc=false) - { - compressor.GenerateIsoHeaderAndCompress(); - if (!isPartOfMultiDisc) compressor.WriteSimpleDatLocation((compressor.IsoOffset + compressor.CompressedIso.Length) + StartDat.Length); + internal void generatePsIsoHeader() + { psarUtil.WriteStr("PSISOIMG0000"); psarUtil.WriteInt64(0x00); // location of STARTDAT @@ -39,12 +37,23 @@ namespace GameBuilder.Pops byte[] isoHdrPgd = compressor.GenerateIsoPgd(); psarUtil.WriteBytes(isoHdrPgd); psarUtil.PadUntil(0x00, compressor.IsoOffset); + } + public override void CreatePsar() + { + // compress ISO and generate header. + compressor.GenerateIsoHeaderAndCompress(); + + // write STARTDAT location + compressor.WriteSimpleDatLocation((compressor.IsoOffset + compressor.CompressedIso.Length) + StartDat.Length); + + // write general PSISO header + generatePsIsoHeader(); + + // Copy compressed ISO to PSAR stream.. compressor.CompressedIso.Seek(0x00, SeekOrigin.Begin); compressor.CompressedIso.CopyTo(Psar); - if (isPartOfMultiDisc) return; - // write STARTDAT Int64 startDatLocation = Psar.Position; psarUtil.WriteBytes(StartDat); @@ -57,10 +66,14 @@ namespace GameBuilder.Pops psarUtil.WriteInt64(startDatLocation); } - + public override void Dispose() + { + compressor.Dispose(); + base.Dispose(); + } private void onProgress(ProgressInfo inf) { - this.UpdateProgress(inf.Done, inf.Remain, inf.CurrentProcess); + this.updateProgress(inf.Done, inf.Remain, inf.CurrentProcess); } private DiscCompressor compressor; diff --git a/PopsBuilder/Pops/PsTitleImg.cs b/PopsBuilder/Pops/PsTitleImg.cs index 97e7b9f..d1b8d69 100644 --- a/PopsBuilder/Pops/PsTitleImg.cs +++ b/PopsBuilder/Pops/PsTitleImg.cs @@ -1,5 +1,6 @@ -using GameBuilder.Atrac3; -using GameBuilder.Progress; +using Li.Progress; +using Li.Utilities; +using GameBuilder.Atrac3; using GameBuilder.Psp; using PspCrypto; using System; @@ -20,7 +21,7 @@ namespace GameBuilder.Pops private void onProgress(ProgressInfo inf) { - this.UpdateProgress(inf.Done, inf.Remain, inf.CurrentProcess + " (disc " + discNumber + ")"); + this.updateProgress(inf.Done, inf.Remain, inf.CurrentProcess + " (disc " + discNumber + ")"); } public PsTitleImg(NpDrmInfo drmInfo, DiscInfo[] discs) : base(drmInfo) @@ -52,7 +53,7 @@ namespace GameBuilder.Pops } - public void CreatePsar() + public override void CreatePsar() { createIsoMap(); @@ -119,20 +120,31 @@ namespace GameBuilder.Pops using (PsIsoImg psIsoImg = new PsIsoImg(this.DrmInfo, compressors[i])) { + // write location of iso isoMapUtil.WriteUInt32(Convert.ToUInt32(PSISO_ALIGN + isoPart.Position)); - psIsoImg.CreatePsar(true); + // compress current iso and generate headers + compressors[i].GenerateIsoHeaderAndCompress(); + // generate PSISOIMG header + psIsoImg.generatePsIsoHeader(); + + // Copy compressed ISO to PSISOIMG + compressors[i].CompressedIso.Seek(0x00, SeekOrigin.Begin); + compressors[i].CompressedIso.CopyTo(psIsoImg.Psar); + + // read 0x400 bytes from PSAR copy iso header after that,. psIsoImg.Psar.Seek(0x0, SeekOrigin.Begin); compressors[i].IsoHeader.Seek(0x00, SeekOrigin.Begin); - byte[] isoHdr = new byte[compressors[i].IsoHeader.Length + 0x400]; psIsoImg.Psar.Read(isoHdr, 0x00, 0x400); compressors[i].IsoHeader.Read(isoHdr, 0x400, Convert.ToInt32(compressors[i].IsoHeader.Length)); + // Calculate checksum byte[] checksum = calculateChecksumForIsoImgTitle(isoHdr); Array.ConstrainedCopy(checksum, 0, checksums, i * 0x10, 0x10); + // copy psiso to TITLE .. psIsoImg.Psar.Seek(0x00, SeekOrigin.Begin); psIsoImg.Psar.CopyTo(isoPart); diff --git a/PopsBuilder/Psp/NpDrmPsar.cs b/PopsBuilder/Psp/NpDrmPsar.cs index f8a29fb..2695ff7 100644 --- a/PopsBuilder/Psp/NpDrmPsar.cs +++ b/PopsBuilder/Psp/NpDrmPsar.cs @@ -1,4 +1,5 @@ -using GameBuilder.Progress; +using Li.Progress; +using Li.Utilities; using PspCrypto; using System; using System.Collections.Generic; @@ -24,6 +25,7 @@ namespace GameBuilder.Psp public NpDrmInfo DrmInfo; public MemoryStream Psar; internal StreamUtil psarUtil; + public abstract void CreatePsar(); public abstract byte[] GenerateDataPsp(); public static byte[] CreateStartDat(byte[] image) { diff --git a/PopsBuilder/Psp/NpUmdImg.cs b/PopsBuilder/Psp/NpUmdImg.cs index edd6de2..34f9fc4 100644 --- a/PopsBuilder/Psp/NpUmdImg.cs +++ b/PopsBuilder/Psp/NpUmdImg.cs @@ -9,6 +9,7 @@ using System.Net.Security; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using Li.Utilities; namespace GameBuilder.Psp { @@ -71,7 +72,7 @@ namespace GameBuilder.Psp npHdrUtil.PadUntil(0x00, 0x100); } - public void CreatePsar() + public override void CreatePsar() { patchSfo(); createNpUmdTbl(); @@ -167,7 +168,7 @@ namespace GameBuilder.Psp isoOffset += wsize; - UpdateProgress(Convert.ToInt32(umdImage.IsoStream.Position), Convert.ToInt32(umdImage.IsoStream.Length), "Compress & Encrypt UMD Image"); + updateProgress(Convert.ToInt32(umdImage.IsoStream.Position), Convert.ToInt32(umdImage.IsoStream.Length), "Compress & Encrypt UMD Image"); } } diff --git a/PopsBuilder/Psp/PbpBuilder.cs b/PopsBuilder/Psp/PbpBuilder.cs index 44c0bab..5c0ff88 100644 --- a/PopsBuilder/Psp/PbpBuilder.cs +++ b/PopsBuilder/Psp/PbpBuilder.cs @@ -1,4 +1,5 @@ -using GameBuilder.Progress; +using Li.Progress; +using Li.Utilities; using System; using System.Collections.Generic; using System.Linq; @@ -7,75 +8,141 @@ using System.Threading.Tasks; namespace GameBuilder.Psp { - public static class PbpBuilder + public class PbpBuilder : ProgressTracker , IDisposable { - public static void CreatePbp(byte[]? paramSfo, byte[]? icon0Png, byte[]? icon1Png, - byte[]? pic0Png, byte[]? pic1Png, byte[]? snd0At3, - NpDrmPsar dataPsar, string outputFile, short version = 1) + private byte[] paramSfo; + private byte[] icon0Png; + private byte[]? icon1Pmf; + private byte[]? pic0Png; + private byte[]? pic1Png; + private byte[]? snd0At3; + private NpDrmPsar psar; + private short pbpVersion; + private MemoryStream pbpStream; + + public PbpBuilder(byte[] paramSfo, byte[] icon0Png, byte[]? icon1Pmf, + byte[]? pic0Png, byte[]? pic1Png, byte[]? snd0At3, + NpDrmPsar dataPsar, short version = 1) { + this.paramSfo = paramSfo; + this.icon0Png = icon0Png; + this.icon1Pmf = icon1Pmf; + this.pic0Png = pic0Png; + this.pic1Png = pic1Png; + this.snd0At3 = snd0At3; + this.psar = dataPsar; + this.pbpVersion = version; - using (FileStream pbpStream = File.Open(outputFile, FileMode.Create)) - { - byte[] dataPsp = dataPsar.GenerateDataPsp(); - - int padLen = MathUtil.CalculatePaddingAmount(dataPsp.Length, 0x100); - if(version == 1) - Array.Resize(ref dataPsp, dataPsp.Length + padLen); - - StreamUtil pbpUtil = new StreamUtil(pbpStream); - pbpUtil.WriteByte(0x00); - pbpUtil.WriteStr("PBP"); - pbpUtil.WriteInt16(version); - pbpUtil.WriteInt16(1); - - // param location - uint loc = 0x28; - if (paramSfo is null) { pbpUtil.WriteUInt32(loc); } - else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(paramSfo.Length); } - - // icon0 location - if (icon0Png is null) { pbpUtil.WriteUInt32(loc); } - else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(icon0Png.Length); } - - // icon1 location - if (icon1Png is null) { pbpUtil.WriteUInt32(loc); } - else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(icon1Png.Length); } - - // pic0 location - if (pic0Png is null) { pbpUtil.WriteUInt32(loc); } - else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(pic0Png.Length); } - - // pic1 location - if (pic1Png is null) { pbpUtil.WriteUInt32(loc); } - else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(pic1Png.Length); } - - // snd0 location - if (snd0At3 is null) { pbpUtil.WriteUInt32(loc); } - else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(snd0At3.Length); } - - // datapsp location - pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(dataPsp.Length); - - // psar location - pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(dataPsar.Psar.Length); - - // write pbp metadata - if (paramSfo is not null) pbpUtil.WriteBytes(paramSfo); - if (icon0Png is not null) pbpUtil.WriteBytes(icon0Png); - if (icon1Png is not null) pbpUtil.WriteBytes(icon1Png); - if (pic0Png is not null) pbpUtil.WriteBytes(pic0Png); - if (pic1Png is not null) pbpUtil.WriteBytes(pic1Png); - if (snd0At3 is not null) pbpUtil.WriteBytes(snd0At3); - - // write DATA.PSP - pbpUtil.WriteBytes(dataPsp); - - // write DATA.PSAR - dataPsar.Psar.Seek(0x00, SeekOrigin.Begin); - dataPsar.Psar.CopyTo(pbpStream); - } - + pbpStream = new MemoryStream(); + psar.RegisterCallback(onProgress); } + public MemoryStream PbpStream + { + get + { + return pbpStream; + } + } + + private void onProgress(ProgressInfo inf) + { + updateProgress(inf.Done, inf.Remain, inf.CurrentProcess); + } + + public void CreatePsarAndPbp() + { + psar.CreatePsar(); + CreatePbp(); + } + + public void WritePbpToFile(string file) + { + using(FileStream pbpFile = File.OpenWrite(file)) + { + pbpStream.Seek(0x00, SeekOrigin.Begin); + copyPsarWithProgress(pbpStream, pbpFile, "Write to Disk"); + } + } + + public void CreatePbp() + { + byte[] dataPsp = psar.GenerateDataPsp(); + + int padLen = MathUtil.CalculatePaddingAmount(dataPsp.Length, 0x100); + if(pbpVersion == 1) + Array.Resize(ref dataPsp, dataPsp.Length + padLen); + + StreamUtil pbpUtil = new StreamUtil(pbpStream); + pbpUtil.WriteByte(0x00); + pbpUtil.WriteStr("PBP"); + pbpUtil.WriteInt16(pbpVersion); + pbpUtil.WriteInt16(1); + + // param location + uint loc = 0x28; + if (paramSfo is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(paramSfo.Length); } + + // icon0 location + if (icon0Png is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(icon0Png.Length); } + + // icon1 location + if (icon1Pmf is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(icon1Pmf.Length); } + + // pic0 location + if (pic0Png is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(pic0Png.Length); } + + // pic1 location + if (pic1Png is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(pic1Png.Length); } + + // snd0 location + if (snd0At3 is null) { pbpUtil.WriteUInt32(loc); } + else { pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(snd0At3.Length); } + + // datapsp location + pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(dataPsp.Length); + + // psar location + pbpUtil.WriteUInt32(loc); loc += Convert.ToUInt32(psar.Psar.Length); + + // write pbp metadata + if (paramSfo is not null) pbpUtil.WriteBytes(paramSfo); + if (icon0Png is not null) pbpUtil.WriteBytes(icon0Png); + if (icon1Pmf is not null) pbpUtil.WriteBytes(icon1Pmf); + if (pic0Png is not null) pbpUtil.WriteBytes(pic0Png); + if (pic1Png is not null) pbpUtil.WriteBytes(pic1Png); + if (snd0At3 is not null) pbpUtil.WriteBytes(snd0At3); + + // write DATA.PSP + pbpUtil.WriteBytes(dataPsp); + + // write DATA.PSAR + copyPsarWithProgress(psar.Psar, pbpStream); + } + + + private void copyPsarWithProgress(Stream src, Stream dst, string msg = "Build PBP") + { + src.Seek(0, SeekOrigin.Begin); + while (src.Position < src.Length) + { + byte[] readBuffer = new byte[0x30000]; + int readAmt = src.Read(readBuffer, 0x00, readBuffer.Length); + dst.Write(readBuffer, 0x00, readAmt); + + updateProgress(Convert.ToInt32(src.Position), Convert.ToInt32(src.Length), msg); + } + } + + public void Dispose() + { + psar.Dispose(); + pbpStream.Dispose(); + } } } diff --git a/PopsBuilder/Psp/Sfo.cs b/PopsBuilder/Psp/Sfo.cs index b9e3ea0..ee12ef7 100644 --- a/PopsBuilder/Psp/Sfo.cs +++ b/PopsBuilder/Psp/Sfo.cs @@ -1,10 +1,11 @@ -using System; +using Li.Utilities; +using System; using System.Collections.Generic; using System.Diagnostics.Tracing; using System.IO; using System.Runtime.CompilerServices; -// A Sfo Parser Written by SilicaAndPina +// A Sfo Parser Written by Li // Because all the others are overly-complicated for no reason! // MIT Licensed. diff --git a/PopsBuilder/VersionKey/EbootPbpMethod.cs b/PopsBuilder/VersionKey/EbootPbpMethod.cs index 21e0802..f9452a9 100644 --- a/PopsBuilder/VersionKey/EbootPbpMethod.cs +++ b/PopsBuilder/VersionKey/EbootPbpMethod.cs @@ -1,4 +1,5 @@ using GameBuilder.Psp; +using Li.Utilities; using PspCrypto; using System; using System.Collections.Generic; diff --git a/PopsBuilder/xXEccD3str0yerXx.cs b/PopsBuilder/xXEccD3str0yerXx.cs deleted file mode 100644 index d0914a8..0000000 --- a/PopsBuilder/xXEccD3str0yerXx.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace GameBuilder -{ - public class xXEccD3str0yerXx - { - const int SECTOR_SZ = 2352; - const int COMPRESS_BLOCK_SZ = 0x9300; - - public static MemoryStream RemoveEcc(string iso) - { - using(FileStream eccIso = File.OpenRead(iso)) - { - MemoryStream noEccIso = new MemoryStream(); - while (eccIso.Position < eccIso.Length) - { - byte[] sector = new byte[SECTOR_SZ]; - - eccIso.Read(sector, 0, sector.Length); - - // clear current sync - Array.Fill(sector, (byte)0x00, 0x1, 0x0A); - - // remove MSF .. - sector[0x0C] = 0x00; // M - sector[0x0D] = 0x00; // S - sector[0x0E] = 0x00; // F - - // remove ecc - - // (only if this is not form2mode2 sector!) - if (!(sector[0xF] == 0x2 && (sector[0x12] & 0x20) == 0x20)) - Array.Fill(sector, (byte)0x00, 0x818, 0x118); - else if(eccIso.Position > 0x9300) // only clear if its past the system section .. - Array.Fill(sector, (byte)0x00, 0x92C, 0x4); - - noEccIso.Write(sector, 0, sector.Length); - } - - // extend ISO image to compress block sz - int padLen = MathUtil.CalculatePaddingAmount(Convert.ToInt32(noEccIso.Length), COMPRESS_BLOCK_SZ); - byte[] padding = new byte[padLen]; - noEccIso.Write(padding, 0x00, padding.Length); - - noEccIso.Seek(0, SeekOrigin.Begin); - - return noEccIso; - } - } - } -} diff --git a/PspCrypto/PspCrypto.csproj b/PspCrypto/PspCrypto.csproj index 20f3b98..876f2c3 100644 --- a/PspCrypto/PspCrypto.csproj +++ b/PspCrypto/PspCrypto.csproj @@ -6,7 +6,7 @@ - + diff --git a/PspCrypto/SceNpDrm.cs b/PspCrypto/SceNpDrm.cs index 020c90b..a9554fc 100644 --- a/PspCrypto/SceNpDrm.cs +++ b/PspCrypto/SceNpDrm.cs @@ -721,6 +721,20 @@ namespace PspCrypto { return SceNpDrmEbootSigGenMultiDisc(fileName, sceDiscInfo, ebootSig, swVer, _memory.Span, BLK_SIZE); } + public static int KsceNpDrmEbootSigGenPs1(Stream file, Span ebootSig, int swVer) + { + return SceNpDrmEbootSigGen(file, 3, ebootSig, swVer, _memory.Span, BLK_SIZE); + } + + public static int KsceNpDrmEbootSigGenPsp(Stream file, Span ebootSig, int swVer) + { + return SceNpDrmEbootSigGen(file, 2, ebootSig, swVer, _memory.Span, BLK_SIZE); + } + + public static int KsceNpDrmPspEbootSigGen(Stream file, Span ebootSig) + { + return SceNpDrmPspEbootSigGen(file, ebootSig, _memory.Span, BLK_SIZE); + } public static int KsceNpDrmEbootSigGenPs1(string fileName, Span ebootSig, int swVer) { @@ -996,8 +1010,20 @@ namespace PspCrypto return ret; } - private static int SceNpDrmEbootSigGen(string fileName, int type, Span ebootSig, int swVer, Span buffer, - int blockSize) + private static int SceNpDrmEbootSigGen(string fileName, int type, Span ebootSig, int swVer, Span buffer, int blockSize) + { + if(string.IsNullOrWhiteSpace(fileName)) return -0x7f78ffff; + var fi = new FileInfo(fileName); + if (!fi.Exists) + { + return -1; + } + using(FileStream fstream = fi.OpenRead()){ + return SceNpDrmEbootSigGen(fstream, type, ebootSig, swVer, buffer, blockSize); + } + + } + private static int SceNpDrmEbootSigGen(Stream ebootStream, int type, Span ebootSig, int swVer, Span buffer, int blockSize) { Span pbpHdrDigest = stackalloc byte[32]; Span npUmdImgDigest = stackalloc byte[32]; @@ -1008,8 +1034,7 @@ namespace PspCrypto blockSize &= unchecked((int)0xFFFFFFC0); } - if (string.IsNullOrWhiteSpace(fileName) || buffer == null || ebootSig == null || blockSize < 0x400 || - type > 3) + if (ebootStream == null || buffer == null || ebootSig == null || blockSize < 0x400 || type > 3) { return -0x7f78ffff; } @@ -1021,21 +1046,16 @@ namespace PspCrypto sceEbootPbp.SwVer = swVer; sceEbootPbp.Aid = Aid; sceEbootPbp.SecureTick = Utils.AsRef(secureTick); - var fi = new FileInfo(fileName); - if (!fi.Exists) - { - return -1; - } - using var stream = fi.OpenRead(); - var ret = SceEbootPbpDigest(stream, fi.Length, pbpHdrDigest, npUmdImgDigest, buffer, blockSize); + long flen = ebootStream.Length; + int ret = SceEbootPbpDigest(ebootStream, flen, pbpHdrDigest, npUmdImgDigest, buffer, blockSize); if (ret < 0) { return ret; } - stream.Close(); + sceEbootPbp.KeyType = 1; - sceEbootPbp.PbpSize = fi.Length; + sceEbootPbp.PbpSize = flen; sceEbootPbp.Type = type; var psarSig = Encoding.ASCII.GetString(buffer.Slice(0, 8)); if (type == 3) @@ -1178,8 +1198,23 @@ namespace PspCrypto return ret; } - private static int SceNpDrmPspEbootSigGen(string fileName, Span ebootSig, Span buffer, - int blockSize) + private static int SceNpDrmPspEbootSigGen(string fileName, Span ebootSig, Span buffer, int blockSize) + { + if(string.IsNullOrWhiteSpace(fileName)) + return -0x7f78ffff; + + var fi = new FileInfo(fileName); + if (!fi.Exists) + { + return -1; + } + using(FileStream fs = fi.OpenRead()) + { + return SceNpDrmPspEbootSigGen(fs, ebootSig, buffer, blockSize); + } + } + + private static int SceNpDrmPspEbootSigGen(Stream ebootStream, Span ebootSig, Span buffer,int blockSize) { Span pbpHdrDigest = stackalloc byte[32]; Span npUmdImgDigest = stackalloc byte[32]; @@ -1190,23 +1225,17 @@ namespace PspCrypto blockSize &= unchecked((int)0xFFFFFFC0); } - if (string.IsNullOrWhiteSpace(fileName) || buffer == null || ebootSig == null || blockSize < 0x400) + if (ebootStream == null || buffer == null || ebootSig == null || blockSize < 0x400) { return -0x7f78ffff; } - var fi = new FileInfo(fileName); - if (!fi.Exists) - { - return -1; - } - using var stream = fi.OpenRead(); - var ret = SceEbootPbpDigest(stream, fi.Length, pbpHdrDigest, npUmdImgDigest, buffer, blockSize); + long fileSize = ebootStream.Length; + int ret = SceEbootPbpDigest(ebootStream, fileSize, pbpHdrDigest, npUmdImgDigest, buffer, blockSize); if (ret < 0) { return ret; } - stream.Close(); if (Encoding.ASCII.GetString(buffer.Slice(0, 8)) != "NPUMDIMG") { diff --git a/PspTest.sln b/PspTest.sln index 9ddbdc5..35f4e67 100644 --- a/PspTest.sln +++ b/PspTest.sln @@ -6,11 +6,13 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PspCrypto", "PspCrypto\PspCrypto.csproj", "{A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PbpResign", "PbpResign\PbpResign.csproj", "{834B9F97-1B63-48EB-922E-0FC155BB2481}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsvImage", "PsvImage\PsvImage.csproj", "{0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}" + ProjectSection(ProjectDependencies) = postProject + {75BBF537-8ED8-42A3-AA8B-80A730F1B3F2} = {75BBF537-8ED8-42A3-AA8B-80A730F1B3F2} + EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameBuilder", "PopsBuilder\GameBuilder.csproj", "{D1DF66DB-52BE-489C-A292-525BC71F2BB2}" ProjectSection(ProjectDependencies) = postProject + {63475285-AAD2-4DE9-B698-AFC9FAA58AC8} = {63475285-AAD2-4DE9-B698-AFC9FAA58AC8} {BF155D74-2923-432F-8566-26D83B15EEE8} = {BF155D74-2923-432F-8566-26D83B15EEE8} EndProjectSection EndProject @@ -19,7 +21,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChovySign-CLI", "ChovySign- {A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE} = {A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DiscUtils", "DiscUtils\DiscUtils.csproj", "{BF155D74-2923-432F-8566-26D83B15EEE8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils", "DiscUtils\DiscUtils.csproj", "{BF155D74-2923-432F-8566-26D83B15EEE8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vita", "PsvImgTools\Vita.csproj", "{75BBF537-8ED8-42A3-AA8B-80A730F1B3F2}" + ProjectSection(ProjectDependencies) = postProject + {63475285-AAD2-4DE9-B698-AFC9FAA58AC8} = {63475285-AAD2-4DE9-B698-AFC9FAA58AC8} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiLib", "LiGeneralUtilities\LiLib.csproj", "{63475285-AAD2-4DE9-B698-AFC9FAA58AC8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -35,10 +44,6 @@ Global {834B9F97-1B63-48EB-922E-0FC155BB2481}.Debug|Any CPU.Build.0 = Debug|Any CPU {834B9F97-1B63-48EB-922E-0FC155BB2481}.Release|Any CPU.ActiveCfg = Release|Any CPU {834B9F97-1B63-48EB-922E-0FC155BB2481}.Release|Any CPU.Build.0 = Release|Any CPU - {0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}.Release|Any CPU.Build.0 = Release|Any CPU {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Debug|Any CPU.Build.0 = Debug|Any CPU {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -51,6 +56,14 @@ Global {BF155D74-2923-432F-8566-26D83B15EEE8}.Debug|Any CPU.Build.0 = Debug|Any CPU {BF155D74-2923-432F-8566-26D83B15EEE8}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF155D74-2923-432F-8566-26D83B15EEE8}.Release|Any CPU.Build.0 = Release|Any CPU + {75BBF537-8ED8-42A3-AA8B-80A730F1B3F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {75BBF537-8ED8-42A3-AA8B-80A730F1B3F2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {75BBF537-8ED8-42A3-AA8B-80A730F1B3F2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {75BBF537-8ED8-42A3-AA8B-80A730F1B3F2}.Release|Any CPU.Build.0 = Release|Any CPU + {63475285-AAD2-4DE9-B698-AFC9FAA58AC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63475285-AAD2-4DE9-B698-AFC9FAA58AC8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63475285-AAD2-4DE9-B698-AFC9FAA58AC8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63475285-AAD2-4DE9-B698-AFC9FAA58AC8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PsvImage/AesHelper.cs b/PsvImage/AesHelper.cs deleted file mode 100644 index 7ba1b77..0000000 --- a/PsvImage/AesHelper.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Text; - -namespace PsvImage -{ - static class AesHelper - { - public static byte[] CbcEncrypt(byte[] plainText, byte[] iv, byte[] key, int size = -1) - { - if (size < 0) - { - size = plainText.Length; - } - - using var aes = Aes.Create(); - aes.Padding = PaddingMode.None; - aes.Key = key; - aes.IV = iv; - using var encrypt = aes.CreateEncryptor(); - var cipherText = encrypt.TransformFinalBlock(plainText, 0, size); - return cipherText; - } - - public static byte[] AesEcbDecrypt(byte[] cipherText, byte[] key, int size = -1) - { - if (size < 0) - { - size = cipherText.Length; - } - - using var aes = Aes.Create(); - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - aes.Key = key; - using var decrypt = aes.CreateDecryptor(); - var plainText = decrypt.TransformFinalBlock(cipherText, 0, size); - return plainText; - } - } -} diff --git a/PsvImage/CmaKeys.cs b/PsvImage/CmaKeys.cs deleted file mode 100644 index c7bc8b1..0000000 --- a/PsvImage/CmaKeys.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Text; - -namespace PsvImage -{ - public class CmaKeys - { - private static readonly byte[] Passphrase = Encoding.ASCII.GetBytes("Sri Jayewardenepura Kotte"); - private static readonly byte[] Key = { 0xA9, 0xFA, 0x5A, 0x62, 0x79, 0x9F, 0xCC, 0x4C, 0x72, 0x6B, 0x4E, 0x2C, 0xE3, 0x50, 0x6D, 0x38 }; - - public static byte[] GenerateKey(string aid) - { - var longlong = Convert.ToUInt64(aid, 16); - var aidBytes = BitConverter.GetBytes(longlong); - Array.Reverse(aidBytes); - return GenerateKey(aidBytes); - } - - public static byte[] GenerateKey(byte[] aid) - { - var derviedKey = aid.Concat(Passphrase).ToArray(); - - using var sha = SHA256.Create(); - derviedKey = sha.ComputeHash(derviedKey); - using var aes = Aes.Create(); - aes.Mode = CipherMode.ECB; - aes.Padding = PaddingMode.None; - aes.Key = Key; - using var decryptor = aes.CreateDecryptor(); - derviedKey = decryptor.TransformFinalBlock(derviedKey, 0, derviedKey.Length); - return derviedKey; - } - } -} diff --git a/PsvImage/PSVIMGBuilder.cs b/PsvImage/PSVIMGBuilder.cs deleted file mode 100644 index a751bc8..0000000 --- a/PsvImage/PSVIMGBuilder.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Text; - -namespace PsvImage -{ - class PSVIMGBuilder - { - private byte[] iv; - private byte[] key; - private Stream mainStream; - private SHA256 sha256; - private Memory blockData; - private int blockPosition; - private long contentSize = 0; - - private int blocksWritten = 0; - private bool finished = false; - - public long ContentSize => contentSize; - - public int BlocksWritten => blocksWritten; - - public bool HasFinished => finished; - - //Footer - private long totalBytes = 0; - - public PSVIMGBuilder(Stream dst, byte[] Key) - { - - totalBytes = 0; - contentSize = 0; - sha256 = SHA256.Create(); - mainStream = dst; - key = Key; - - RandomNumberGenerator.Fill(iv); - iv = AesHelper.AesEcbDecrypt(iv, key); - - mainStream.Write(iv.AsSpan()); - totalBytes += iv.Length; - - startNewBlock(); - } - - public void AddFile(string FilePath, string ParentPath, string PathRel) - { - var sz = Utils.ToSceIoStat(FilePath).Size; - } - - private void startNewBlock() - { - blockData = new byte[PSVIMGConstants.FULL_PSVIMG_SIZE]; - blockPosition = 0; - } - - private byte[] shaBlock(int length = PSVIMGConstants.PSVIMG_BLOCK_SIZE) - { - return sha256.ComputeHash(blockData.ToArray(), 0, length); - } - - private void finishBlock(bool final = false) - { - int len = blockPosition; - var shaBytes = shaBlock(len); - shaBytes.CopyTo(blockData.Slice(blockPosition)); - len += PSVIMGConstants.SHA256_BLOCK_SIZE; - - //Get next IV - var encryptedBlock = AesHelper.CbcEncrypt(blockData.ToArray(), iv, key, len); - for (int i = 0; i < iv.Length; i++) - { - int encBlockOffset = (encryptedBlock.Length - iv.Length) + i; - iv[i] = encryptedBlock[encBlockOffset]; - } - - mainStream.Write(encryptedBlock.AsSpan()); - totalBytes += encryptedBlock.Length; - } - - private int remainingBlockSize() - { - return (int)(PSVIMGConstants.PSVIMG_BLOCK_SIZE - blockPosition); - } - - private void writeBlock(Span date, bool update = false) - { - var dLen = date.Length; - var writeTotal = 0; - while (dLen > 0) - { - int remaining = remainingBlockSize(); - - if (dLen > remaining) - { - date.Slice(writeTotal, remaining).CopyTo(blockData.Span.Slice(blockPosition)); - blockPosition += remaining; - writeTotal += remaining; - dLen -= remaining; - - finishBlock(); - startNewBlock(); - if (update) - { - blocksWritten += 1; - } - } - else - { - - } - } - - } - } -} diff --git a/PsvImage/PsvImgStream.cs b/PsvImage/PsvImgStream.cs deleted file mode 100644 index 9da6020..0000000 --- a/PsvImage/PsvImgStream.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Security.Cryptography; -using System.Text; - -namespace PsvImage -{ - class PsvImgStream : Stream - { - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override bool CanRead { get; } - public override bool CanSeek { get; } - public override bool CanWrite { get; } - public override long Length { get; } - public override long Position { get; set; } - } -} diff --git a/PsvImage/PsvmdBuilder.cs b/PsvImage/PsvmdBuilder.cs deleted file mode 100644 index 38a3f09..0000000 --- a/PsvImage/PsvmdBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; - -namespace PsvImage -{ - class PsvmdBuilder - { - - public static void CreatePsvmd(Stream OutputStream, Stream EncryptedPsvimg, long ContentSize, string BackupType, - byte[] Key) - { - Span iv = new byte[PSVIMGConstants.AES_BLOCK_SIZE]; - EncryptedPsvimg.Seek(0, SeekOrigin.Begin); - EncryptedPsvimg.Read(iv); - iv = AesHelper.AesEcbDecrypt(iv.ToArray(), Key); - - } - - - } -} diff --git a/PsvImage/Utils.cs b/PsvImage/Utils.cs deleted file mode 100644 index ab7be2c..0000000 --- a/PsvImage/Utils.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace PsvImage -{ - static class Utils - { - public static SceDateTime ToSceDateTime(this DateTime dateTime) - { - var sceDateTime = new SceDateTime(); - sceDateTime.Year = (ushort)dateTime.Year; - sceDateTime.Month = (ushort)dateTime.Month; - sceDateTime.Day = (ushort)dateTime.Day; - sceDateTime.Hour = (ushort)dateTime.Hour; - sceDateTime.Minute = (ushort)dateTime.Minute; - sceDateTime.Second = (ushort)dateTime.Second; - sceDateTime.Microsecond = (uint)dateTime.Millisecond * 1000; - return sceDateTime; - } - - public static SceIoStat ToSceIoStat(string path) - { - var stats = new SceIoStat(); - var attributes = File.GetAttributes(path); - - if (attributes.HasFlag(FileAttributes.Directory)) - { - stats.Mode |= SceIoStat.Modes.Directory; - stats.Size = 0; - } - else - { - stats.Mode |= SceIoStat.Modes.File; - stats.Size = (ulong)(new FileInfo(path).Length); - } - - if (attributes.HasFlag(FileAttributes.ReadOnly)) - { - stats.Mode |= SceIoStat.Modes.GroupRead; - stats.Mode |= SceIoStat.Modes.OthersRead; - stats.Mode |= SceIoStat.Modes.UserRead; - } - else - { - stats.Mode |= SceIoStat.Modes.GroupRead; - stats.Mode |= SceIoStat.Modes.GroupWrite; - stats.Mode |= SceIoStat.Modes.OthersRead; - stats.Mode |= SceIoStat.Modes.OthersWrite; - stats.Mode |= SceIoStat.Modes.UserRead; - stats.Mode |= SceIoStat.Modes.UserWrite; - } - - stats.CreationTime = File.GetCreationTimeUtc(path).ToSceDateTime(); - stats.AccessTime = File.GetLastAccessTimeUtc(path).ToSceDateTime(); - stats.ModificaionTime = File.GetLastWriteTimeUtc(path).ToSceDateTime(); - - return stats; - } - } -} diff --git a/PsvImgTools/ContentManager/KeyGenerator.cs b/PsvImgTools/ContentManager/KeyGenerator.cs new file mode 100644 index 0000000000000000000000000000000000000000..22014885ee48082d0f896cf8aecf6c63b1e0ed74 GIT binary patch literal 2085 ecmZQz7zLvtFd71*Aut*OqaiRF0;3^-B?JHnB>({c literal 0 HcmV?d00001 diff --git a/PsvImgTools/PsvImgTools/PSVIMGBuilder.cs b/PsvImgTools/PsvImgTools/PSVIMGBuilder.cs new file mode 100644 index 0000000..78f4323 --- /dev/null +++ b/PsvImgTools/PsvImgTools/PSVIMGBuilder.cs @@ -0,0 +1,516 @@ +using Li.Progress; +using Org.BouncyCastle.Crypto.Digests; +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using static Vita.PsvImgTools.SceIoStat; + +namespace Vita.PsvImgTools +{ + public class PSVIMGBuilder : ProgressTracker + { + private const Int64 BUFFER_SZ = 0x33554432; + private byte[] IV = new byte[0x10]; + private byte[] KEY; + private Random rnd = new Random(); + private Stream mainStream; + private Sha256Digest shaCtx; + private byte[] blockData; + private MemoryStream blockStream; + private long contentSize = 0; + + //async + private int blocksWritten = 0; + private bool finished = false; + + public Int64 ContentSize + { + get + { + return contentSize; + } + } + public Int32 BlocksWritten + { + get + { + return blocksWritten; + } + } + + public Boolean HasFinished + { + get + { + return finished; + } + } + + //Footer + private long totalBytes = 0; + + private byte[] aes_cbc_encrypt(byte[] plainText, byte[] IV, byte[] KEY, int size=-1) + { + if (size < 0) + { + size = plainText.Length; + } + + MemoryStream ms = new MemoryStream(); + /* DEBUG Disable Encryption + ms.Write(plainText, 0x00, size); + ms.Seek(0x00,SeekOrigin.Begin); + return ms.ToArray();*/ + + Aes alg = Aes.Create(); + alg.Mode = CipherMode.CBC; + alg.Padding = PaddingMode.None; + alg.KeySize = 256; + alg.BlockSize = 128; + alg.Key = KEY; + alg.IV = IV; + CryptoStream cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write); + cs.Write(plainText, 0, size); + cs.Close(); + byte[] cipherText = ms.ToArray(); + return cipherText; + } + + private byte[] aes_ecb_encrypt(byte[] plainText, byte[] KEY, int size = -1) + { + if (size < 0) + { + size = plainText.Length; + } + + MemoryStream ms = new MemoryStream(); + /* DEBUG Disable Encryption + ms.Write(plainText, 0x00, size); + ms.Seek(0x00,SeekOrigin.Begin); + return ms.ToArray();*/ + + Aes alg = Aes.Create(); + alg.Mode = CipherMode.ECB; + alg.Padding = PaddingMode.None; + alg.KeySize = 256; + alg.BlockSize = 128; + alg.Key = KEY; + CryptoStream cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write); + cs.Write(plainText, 0, size); + cs.Close(); + byte[] cipherText = ms.ToArray(); + return cipherText; + } + + // TODO: Switch to Li.Utilities.StreamUtil for this . + private void writeUInt64(Stream dst,UInt64 value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x8); + } + private void writeInt64(Stream dst,Int64 value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x8); + } + private void writeUInt16(Stream dst, UInt16 value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x2); + } + private void writeInt16(Stream dst, Int16 value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x2); + } + + private void writeInt32(Stream dst, Int32 value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x4); + } + private void writeUInt32(Stream dst, UInt32 value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x4); + } + + internal virtual SceDateTime dateTimeToSceDateTime(DateTime dt) + { + SceDateTime sdt = new SceDateTime(); + sdt.Day = Convert.ToUInt16(dt.Day); + sdt.Month = Convert.ToUInt16(dt.Month); + sdt.Year = Convert.ToUInt16(dt.Year); + + + sdt.Hour = Convert.ToUInt16(dt.Hour); + sdt.Minute = Convert.ToUInt16(dt.Minute); + sdt.Second = Convert.ToUInt16(dt.Second); + sdt.Microsecond = Convert.ToUInt32(dt.Millisecond * 1000); + return sdt; + } + + internal virtual SceIoStat sceIoStat(Stream str) + { + SceIoStat stats = new SceIoStat(); + + // streams being a directory doesnt really make sense .. + stats.Mode |= Modes.File; + + // set size.. + stats.Size = Convert.ToUInt64(str.Length); + + // fake the rest-- + stats.Mode |= Modes.GroupRead; + stats.Mode |= Modes.GroupWrite; + + stats.Mode |= Modes.OthersRead; + stats.Mode |= Modes.OthersWrite; + + stats.Mode |= Modes.UserRead; + stats.Mode |= Modes.UserWrite; + + stats.CreationTime = dateTimeToSceDateTime(DateTime.Now); + stats.AccessTime = dateTimeToSceDateTime(DateTime.Now); + stats.ModificaionTime = dateTimeToSceDateTime(DateTime.Now); + + return stats; + } + internal virtual SceIoStat sceIoStat(string path) + { + SceIoStat stats = new SceIoStat(); + FileAttributes attrbutes = File.GetAttributes(path); + + if (attrbutes.HasFlag(FileAttributes.Directory)) + { + stats.Mode |= Modes.Directory; + stats.Size = 0; + } + else + { + stats.Mode |= Modes.File; + stats.Size = Convert.ToUInt64(new FileInfo(path).Length); + } + + if(attrbutes.HasFlag(FileAttributes.ReadOnly)) + { + stats.Mode |= Modes.GroupRead; + + stats.Mode |= Modes.OthersRead; + + stats.Mode |= Modes.UserRead; + } + else + { + stats.Mode |= Modes.GroupRead; + stats.Mode |= Modes.GroupWrite; + + stats.Mode |= Modes.OthersRead; + stats.Mode |= Modes.OthersWrite; + + stats.Mode |= Modes.UserRead; + stats.Mode |= Modes.UserWrite; + } + + stats.CreationTime = dateTimeToSceDateTime(File.GetCreationTimeUtc(path)); + stats.AccessTime = dateTimeToSceDateTime(File.GetLastAccessTimeUtc(path)); + stats.ModificaionTime = dateTimeToSceDateTime(File.GetLastWriteTimeUtc(path)); + + return stats; + } + + internal virtual void writeSceDateTime(Stream dst,SceDateTime time) + { + writeUInt16(dst, time.Year); + writeUInt16(dst, time.Month); + writeUInt16(dst, time.Day); + + writeUInt16(dst, time.Hour); + writeUInt16(dst, time.Minute); + writeUInt16(dst, time.Second); + writeUInt32(dst, time.Microsecond); + } + + internal virtual void writeSceIoStat(Stream dst, SceIoStat stats) + { + writeUInt32(dst, Convert.ToUInt32(stats.Mode)); + writeUInt32(dst, Convert.ToUInt32(stats.Attributes)); + writeUInt64(dst, stats.Size); + writeSceDateTime(dst, stats.CreationTime); + writeSceDateTime(dst, stats.AccessTime); + writeSceDateTime(dst, stats.ModificaionTime); + foreach(UInt32 i in stats.Private) + { + writeUInt32(dst,i); + } + } + + internal virtual void memset(byte[] buf, byte content, long length) + { + for(int i = 0; i < length; i++) + { + buf[i] = content; + } + } + + internal virtual void writeStringWithPadding(Stream dst, string str, int padSize, byte padByte = 0x78) + { + int StrLen = str.Length; + if(StrLen > padSize) + { + StrLen = padSize; + } + + int PaddingLen = (padSize - StrLen)-1; + writeString(dst, str, StrLen); + dst.WriteByte(0x00); + writePadding(dst, padByte, PaddingLen); + } + + internal virtual void writeString(Stream dst, string str, int len=-1) + { + if(len < 0) + { + len = str.Length; + } + + byte[] StrBytes = Encoding.UTF8.GetBytes(str); + dst.Write(StrBytes, 0x00, len); + } + + internal virtual void writePadding(Stream dst, byte paddingByte, long paddingLen) + { + byte[] paddingData = new byte[paddingLen]; + memset(paddingData, paddingByte, paddingLen); + dst.Write(paddingData, 0x00, paddingData.Length); + } + internal virtual byte[] getHeader(SceIoStat Stat, string ParentPath, string PathRel) + { + using (MemoryStream Header = new MemoryStream()) + { + writeInt64(Header, DateTime.UtcNow.Ticks); // SysTime + writeInt64(Header, 0); // Flags + writeSceIoStat(Header, Stat); + writeStringWithPadding(Header, ParentPath, 256); // Parent Path + writeUInt32(Header, 1); //unk_16C + writeStringWithPadding(Header, PathRel, 256); //Relative Path + writePadding(Header, 0x78, 904); //'x' + writeString(Header, PSVIMGConstants.PSVIMG_HEADER_END); //EndOfHeader + Header.Seek(0x00, SeekOrigin.Begin); + return Header.ToArray(); + } + } + internal virtual byte[] getHeader(string FilePath, string ParentPath, string PathRel) + { + using (MemoryStream Header = new MemoryStream()) + { + writeInt64(Header, DateTime.UtcNow.Ticks); // SysTime + writeInt64(Header, 0); // Flags + writeSceIoStat(Header, sceIoStat(FilePath)); + writeStringWithPadding(Header, ParentPath, 256); // Parent Path + writeUInt32(Header, 1); //unk_16C + writeStringWithPadding(Header, PathRel, 256); //Relative Path + writePadding(Header, 0x78, 904); //'x' + writeString(Header, PSVIMGConstants.PSVIMG_HEADER_END); //EndOfHeader + Header.Seek(0x00, SeekOrigin.Begin); + return Header.ToArray(); + } + } + + internal virtual void startNewBlock() + { + blockData = new byte[PSVIMGConstants.FULL_PSVIMG_SIZE]; + blockStream = new MemoryStream(blockData, 0x00, PSVIMGConstants.FULL_PSVIMG_SIZE); + } + + + internal virtual byte[] shaBlock(int length = PSVIMGConstants.PSVIMG_BLOCK_SIZE,bool final=false) + { + byte[] outbytes = new byte[PSVIMGConstants.SHA256_BLOCK_SIZE]; + shaCtx.BlockUpdate(blockData, 0x00, length); + Sha256Digest shaTmp = (Sha256Digest)shaCtx.Copy(); + shaTmp.DoFinal(outbytes,0x00); + return outbytes; + } + + internal virtual void finishBlock(bool final = false) + { + int len = Convert.ToInt32(blockStream.Position); + byte[] shaBytes = shaBlock(len, final); + blockStream.Write(shaBytes, 0x00, PSVIMGConstants.SHA256_BLOCK_SIZE); + len += PSVIMGConstants.SHA256_BLOCK_SIZE; + + //Get next IV + byte[] encryptedBlock = aes_cbc_encrypt(blockData, IV, KEY, len); + for (int i = 0; i < IV.Length; i++) + { + int encBlockOffset = (encryptedBlock.Length - IV.Length)+i; + IV[i] = encryptedBlock[encBlockOffset]; + } + + mainStream.Write(encryptedBlock, 0x00, encryptedBlock.Length); + totalBytes += encryptedBlock.Length; + + blockStream.Dispose(); + } + + internal virtual int remainingBlockSize() + { + return Convert.ToInt32((PSVIMGConstants.PSVIMG_BLOCK_SIZE - blockStream.Position)); + } + + internal virtual void writeBlock(byte[] data, bool update=false) + { + long dLen = data.Length; + long writeTotal = 0; + while (dLen > 0) + { + int remaining = remainingBlockSize(); + + if (dLen > remaining) + { + byte[] dataRemains = new byte[remaining]; + Array.Copy(data, writeTotal, dataRemains, 0, remaining); + blockStream.Write(dataRemains, 0x00, remaining); + + writeTotal += remaining; + dLen -= remaining; + + + finishBlock(); + startNewBlock(); + if (update) + blocksWritten += 1; + } + else + { + byte[] dataRemains = new byte[dLen]; + Array.Copy(data, writeTotal, dataRemains, 0, dLen); + blockStream.Write(dataRemains, 0x00, Convert.ToInt32(dLen)); + + writeTotal += dLen; + dLen -= dLen; + } + } + } + internal virtual byte[] getPadding(long size) + { + using (MemoryStream ms = new MemoryStream()) + { + long paddingSize = PSVIMGPadding.GetPadding(size); + if(paddingSize != 0) + { + writePadding(ms, 0x2B, paddingSize-PSVIMGConstants.PSVIMG_PADDING_END.Length); + writeString(ms, PSVIMGConstants.PSVIMG_PADDING_END); + } + ms.Seek(0x00, SeekOrigin.Begin); + return ms.ToArray(); + } + } + + internal virtual byte[] getTailer() + { + using (MemoryStream ms = new MemoryStream()) + { + writeUInt64(ms, 0x00); + writePadding(ms, 0x7a, 1004); + writeString(ms, PSVIMGConstants.PSVIMG_TAILOR_END); + + ms.Seek(0x00, SeekOrigin.Begin); + return ms.ToArray(); + } + } + + internal virtual void writeStream(Stream dst) + { + while(dst.Position < dst.Length) + { + byte[] work_buf; + Int64 bytes_remain = (dst.Length - dst.Position); + if (bytes_remain > BUFFER_SZ) + work_buf = new byte[BUFFER_SZ]; + else + work_buf = new byte[bytes_remain]; + dst.Read(work_buf, 0x00, work_buf.Length); + writeBlock(work_buf, true); + + updateProgress(Convert.ToInt32(dst.Position), Convert.ToInt32(dst.Length), "PSVIMG Creation"); + } + } + + private byte[] getFooter() + { + using (MemoryStream ms = new MemoryStream()) + { + totalBytes += 0x10; //number of bytes used by this footer. + + writeInt32(ms, 0x00); // int padding (idk wht this is) + writeUInt32(ms, 0x00); + writeInt64(ms, totalBytes); + ms.Seek(0x00, SeekOrigin.Begin); + return aes_cbc_encrypt(ms.ToArray(), IV, KEY); + } + + } + public void AddFile(Stream sData, string ParentPath, string PathRel) + { + SceIoStat stat = sceIoStat(sData); + long sz = Convert.ToInt64(stat.Size); + writeBlock(getHeader(stat, ParentPath, PathRel)); + writeStream(sData); + writeBlock(getPadding(sz)); + writeBlock(getTailer()); + contentSize += sz; + finished = true; + } + public void AddFile(string FilePath, string ParentPath, string PathRel) + { + + long sz = Convert.ToInt64(sceIoStat(FilePath).Size); + writeBlock(getHeader(FilePath, ParentPath, PathRel)); + using (FileStream fs = File.OpenRead(FilePath)) + writeStream(fs); + writeBlock(getPadding(sz)); + writeBlock(getTailer()); + contentSize += sz; + finished = true; + } + + public void AddDir(string DirPath, string ParentPath, string PathRel) + { + writeBlock(getHeader(DirPath, ParentPath, PathRel)); + writeBlock(getPadding(0)); + writeBlock(getTailer()); + } + public long Finish() + { + finishBlock(true); + byte[] footer = getFooter(); + mainStream.Write(footer, 0x00, footer.Length); + + blockStream.Dispose(); + mainStream.Dispose(); + return contentSize; + } + + + public PSVIMGBuilder(Stream dst, byte[] Key) + { + totalBytes = 0; + contentSize = 0; + shaCtx = new Sha256Digest(); + mainStream = dst; + KEY = Key; + + rnd.NextBytes(IV); + IV = aes_ecb_encrypt(IV, Key); + + mainStream.Write(IV, 0x00, IV.Length); + totalBytes += IV.Length; + + startNewBlock(); + } + } +} diff --git a/PsvImgTools/PsvImgTools/PSVIMGFileStream.cs b/PsvImgTools/PsvImgTools/PSVIMGFileStream.cs new file mode 100644 index 0000000..d43f5f1 --- /dev/null +++ b/PsvImgTools/PsvImgTools/PSVIMGFileStream.cs @@ -0,0 +1,314 @@ +using System; +using System.IO; +using static Vita.PsvImgTools.SceIoStat; + +namespace Vita.PsvImgTools +{ + class PSVIMGFileStream : Stream + { + + long length = 0; + long startPos = 0; + long endPos = 0; + long position = 0; + + PSVIMGStream psvStream; + public PSVIMGFileStream(PSVIMGStream psv, string Path) + { + psvStream = psv; + findFile(Path); + } + + public void WriteToFile(string FilePath) + { + using (FileStream fs = File.OpenWrite(FilePath)) + { + fs.SetLength(0); + int written = 0; + long left = length - written; + byte[] work_buf; + Seek(0x00, SeekOrigin.Begin); + while (left > 0) + { + left = length - written; + if (left < 0x10000) + { + work_buf = new byte[left]; + } + else + { + work_buf = new byte[0x10000]; + } + Read(work_buf, 0x00, work_buf.Length); + fs.Write(work_buf, 0x00, work_buf.Length); + written += work_buf.Length; + } + + + } + + } + public override bool CanRead + { + get + { + return true; + } + } + + public override bool CanSeek + { + get + { + return true; + } + } + public override bool CanWrite + { + get + { + return false; + } + } + public override long Length + { + get + { + return length; + } + } + + public override long Position + { + get + { + return position; + } + set + { + Seek(value, SeekOrigin.Begin); + } + } + + public override void Flush() + { + psvStream.Flush(); + } + private int _read(byte[] buffer, int offset, int count) + { + + using (MemoryStream ms = new MemoryStream()) + { + int read = 0; + while (true) + { + int remainBlock = Convert.ToInt32(psvStream.BlockRemaining - PSVIMGConstants.SHA256_BLOCK_SIZE); + int remainRead = count - read; + + if (remainRead < remainBlock) + { + byte[] tmp = new byte[remainRead]; + psvStream.Read(tmp, 0x00, remainRead); + ms.Write(tmp, 0x00, tmp.Length); + read += remainRead; + break; + } + else + { + byte[] tmp = new byte[remainBlock]; + psvStream.Read(tmp, 0x00, remainBlock); + ms.Write(tmp, 0x00, tmp.Length); + psvStream.Seek(PSVIMGConstants.SHA256_BLOCK_SIZE, SeekOrigin.Current); + read += Convert.ToInt32(remainBlock); + } + } + ms.Seek(0x00, SeekOrigin.Begin); + return ms.Read(buffer, offset, count); + } + + } + + private long _seek(long amount, SeekOrigin orig) + { + long ToSeek = 0; + long SeekAdd = 0; + long remainBlock = psvStream.BlockRemaining - PSVIMGConstants.SHA256_BLOCK_SIZE; + while (true) + { + long remainSeek = amount - ToSeek; + + if (remainSeek < remainBlock) + { + ToSeek += remainSeek; + break; + } + else + { + ToSeek += remainBlock; + SeekAdd += PSVIMGConstants.SHA256_BLOCK_SIZE; + } + remainBlock = PSVIMGConstants.PSVIMG_BLOCK_SIZE; + } + ToSeek += SeekAdd; + return psvStream.Seek(ToSeek, orig); + } + public override int Read(byte[] buffer, int offset, int count) + { + if (startPos + count > endPos) + { + count = Convert.ToInt32(endPos); + } + return _read(buffer, offset, count); + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + if (offset <= endPos) + { + psvStream.Seek(startPos, SeekOrigin.Begin); + _seek(offset, SeekOrigin.Current); + position = offset; + return position; + } + else + { + throw new IndexOutOfRangeException("Offset is out of range of file"); + } + + } + else if (origin == SeekOrigin.Current) + { + if (offset <= endPos) + { + _seek(offset, SeekOrigin.Current); + position += offset; + return position; + } + else + { + throw new IndexOutOfRangeException("Offset is out of range of file"); + } + } + else + { + long realOffset = endPos + offset; + if (realOffset <= endPos) + { + _seek(realOffset, SeekOrigin.Begin); + return Position; + } + else + { + throw new IndexOutOfRangeException("Offset is out of range of file"); + } + } + + } + + public override void SetLength(long value) + { + throw new NotImplementedException("PSVFileStream is Read-Only"); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException("PSVFileStream is Read-Only"); + } + + private ushort readUInt16() + { + byte[] intBuf = new byte[0x2]; + _read(intBuf, 0x00, 0x02); + return BitConverter.ToUInt16(intBuf, 0); + } + private uint readUInt32() + { + byte[] intBuf = new byte[0x4]; + _read(intBuf, 0x00, 0x04); + return BitConverter.ToUInt32(intBuf, 0); + } + private ulong readUInt64() + { + byte[] intBuf = new byte[0x8]; + _read(intBuf, 0x00, 0x08); + return BitConverter.ToUInt64(intBuf, 0); + } + private SceDateTime readDatetime() + { + SceDateTime dateTime = new SceDateTime(); + dateTime.Year = readUInt16(); + dateTime.Month = readUInt16(); + dateTime.Day = readUInt16(); + dateTime.Hour = readUInt16(); + dateTime.Minute = readUInt16(); + dateTime.Second = readUInt16(); + dateTime.Microsecond = readUInt32(); + return dateTime; + } + private SceIoStat readStats() + { + SceIoStat stat = new SceIoStat(); + stat.Mode = (Modes)readUInt32(); + stat.Attributes = (AttributesEnum)readUInt32(); + stat.Size = readUInt64(); + stat.CreationTime = readDatetime(); + stat.AccessTime = readDatetime(); + stat.ModificaionTime = readDatetime(); + for (int i = 0; i < stat.Private.Length; i++) + { + stat.Private[i] = readUInt32(); + } + return stat; + } + private PsvImgHeader readHeader() + { + PsvImgHeader header = new PsvImgHeader(); + header.SysTime = readUInt64(); + header.Flags = readUInt64(); + header.Statistics = readStats(); + _read(header.bParentPath, 0x00, 256); + header.unk_16C = readUInt32(); + _read(header.bPath, 0x00, 256); + _read(header.Padding, 0x00, 904); + _read(header.bEnd, 0x00, 12); + return header; + } + + private PsvImgTailer readTailer() + { + PsvImgTailer tailer = new PsvImgTailer(); + tailer.Flags = readUInt64(); + _read(tailer.Padding, 0x00, 1004); + _read(tailer.bEnd, 0x00, 12); + return tailer; + } + private void findFile(string path) + { + _seek(0x00, SeekOrigin.Begin); + while (psvStream.Position < psvStream.Length) + { + PsvImgHeader header = readHeader(); + long size = (long)header.Statistics.Size; + long padding = PSVIMGPadding.GetPadding(size); + + if (header.Path == path) + { + length = size; + startPos = psvStream.Position; + endPos = startPos + length; + return; + } + else + { + _seek(size + padding, SeekOrigin.Current); + PsvImgTailer tailer = readTailer(); + } + + } + throw new FileNotFoundException("Cannot find file specified"); + + } + } +} diff --git a/PsvImgTools/PsvImgTools/PSVIMGStream.cs b/PsvImgTools/PsvImgTools/PSVIMGStream.cs new file mode 100644 index 0000000..742cb11 --- /dev/null +++ b/PsvImgTools/PsvImgTools/PSVIMGStream.cs @@ -0,0 +1,367 @@ +using System; +using System.IO; +using System.Security.Cryptography; + + +namespace Vita.PsvImgTools +{ + class PSVIMGStream : Stream + { + private Stream baseStream; + private MemoryStream blockStream; + private byte[] key; + public Stream BaseStream + { + get + { + return baseStream; + } + } + + public byte[] Key + { + get + { + return key; + } + set + { + key = value; + } + } + + public long BlockNo + { + get + { + return getBlockIndex(); + } + set + { + seekToBlock(value); + update(); + } + } + + public long BlockRemaining + { + get + { + return getRemainingBlock(); + } + } + + public long BlockPosition + { + get + { + return blockStream.Position; + } + set + { + blockStream.Seek(value, SeekOrigin.Begin); + } + } + public override bool CanWrite + { + get + { + return false; + } + } + + public override bool CanRead + { + get + { + return true; + } + } + public override long Length + { + get + { + return baseStream.Length - PSVIMGConstants.AES_BLOCK_SIZE; + } + } + + public override bool CanSeek + { + get + { + return true; + } + } + + public override long Position + { + get + { + return baseStream.Position - PSVIMGConstants.AES_BLOCK_SIZE; + } + set + { + Seek(value, SeekOrigin.Begin); + } + + } + + public PSVIMGStream(Stream file, byte[] KEY) + { + baseStream = file; + key = KEY; + if (!verifyFooter()) + { + throw new Exception("Invalid KEY!"); + } + blockStream = new MemoryStream(); + baseStream.Seek(PSVIMGConstants.AES_BLOCK_SIZE, SeekOrigin.Begin); + update(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int remaining = (int)getRemainingBlock(); + int read = 0; + + if (count < remaining) + { + read += blockStream.Read(buffer, offset, count); + baseStream.Seek(count, SeekOrigin.Current); + } + else + { + using (MemoryStream ms = new MemoryStream()) + { + while (true) + { + update(); + remaining = (int)getRemainingBlock(); + int curPos = count - read; + + if (curPos > remaining) + { + read += remaining; + blockStream.CopyTo(ms, remaining); + baseStream.Seek(remaining, SeekOrigin.Current); + } + else + { + read += curPos; + blockStream.CopyTo(ms, curPos); + baseStream.Seek(curPos, SeekOrigin.Current); + break; + } + + } + ms.Seek(0x00, SeekOrigin.Begin); + ms.Read(buffer, offset, count); + } + } + return read; + + } + + public override void Flush() + { + update(); + baseStream.Flush(); + blockStream.Flush(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + long ret = 0; + if (origin == SeekOrigin.Begin) + { + ret = baseStream.Seek(offset + PSVIMGConstants.AES_BLOCK_SIZE, SeekOrigin.Begin); + } + else if (origin == SeekOrigin.Current) + { + long pos = baseStream.Position; + if (pos + offset >= PSVIMGConstants.AES_BLOCK_SIZE) + { + ret = baseStream.Seek(offset, SeekOrigin.Current); + } + else + { + ret = baseStream.Seek(offset + PSVIMGConstants.AES_BLOCK_SIZE, SeekOrigin.Current); + } + } + else if (origin == SeekOrigin.End) + { + long pos = baseStream.Length; + if (pos + offset >= PSVIMGConstants.AES_BLOCK_SIZE) + { + ret = baseStream.Seek(offset, SeekOrigin.End); + } + else + { + ret = baseStream.Seek(offset + PSVIMGConstants.AES_BLOCK_SIZE, SeekOrigin.End); + } + } + update(); + return ret; + } + + public override void SetLength(long value) + { + throw new NotImplementedException("PSVIMGStream is Read-Only"); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException("PSVIMGStream is Read-Only"); + } + + + public override void Close() + { + blockStream.Close(); + blockStream.Dispose(); + baseStream.Close(); + baseStream.Dispose(); + Dispose(); + } + private void update() + { + long offset = Position % PSVIMGConstants.FULL_PSVIMG_SIZE; + long blockIndex = getBlockIndex(); + byte[] decryptedBlock = getBlock(blockIndex); + blockStream.Seek(0x00, SeekOrigin.Begin); + blockStream.SetLength(decryptedBlock.Length); + blockStream.Write(decryptedBlock, 0x00, decryptedBlock.Length); + seekToBlock(blockIndex); + baseStream.Seek(offset, SeekOrigin.Current); + blockStream.Seek(offset, SeekOrigin.Begin); + } + + private long getBlockIndex() + { + long i = 0; + long curPos = baseStream.Position; + long fullBlock; + long blockOffset; + + while (true) + { + blockOffset = i * PSVIMGConstants.FULL_PSVIMG_SIZE + PSVIMGConstants.AES_BLOCK_SIZE; + long remaining = getRemainingBase(); + if (remaining < PSVIMGConstants.FULL_PSVIMG_SIZE) + { + fullBlock = blockOffset + remaining; + } + else + { + fullBlock = blockOffset + PSVIMGConstants.FULL_PSVIMG_SIZE; + } + if (curPos >= blockOffset && curPos < fullBlock) + { + break; + } + if (blockOffset > baseStream.Length) + { + break; + } + i++; + } + return i; + + + } + private long getRemainingBase() + { + return baseStream.Length - baseStream.Position; + } + private long getRemainingBlock() + { + return blockStream.Length - blockStream.Position; + } + private byte[] getIV(long blockindex) + { + byte[] iv = new byte[0x10]; + seekToBlock(blockindex); + baseStream.Seek(baseStream.Position - PSVIMGConstants.AES_BLOCK_SIZE, SeekOrigin.Begin); + baseStream.Read(iv, 0x00, iv.Length); + return iv; + } + private byte[] aes_cbc_decrypt(byte[] cipherData, byte[] IV) + { + MemoryStream ms = new MemoryStream(); + Aes alg = Aes.Create(); + alg.Mode = CipherMode.CBC; + alg.Padding = PaddingMode.None; + alg.KeySize = 256; + alg.BlockSize = 128; + alg.Key = key; + alg.IV = IV; + CryptoStream cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write); + cs.Write(cipherData, 0, cipherData.Length); + cs.Close(); + byte[] decryptedData = ms.ToArray(); + return decryptedData; + } + + private void seekToBlock(long blockIndex) + { + long blockOffset; + blockOffset = blockIndex * PSVIMGConstants.FULL_PSVIMG_SIZE + PSVIMGConstants.AES_BLOCK_SIZE; + + if (blockOffset > baseStream.Length) + { + blockOffset = baseStream.Length; + } + + baseStream.Seek(blockOffset, SeekOrigin.Begin); + } + + private bool verifyFooter() + { + byte[] Footer = new byte[0x10]; + byte[] IV = new byte[PSVIMGConstants.AES_BLOCK_SIZE]; + + baseStream.Seek(baseStream.Length - (Footer.Length + IV.Length), SeekOrigin.Begin); + baseStream.Read(IV, 0x00, PSVIMGConstants.AES_BLOCK_SIZE); + baseStream.Read(Footer, 0x00, 0x10); + + byte[] FooterDec = aes_cbc_decrypt(Footer, IV); + ulong FooterLen; + using (MemoryStream ms = new MemoryStream(FooterDec)) + { + ms.Seek(0x4, SeekOrigin.Current); + ms.Seek(0x4, SeekOrigin.Current); + byte[] LenInt = new byte[0x8]; + ms.Read(LenInt, 0x00, 0x8); + FooterLen = BitConverter.ToUInt64(LenInt, 0x00); + } + if (Convert.ToUInt64(baseStream.Length) == FooterLen) + { + return true; + } + else + { + return false; + } + } + + private byte[] getBlock(long blockIndex) + { + byte[] iv = getIV(blockIndex); + long remaining = getRemainingBase(); + byte[] encryptedBlock; + if (PSVIMGConstants.FULL_PSVIMG_SIZE < remaining) + { + encryptedBlock = new byte[PSVIMGConstants.FULL_PSVIMG_SIZE]; + } + else + { + encryptedBlock = new byte[remaining]; + } + + + baseStream.Read(encryptedBlock, 0x00, encryptedBlock.Length); + byte[] decryptedBlock = aes_cbc_decrypt(encryptedBlock, iv); + return decryptedBlock; + } + } +} \ No newline at end of file diff --git a/PsvImage/PsvImgStructs.cs b/PsvImgTools/PsvImgTools/PSVIMGStructs.cs similarity index 63% rename from PsvImage/PsvImgStructs.cs rename to PsvImgTools/PsvImgTools/PSVIMGStructs.cs index ec2712a..2af2d74 100644 --- a/PsvImage/PsvImgStructs.cs +++ b/PsvImgTools/PsvImgTools/PSVIMGStructs.cs @@ -1,10 +1,8 @@ using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Text; -namespace PsvImage +namespace Vita.PsvImgTools { + internal class PSVIMGConstants { public const int AES_BLOCK_SIZE = 0x10; @@ -19,8 +17,27 @@ namespace PsvImage public const int FULL_PSVIMG_SIZE = PSVIMG_BLOCK_SIZE + SHA256_BLOCK_SIZE; } - [StructLayout(LayoutKind.Sequential)] - internal struct SceDateTime + internal class StringReader + { + internal static string ReadUntilTerminator(byte[] StringBytes) + { + string str = ""; + foreach (byte sByte in StringBytes) + { + if (sByte != 0x00) + { + str += (char)sByte; + } + else + { + return str; + } + } + return str; + } + } + + internal class SceDateTime { public ushort Year; public ushort Month; @@ -29,13 +46,15 @@ namespace PsvImage public ushort Minute; public ushort Second; public uint Microsecond; + + public SceDateTime() + { + + } } - - [StructLayout(LayoutKind.Sequential)] - internal unsafe struct SceIoStat + internal class SceIoStat { - [Flags] public enum Modes { /** Format bits mask */ @@ -103,7 +122,7 @@ namespace PsvImage public Modes Mode; public AttributesEnum Attributes; /** Size of the file in bytes. */ - public UInt64 Size; + public ulong Size; /** Creation time. */ public SceDateTime CreationTime; /** Access time. */ @@ -111,26 +130,38 @@ namespace PsvImage /** Modification time. */ public SceDateTime ModificaionTime; /** Device-specific data. */ - public fixed uint Private[6]; - } + public uint[] Private = new uint[6]; + public SceIoStat() + { + for (int i = 0; i < Private.Length; i++) + { + Private[i] = 0; + } + } + }; - [StructLayout(LayoutKind.Sequential)] - internal unsafe struct PsvImgTailer + internal class PsvImgTailer { - public ulong Flags; - public fixed byte Padding[1004]; - public fixed byte bEnd[12]; - } + public byte[] Padding = new byte[1004]; + public byte[] bEnd = new byte[12]; + public string End + { + get + { + return StringReader.ReadUntilTerminator(bEnd); + } + } + } internal class PSVIMGPadding { public static long GetPadding(long size) { long padding; - if ((size & (PSVIMGConstants.PSVIMG_ENTRY_ALIGN - 1)) >= 1) + if ((size & PSVIMGConstants.PSVIMG_ENTRY_ALIGN - 1) >= 1) { - padding = (PSVIMGConstants.PSVIMG_ENTRY_ALIGN - (size & (PSVIMGConstants.PSVIMG_ENTRY_ALIGN - 1))); + padding = PSVIMGConstants.PSVIMG_ENTRY_ALIGN - (size & PSVIMGConstants.PSVIMG_ENTRY_ALIGN - 1); } else { @@ -140,15 +171,43 @@ namespace PsvImage } } - internal unsafe struct PsvImgHeader + internal class PsvImgHeader { public ulong SysTime; public ulong Flags; public SceIoStat Statistics; - public fixed byte bParentPath[256]; - public uint unk_16C; - public fixed byte bPath[256]; - public fixed byte Padding[904]; - public fixed byte bEnd[12]; + public byte[] bParentPath = new byte[256]; + public uint unk_16C; // set to 1 + public byte[] bPath = new byte[256]; + public byte[] Padding = new byte[904]; + public byte[] bEnd = new byte[12]; + + public string Path + { + get + { + return StringReader.ReadUntilTerminator(bPath); + } + } + + public string End + { + get + { + return StringReader.ReadUntilTerminator(bEnd); + } + } + + public string ParentPath + { + get + { + return StringReader.ReadUntilTerminator(bParentPath); + } + } + public PsvImgHeader() + { + + } } } diff --git a/PsvImgTools/PsvImgTools/PSVMDBuilder.cs b/PsvImgTools/PsvImgTools/PSVMDBuilder.cs new file mode 100644 index 0000000..01537c0 --- /dev/null +++ b/PsvImgTools/PsvImgTools/PSVMDBuilder.cs @@ -0,0 +1,208 @@ +using Ionic.Zlib; +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; +namespace Vita.PsvImgTools +{ + class PSVMDBuilder + { + private static void memset(byte[] buf, byte content, long length) + { + for (int i = 0; i < length; i++) + { + buf[i] = content; + } + } + + private static void writeUInt64(Stream dst, ulong value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x8); + } + private static void writeInt64(Stream dst, long value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x8); + } + private static void writeUInt16(Stream dst, ushort value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x2); + } + private static void writeInt16(Stream dst, short value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x2); + } + + private static void writeInt32(Stream dst, int value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x4); + } + private static void writeUInt32(Stream dst, uint value) + { + byte[] ValueBytes = BitConverter.GetBytes(value); + dst.Write(ValueBytes, 0x00, 0x4); + } + + + private static void writeString(Stream dst, string str, int len = -1) + { + if (len < 0) + { + len = str.Length; + } + + byte[] StrBytes = Encoding.UTF8.GetBytes(str); + dst.Write(StrBytes, 0x00, len); + } + + private static void writePadding(Stream dst, byte paddingByte, long paddingLen) + { + byte[] paddingData = new byte[paddingLen]; + memset(paddingData, paddingByte, paddingLen); + dst.Write(paddingData, 0x00, paddingData.Length); + } + private static void writeStringWithPadding(Stream dst, string str, int padSize, byte padByte = 0x78) + { + int StrLen = str.Length; + if (StrLen > padSize) + { + StrLen = padSize; + } + + int PaddingLen = padSize - StrLen - 1; + writeString(dst, str, StrLen); + dst.WriteByte(0x00); + writePadding(dst, padByte, PaddingLen); + } + + private static byte[] aes_ecb_decrypt(byte[] cipherText, byte[] KEY, int size = -1) + { + if (size < 0) + { + size = cipherText.Length; + } + + MemoryStream ms = new MemoryStream(); + + Aes alg = Aes.Create(); + alg.Mode = CipherMode.ECB; + alg.Padding = PaddingMode.None; + alg.KeySize = 256; + alg.BlockSize = 128; + alg.Key = KEY; + CryptoStream cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write); + cs.Write(cipherText, 0, size); + cs.Close(); + byte[] plainText = ms.ToArray(); + return plainText; + } + + private static byte[] aes_cbc_decrypt(byte[] cipherData, byte[] IV, byte[] Key) + { + MemoryStream ms = new MemoryStream(); + Aes alg = Aes.Create(); + alg.Mode = CipherMode.CBC; + alg.Padding = PaddingMode.None; + alg.KeySize = 256; + alg.BlockSize = 128; + alg.Key = Key; + alg.IV = IV; + CryptoStream cs = new CryptoStream(ms, alg.CreateDecryptor(), CryptoStreamMode.Write); + cs.Write(cipherData, 0, cipherData.Length); + cs.Close(); + byte[] decryptedData = ms.ToArray(); + return decryptedData; + } + private static byte[] aes_cbc_encrypt(byte[] plainText, byte[] IV, byte[] KEY, int size = -1) + { + if (size < 0) + { + size = plainText.Length; + } + + MemoryStream ms = new MemoryStream(); + + Aes alg = Aes.Create(); + alg.Mode = CipherMode.CBC; + alg.Padding = PaddingMode.None; + alg.KeySize = 256; + alg.BlockSize = 128; + alg.Key = KEY; + alg.IV = IV; + CryptoStream cs = new CryptoStream(ms, alg.CreateEncryptor(), CryptoStreamMode.Write); + cs.Write(plainText, 0, size); + cs.Close(); + byte[] cipherText = ms.ToArray(); + return cipherText; + } + + + public static void CreatePsvmd(Stream OutputStream, Stream EncryptedPsvimg, long ContentSize, string BackupType, byte[] Key) + { + byte[] IV = new byte[PSVIMGConstants.AES_BLOCK_SIZE]; + EncryptedPsvimg.Seek(0x00, SeekOrigin.Begin); + EncryptedPsvimg.Read(IV, 0x00, IV.Length); + IV = aes_ecb_decrypt(IV, Key); + MemoryStream ms = new MemoryStream(); + + writeUInt32(ms, 0xFEE1900D); // magic + writeUInt32(ms, 0x2); // type + writeUInt64(ms, 0x03000000); // fw ver + ms.Write(new byte[0x10], 0x00, 0x10); // PSID + writeStringWithPadding(ms, BackupType, 0x40, 0x00); //backup type + writeInt64(ms, EncryptedPsvimg.Length); // total size + writeUInt64(ms, 0x2); //version + writeInt64(ms, ContentSize); // content size + ms.Write(IV, 0x00, 0x10); // IV + writeUInt64(ms, 0x00); //ux0 info + writeUInt64(ms, 0x00); //ur0 info + writeUInt64(ms, 0x00); //unused 98 + writeUInt64(ms, 0x00); //unused A0 + writeUInt32(ms, 0x1); //add data + + ms.Seek(0x00, SeekOrigin.Begin); + byte[] psvMd = ms.ToArray(); + ms.Close(); + + ms = new MemoryStream(); + byte[] psvMdCompressed = ZlibStream.CompressBuffer(psvMd); + psvMdCompressed[0x1] = 0x9C; + ms.Write(psvMdCompressed, 0x00, psvMdCompressed.Length); + + SHA256 sha = SHA256.Create(); + byte[] shadata = sha.ComputeHash(psvMdCompressed); + ms.Write(shadata, 0x00, shadata.Length); + + int PaddingLen = Convert.ToInt32(PSVIMGConstants.AES_BLOCK_SIZE - (ms.Length & PSVIMGConstants.AES_BLOCK_SIZE - 1)); + writePadding(ms, 0x00, PaddingLen); + + writeInt32(ms, PaddingLen); //Padding Length + writeUInt32(ms, 0x00); + writeInt64(ms, ms.Length + 0x8 + IV.Length); + ms.Seek(0x00, SeekOrigin.Begin); + byte[] toEncrypt = ms.ToArray(); + ms.Close(); + + byte[] EncryptedData = aes_cbc_encrypt(toEncrypt, IV, Key); + OutputStream.Write(IV, 0x00, IV.Length); + OutputStream.Write(EncryptedData, 0x00, EncryptedData.Length); + return; + } + + + public static byte[] DecryptPsvmd(Stream PsvMdFile, byte[] Key) + { + byte[] IV = new byte[PSVIMGConstants.AES_BLOCK_SIZE]; + PsvMdFile.Read(IV, 0x00, IV.Length); + byte[] remaining = new byte[PsvMdFile.Length - IV.Length]; + PsvMdFile.Read(remaining, 0x00, remaining.Length); + byte[] zlibCompressed = aes_cbc_decrypt(remaining, IV, Key); + return zlibCompressed; + // return ZlibStream.UncompressBuffer(zlibCompressed); + } + } +} diff --git a/PsvImgTools/Vita.csproj b/PsvImgTools/Vita.csproj new file mode 100644 index 0000000..fa9bbf2 --- /dev/null +++ b/PsvImgTools/Vita.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + From fb10fc23c7727780f3e736370ffe93ffde2c1b45 Mon Sep 17 00:00:00 2001 From: Li Date: Tue, 18 Apr 2023 07:07:24 +1200 Subject: [PATCH 15/31] Revert corruption .. --- PbpResign/Program.cs | Bin 53703 -> 52148 bytes PsvImgTools/ContentManager/KeyGenerator.cs | Bin 2085 -> 2023 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/PbpResign/Program.cs b/PbpResign/Program.cs index c3ad072579276a553bc824f8236e421b3dddb300..056a5bea557473ae17123c7ab70c6b5443db3abd 100644 GIT binary patch literal 52148 zcmeHwYjYDx*5>#A71fU3Y0WqM{_U=^}dHd?I9sARB*;J@$l zoXo7sTS@Yzr)M@+L13vW^W@2M&y%OJF0;YtJlP!&hnJ(l?D}LpzW6+tRo@KGKef{7 z*?2naj(X|yA19POGi)B#U0pCgyT0o9s@nC$n*J z;Y)MaJ5>vVFQW`pz558r-B@Oj^!O)q=1{qFVna#m4)-VR3nYCHYMWeQxo7pqCD+xwin zNa_zhN}o)+5Y(Hr3v$av1&$ymm!}tlUNRWXlJ?;Ix!m=x|DH~x*IV6bcX)Jm7QNZ* zjYqY4H|u3LTZ3L{N8Bsz*B#FkIm)_cX`;z~IC(eh zH;3mt&o4QoE{c)oMONbn|zk&+F2Vjdk-zEz+Ef zMdQtYNtgQ0R_UGkdikx#@mu?+-S(_Ig`6wAa``f&t(PEjsOAsND(SdUf(%A%jQXYB zHf!-)I~U{L=f-F@y#~*-)JAwZ_|l!F$=Tp4?I)+#_}U+2y=K4jzIIiweGK5WC*9F+ zZ1*zRlZN>ezUALIsq=K^{C4lMg&ML#SidBb*%Vr=hkoGnWZSSeuTIn1<#d#c(yvK@ zRt2C|Ax`z$g93TB=I6Tux%gFl(-uMto-!iB#zml?WAYg<~0jTjiSeR8Q{S z&CZ+t9D9GzJ%_0c2tcODt&m~7bvSu-(LE0pmG?xecDmWXz8&flT*D0BN~zSwe3jZY zZF4#OmVcxC-b$+q1nxz#HtwWYwu?yUCoGPHwEHAZ{B3%@ggH{Winck$?tyj%0{23@ z+Zv^iGJW{?<>*3pf6r7gga4b+L^114H~X}-xUlj4%Zs4!Z$_L}w-1-TWqb5cs?y!P ztc%E)^jmk@DsNY~*`%SH-Y50);qQfZ>svRD0JKZ;qgt`_GSR}MOhW1nyS-fSaeG)@?((U(=5s2RVoNAZ0e0ZagcBQV_KDs69IwXx^ua;lK z?({R3xfHqlqt;;cWIW|s%vyedsq z<97z+65l78to<^z=H&39hL{0z+83N-hucL#2I)UTSBNz~daBlHON)|anr|#Odhy*p z**-qm+dk>MX>9K`jy;Kgf6$$R9~awM<=qJBDrA%TC)bm-x^w+Iu1=@j>s8TFgrH#L zi)2enrG+-v5$``H$V4G?!|UL?88v5>o}#P+E~(mVh>w=9vUd6UzN8&Zi1P29s9^Ix z!xHQ6jS1;0V7qzH+Hb_E88!Z(O6NVCrkSn=(3vOcLd%>c_aNp1@IL&(&7CLbUB-RI z{>Pj7RK12d`^)4c!e47V-i5Gj7-3W5MqAeV1XDHiht=&j=eF1K)4S+qS<;%0&yfHW zK;JBmYU`NIx-%pewT3kNlME{KO)M#s5w)82e_g(-|2K;3_08hD)86jud{4e3e=5>O z%Db4rC`it_*#{yk|J|z9pFH{Xu=U?>9@Xlbk22N%~)_EdiR5K)f0R*TpStC@cKaPZ=nv8Kchw0Zj zpmIX2t!aZ;G`8*)Dh5MCvMG138TX25u)?j!n$T!k*_pB?8#WXzNgQu&&0|zP*S&${ z#*w@KO)+mZDQ5680r@CXpkJl%U~6mseEi=Wb2;{}T2x{$e#or?iAaOkaO^0AwAuYp zv|(e5GQKWSyeI$Hl^#6d^=;9HSFiYeqaoU`&Hp!d`M>J5^+@hM<^Oeoup?IAo(&YclkXptqxd3j#ZjInXP)-jM%a$raV|k*MtU9@}gwIMQqV(R^e! zLS&u_GP~;prI4w~_jRF+y8A>8A^$%S%IvD41^Ep%9YL}#S5&X94J+-b#Fa!TQl9Es zK)o1iR~oIS$h*UkG+yIxO$-UYSfBbzj1TuMNsnayJC+1AgkHNc^L1fELoroazLG17 zku|xpCs#H^dUk}4PlR?`Tl1N>9y0IGLnb0gwH*;h5zR+JpN9PZSg7$xsI(zM{6u*i z(ZIS$)nnJwV{j3fBNR#1eBK-vq5K&jXM&(fWC$7 zQB-SJoChO=Rt`#E$DDY*?OzA(ynfi_j?OsP120+jdiPCt)W1lRPwLN$#9XOvUtmA6LeQ)EMOjO>2BT5h z_W+$FTuzz+TeIoOSWWSAnsQ(Tzq;M;V@Fy+I6D15ND>4n>6WjiXBCYr5`SVW6BxW_@$^a87*7aA=p zSB#x(%gM0qk1u2@F79j)l8IL}sm{6=nMW($wIrNB`BBV6fy~Wjt-Jjdd(vsy6qaOL z9AyhtEstNugMRY2@${s7dXZK{shBc)nam~ybzG{tCja{dn!Mk@7ye!M7=ufX zOq+GxrXIF=Y}(Xxn~ku|6Vs-y+t^#T;e-kW(?u}z)hn-1N+zgP>Q5n^MPrQvX zyqfJ&v`9v5S4JGGcZpfT5~YSIz+2|Ffg(dahiE=`E?Us~gY#4pg1(u<`fJmpTDzC- z^{4&QYNo$%Bdk))rgqgwh^-)c%4dx@851XHif31iEo>{F^|pEZwlRVlr&a7lwYNL1 zqvjzUnTHe>&cAmNVOCpL>(%Xy163ZN$Zdi;zKV?Hp;C0XF6qG9n9XYSqt}tf1M&~l z143X=?aTC94QD8@syIsQWz)@rMw>xsuo=tR)QF+51eM$-ieEmvnHVmlJ(^kE7^>C_ zn6Z7Zdvd&AJ=osuylwnFWP-j=xtI>6ilvE4$_jz6CL7+&!^ZC>$nB=Oxh+pUzZgOH zJw4^N7&ZzpfbJ&hQ&m+0l*?5i1cr7b-y3^(FhRT}xWY&Z`Fesg7EM?g*)ZR7Dx@uz zU-KuVfa*C-)3Zc@H1mRVZPp-NT*f_K3e+o#4G0qpGUVl3%@RZPvoUSzEh#VqhT0icUqr%uND?<%D`Ro^>x| z0A|T5upkdA2KhCED9hlaUO~+p)GkG|kwJ#?V6qKg@##Jw!u(r6ef{lRD_$%rHC&?L#w^j<5 zsWANRF^C+Uu)3Fy1;kMfxoT>9A?Nx^SOQFd?9QDhRNT4%7-P5K$+0P3z)%<6+FS#L zj@y99@p%Zj*s$u>*q9=fH-3o*@a*m^-N=c3;(;0OcrJp#^@Iy#p)mFwUz(tIxmbMZ zu`OX$9D%b2M||g#bIWsER?P3QY+>-Px=IY6kL(#zJGj0ncqhUx8z8@Kq7(AiLqb{7 zyiU+=`93F07@CBXDR{e(Hqlj1wT(*pw33er38A3UX`%4O!`{X?qf*Hf;Bk>M?-{DE`@U!<3|%{KJz49 zM0;biOtcv@T;aCE!#l_}XWkg~#w;|dZnt-vO?px!Y4BTz6S)1bZH^K*-UgF#&n)raGVz!K5|d7# z%14F9XL|BFczHmDY>s9d>%Kzbu`G!LlK}^Pjz7O8*zg}be7I;{B)yPDcrffndVj>nvV^oz}Su*Hx$^<3~9Ojp2-t;Aou+Y=rz@OQ^ zUJY6$nSBB*<&K3X^SrP_Hd*SwNiQaN5D+PWQA@(Sv#}DlYBlscSOsb-7#t}w=A$@g zhPNAH5efSm1_GjMlTnrp8G};goIEoFpx>}~;;K-%E29F~#(Y>>2$lsFNuG$MFA$R@ z*kAsVjMXK9&ugyAh3aO_50lF$A)Zmg!n{{)xL&M6+NuXR32R7)t~*k3BuVa@8n;V- z)MY11@!sKfTXz{|VP1g!ECtuYP?B|j$v+dGPI`My(TzO(+^N~sGPI^ULGBA6T56-*A;A#Tpqk0m}&gK&hB_+@zP5jic>BYaC zidig`(~m9ZPh)=6!T$WrnF5zIy>xrnn@uk|ebg7G)6UcN^(T$(Cr=#>Q;^jEp|&~6 zTgup~p)gg|?rXl6;G|YW;Q*5UTzAsEG$eUK)-yw96w*1q1Trnk>ARS=Zd6Bz;guBo zdI16AgU%n&1?~O{1_=i!2kwHpl@+rboanM`VRGg69GjSc30k@V#cns7Nm1!)BAbQE z%z5m!qB{Za3mK$(FBJ!voG_2;Fd%Bxiqvvs`9RL*`~JDJoEN)VElFO$s(M;iBDWOnX(FL7FIYU-NWLIsh%52T%wpMrtTBK*` z3P|No9$FUX26oh08RaVD4ewb4OP>FDr)b20e{B-vE1ZSy!kJ>!%ZF^9;|9XUjth_h z5VV?IUyqbwZ(E|&hD{!iE|jQpX>uTWIEyTt)&=+S~tH8kIgxvGOtCV#0{|63h<}1es|Uxdk>705JkT5lsk#g#>6h(MmdVq z)bjeflUG|NJCACJgcI_(=M4pbr&FmGZpQXG7%BQaf~1c-AEFEmV5p69iZp+SSF3`f zDQt`3Q8tsROs1+XEr^Dl5O4T}3@1!3hVp(|1z*?&@aD;4{RHmI%IVaj(|w^9%r{CK z@V@KX2afNEhE7>|)2gc3$&<-8vDD=%;aX2E6R7z}`uAf&CN?dkLG9?|gK_W(founR zXQnxRHy@d3!LF?qh(>U6GYMLoa}d5#g^DfpwR+*+%*NUvDUnp`QHWhpLaT_YO4l|0 z)fmWJqmYkmlh8;gU#9nv1MW{Jv$3cNj%3gp;>rP2% zSU`~K#!@NZz@ew&Rq+-%Ti`!!RFu>!vGObJ%uaMT>quIQBSrSf3iL5BtXz&qd}pk< zU%D}-^A}XLXP)Xsh``zV1}9Rc}fX}pGT7nQd_ z+(qXtFrLzTp#$_jGVkSe+#Lw}3x(jtwcKj(uT=BMB$pBlJ`6?j2rD!FBeiAB$jlEXh+(3 zX%+e?BBY!?hI6-gh`uVV$+1RV&6Bg9-YO!8%!h%eJWYXKEK6c?%)n)6!1F6b3E!tjeT{+*as`IK?H#9<>l0 zdsY$oA!LOCj#5)=e4Ju5i-|UyGFr0`sTyiI8pDSCgmdN9(`nlM>@FL4*^09HlON!< zKkhN{M){FNh^dnqjN}@t4Xds8_OUBc>^#@2BDf`mESblZa9&H<%Z2gQtw(YMM-zm$ zL8cxmIa^p3Pid2=qg5zrPm*izh~HC`<3N=HMkKy-oP^UdhIkXDWa$v%E7(hOV>v?R zDJ&ICu?G(yx)Ku&78hmw@&|95*_|ASMP#+&RFR@3O?cZtbjA0nKh0Opg+@ zj6>Tk&=6EU_2)h3V|5}|(WQ2^^Y}5FIn01c!Fyd!q=Jj@fN<{86pOTSb+58e1oU*n zAI_pc(Vlyt7?`O`S@#upl%R=1^OX_YDCHUQ9K}UIr?HBjec4%^3l&o;_FoII&ctGt zM7)MOJm)W$3(t&tR-Ac;)87C>9aW`FUe%>*PPxL13J>aX+!1(YaZmxvy|&pp2I__L z_OZ{^UU|_-;6VwP;~W559TNJoLh8ya-`jIlzJWHxd@rHNc=*9H1(( zcZp|7({j%cx6Er#s(Npz^l4&n?QNP)kO1oP@d%+1=ffA6TqIvp-spW2lY~$K-bGm_ z?%%nh@%0m4bdppQ;YG!kNWR3xPJ7J74vgeiJ`Y8qbXKC2lor$m7I>5z@bGyWvsWz8 zEJ1y!y}d$hDW_-o2*In|J^Z0)I4;N_;^+>AIS`lS9xh+h)!F%VIkc)NN{7aSxZ~sXvD~;ph zqvL1E&i>Kv+hq6nsI?NA75m21HoU`+>$Q1h-u&lmp)Trrd)8RC$k5sLKI=8{zrisG zix9H+`3>Cn>+0G4=pJn##C&F-CZU^VJmP~VHJy3uM)kzLb=n2LcD5Vs`uf)H&ThUv zW)O(*P(iN_$9={SKB$AEy#~CtOcjxH~#eD`sxoJ?E*9-tT~{9K-@G6x%)6%V*jA zz2on}vx@<%pKVnqPU2W}v*Qs_*9f)YA6{d_XpETYs&vU^J8726n^;8dMz1b#{5qcxa;TsU=+aA>R(7>N=6P@P zGh+U7WZvRqq8^bU?I1Z}O4{Ez36CgH(xwmfy%6*ODPI^^19;I(*`l38ER0L6m_&_f z^4AW5EdlL~M`wd`8#An|0@H56(%KHoqK?Z%<6PqvFpt$V*pR+J0hOi3^OA26m!{M8 zmlbQ22n-c0;34*dG&|o<`#d&De>IO=G{cZlE z*?8;_#tr3uztlWnX?HNTj!+ikf^fy6#CsgN#e?Fm>Sqa_@U!pWs4cm(by|orA%v(E zno9*{=bwj`cW<5i`?$Za3&9y`<%2Bov4O z0G`al>66SU=vC5&b@|sKMjnj9vguAj4djzN^zKLo_?jQJxIj7Hh$lAnmpKw#M}Rbi z^K^pAMZRaATXZN5(Q9|VsErPaEu}8uDHbA6c*vN^ClrMKis!|gj$~tGtCRgZ(qAQy zRKAagcLecG2?RB!)jJP<8tNeYEq`5qJn#E)xN&rZ&z(MXUiC6dUZ^L@^*rYQoA4856>qpUBd&Fp#%Yeq z+2XR#;j+|N&yc6_Xz6ae)$G>^H!y$OV?G-wl6cE$2n~CA{D@>y^WG>`5ZcDqwCbLd zN%fd8e@TGNkz9^%wmirYw8jhhl~-`IvJ=L{|C$x;vNI{!>Ri~}QoD(om#ud&1s;Dz z*VO_}x3`E!WpEZ=bDPAI&}x8^>43*|ycFnPw7%zb%*UdW5*M3HVl#b0F$S;$&g)g-YxU$1?vS|^%F?C!DLqRp_|!+|yh0Bet`{uoD& zJVIB;u*Pj#u`bI|$(MkS@fIW!+(V)SkSu>fAoUtDYpk;Lpja!td}%0aFV`Jd&5HGv z+eE#l)0u1NdCvGIKOEzTx83}bOlI_XAbc_V!f6{h3}Nt2Wb#BG;N0cc?leW>$Zq{= zIvyTgUR<1{(;?1hM^>odziEff}g>WmPn*80-D z!0Dz;(+uz$IImF4HiT+F0kNL*)+KKPy|r{PS?ow9YM8dzI&50%TqQXeB`6+)d$2UEWB;BLPESn89Bj#DQ<+6f&0h2M(-a5fRyb+_A z-CmS2G=Oi-pnQ(an`IJLkljQe-m#lQY4ZhD=J{kDS(808||5CsH zJuQvpL*R(9Rs){7N1j(<%IPhhmR)PA;LoizwP|L)-(#_Q5-rvUf4@miwxcun+Fe4VcO9Lv(H%%=__!sMB z*Cdujea!buHTg2HH?eF+Z;Z!nN3-VWY-}42Jayy0Wl@bRLAKS&lLY#6C=VwHaOyCs z7G4s9gJHg;S#g@w&fpyH4H@8_h!(7p^ZvEX)?lCK;y)*&yN6nEkHBsBJ+oml9)gLr z2{r=Rx z=NXT=Lu#^5EI4)EQkQ&lhFuiTlyiuqxfYmLwD5Y{LA$xn#vN>SkZELnlA z*%{2xiq(~Pt%$r`J1nlu-&)1@7X^d78}`NUAXqdf>*!0Bm9w*16}}<1>M(ETflOQu z`CJG*SYj?#)DJ8G3EHdm%+b*mmkzygDjpG8MHq<#(qs6_cvY2iZQ=O7px~aAgJqHQ z5{RN(o=z$REE}mT63yPPw6>485321~M=KxQHx3d)?BfGy?j9Z1s;$G<01zIdv@C>r zwRQ07)(~3F-MgUx0Kh=z5Xi=?FE)|G32KSD_;v zBrXR%Z%G;2AlElrk%-1qM^q7Gm#12mH=x>Edy^yS?#FblxR$fNVC{EHTj}|EqQCab z9(0+u<^4yo7p4Y3zwEypoV}>NnLL3Y#>%vG%2B`nbv(W3 zKTEbx)1NbxCyo34%Z#_$`;B(3zPVM4*+ew}A7qFRu)gE~^|AqqP4dKS#L&_fmlv2? zK`1K%;**iN;$@rg$s}kh|#Z&oueaTvjY3Hw${p!DnX5dyqHe0 zPF<+193SoP?{2?_AewJRTH5>X+O--Ez@Y$|oA@rzD;zq3Q{h`9g9|rHd2w$XLk5qZ z@_~%Ux(SaWLc6vmL&&<;6~F3E^APkIFvbLVRTdTJ_Jme-4h-x@^(-Ll4j&Z8PbNsG z*D>rZStq;WFK{xCeJ3--i|hxT|LXK9dJ^h;ag1uCkw3;gmF zugcR1;8>3F z){-9UqYO{>fwt;kA4xCph2nT>>~Ml*3Ln3eFHS^LvHO`Rt|kU&JVeAaMS-1#nNBz5 zH|! zhEgzj&>f64W(cUd)AOth9|n%RueZjz_TDJkaugSTUU8POm6dlHw)nY1J5KTX5xMpzZr!Zp2+o0pf3fCfCd8;frion<`!u=Lwi#MSxRnQp62|(ab&G_bFaegJ{6(hngVrC+DId9 z@6iOaKy>@PiYqJbFaf2ElUO91AqO#t?tFJz=|wqEXH>78c*URw8yzSfnbgI@q}b5J zA4aNsgK65Eji=XixD_n`u6ajjjq^!uSh(`d{w=nqa2gV)V2r?TKo&jTuqT;-4PreD zIlIlTtuoI<;2cbkg9bp53SY)Kmg`vW8GMJ60G&+HX* z9K7y14_+cACi0{eh+${aK{&OZcv&f*m+HyaJgZbc^5^T1qOU`};q(9)k1?O$!vuv; z`h6ZUPnhg!cZeOVg?lAAedG<+&Y>dB-gs2wF4(W;m=Ocj)nbCM;xl>Owk>sXA@U#u z`zg%THVZL`$d(w~v+*9n*%wLsY%D1X{8jJRj&!ghF{)#TETuIQi0@bSn(f_Ab8qD% z%8IQR?la!69;jlRzl`{7?Z@d8r#zNR8--QaKZZtW|HFrK7=sTVF0X2xNj6D`mz`NU zoOF1#gVRPjueT2xoo|4L(WR({6tX|AtL2^G#0XxD;~7!2-R|h;S#pkT7Y@qsB_tLB zmg^et+8?p#x$&#&fX$G|bAjv_$F+c17#KJL$KZo(<jhjUC2L1hJ)QB3+r7K>C_9V&2$y-8` zF9RgJERSWVV$J6F=E-)awSDrYdh)kYx)?Y*+(T{HRqdIQ?KR5w8fAM8vb{#xUNf@& zp|%nVn#|86fKG7aR>eTa%QZE=k@%0ZB#ZcF_)(U|W);yy-Fl8P?l*TEhwVm8BH}`B zA+R)0bGg>}+WdFLh8EH)Gz268xwo&#;ETHjZqEL?P^V#;KZ zMttz-s`AQ~g>%yT0gNKAsb>%u1Py2Gpj7lM!usCFvZm+@ zcXzs&-Q=}*hi?y${(8tVnR_OTCo}C)-<286>aHTAK?q^)Nnp$l`Wn8RNcGTtk|hHX zTzR4A!e+apO`2495g82v8!>vsyNisehPp3$w8>!cQw|ODM@x*f=bU3&0t&Zr_vkWv_=9O`Kg`l*JMfj1SIZ9>)seIvob# zDfZ@O2q4N}DHpeUG0sw!7}7ELA030uZDToT84-CM_~@{(I6M@>jFx>J;K{F~BHWGQ z@hy6l{d;&RKvH{FdY^6DW)Ji&$ReAd4Uz`QOKgd-#6>dzN8s~1Ccq~mHt;GEW;ElJ zAzXf(1-dE_LVxn7fL5Gyx&TYwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM z7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b* z1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd z0RsjM7%*VKfB^#r3>YwAz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwA xz<>b*1`HT5V8DO@0|pEjFkrxd0RsjM7%*VKfB^#r3>YwAz<>b*1`HUu1_sf`00961 diff --git a/PsvImgTools/ContentManager/KeyGenerator.cs b/PsvImgTools/ContentManager/KeyGenerator.cs index 22014885ee48082d0f896cf8aecf6c63b1e0ed74..95b3c0f681bc8147bd63f1085f66b3cb8ebe17fd 100644 GIT binary patch literal 2023 zcma)7TW{Jh6n^JdSUd%ZMJ;VUcxJvF^0_x$=RLI8oIqJ6Mi=^BEX0r z?C34H?BjSH(0FFX;eh7nX8-b<=3z+VsTt4B>2PFrCNvJr{^W$_XGb&+$26W?unm@l zlGB$LFDty6y`a*PNU&u9#52>v{lo2Rrh&Ghh;twgqfO45g z|423lnXDnM7ztC36NQ>7(0lW1YO9iiE}Qnm{?xV#qJ@be1+olx?j3|)~!o} zU~d|cBd((*^CoW*bwr;pFU{dD{%n2x(ndd>JQQ&SPP_Vt@PIvjvz z@->$%Rmf8Yuj~O*AxTHTQ?Er$s{EhW$A%FUXP+T6{yk(1y*y;;n$I}6<$gcn8Cw=h zzD~AuV%E7Zir9l^oTyx&M%vn#-3EiRAyZ3^_o^27`T$4pEvSuLXpe7Bnim>wxmptU z7D&sB3=idYPMCWP5j}dfNPT*`+ITJb&ebu$aV?gk%JB-@{^bLSM+G$>_E}++z7j zv@>J+C^Fc!#e2+>cZzyExY&D)M`+YMs{WNK$+KwN2=!erRlBt$aFwJi0YZAcKCAl~ zsHSCF@`<*cR3nQT$}RWz2l|C8tM@vWmdCgf8^pvuUiEP6SiLnITve>(Hl591S+6Ly literal 2085 ecmZQz7zLvtFd71*Aut*OqaiRF0;3^-B?JHnB>({c From eeabf1a33a0e418bf394b9fdc163ab1b7dd0b3b6 Mon Sep 17 00:00:00 2001 From: Li Date: Tue, 18 Apr 2023 07:28:50 +1200 Subject: [PATCH 16/31] change names of folders --- .gitignore | 11 +++++++---- ChovySign-CLI/ChovySign-CLI.csproj | 2 +- .../Atrac3/Atrac3ToolEncoder.cs | 0 .../Atrac3/IAtracEncoderBase.cs | 0 {PopsBuilder => GameBuilder}/Cue/CueIndex.cs | 0 {PopsBuilder => GameBuilder}/Cue/CueReader.cs | 0 {PopsBuilder => GameBuilder}/Cue/CueStream.cs | 0 {PopsBuilder => GameBuilder}/Cue/CueTrack.cs | 0 {PopsBuilder => GameBuilder}/Cue/TrackType.cs | 0 {PopsBuilder => GameBuilder}/GameBuilder.csproj | 2 +- {PopsBuilder => GameBuilder}/Pops/DiscCompressor.cs | 0 {PopsBuilder => GameBuilder}/Pops/DiscInfo.cs | 0 .../Pops/EccRemoverStream.cs | 0 {PopsBuilder => GameBuilder}/Pops/PopsImg.cs | 0 {PopsBuilder => GameBuilder}/Pops/PsIsoImg.cs | 0 {PopsBuilder => GameBuilder}/Pops/PsTitleImg.cs | 0 {PopsBuilder => GameBuilder}/Psp/NpDrmInfo.cs | 0 {PopsBuilder => GameBuilder}/Psp/NpDrmPsar.cs | 0 {PopsBuilder => GameBuilder}/Psp/NpUmdImg.cs | 0 {PopsBuilder => GameBuilder}/Psp/PbpBuilder.cs | 0 {PopsBuilder => GameBuilder}/Psp/Rng.cs | 0 {PopsBuilder => GameBuilder}/Psp/Sfo.cs | 0 {PopsBuilder => GameBuilder}/Psp/UmdInfo.cs | 0 {PopsBuilder => GameBuilder}/Resources.Designer.cs | 0 {PopsBuilder => GameBuilder}/Resources.resx | 0 .../Resources/DATAPSPSD.ELF | Bin .../Resources/DATAPSPSDCFG.BIN | Bin {PopsBuilder => GameBuilder}/Resources/SIMPLE.PNG | Bin .../Resources/STARTDATMINIS.PNG | Bin .../Resources/STARTDATPOPS.PNG | Bin .../Resources/STARTDATPSP.PNG | Bin .../VersionKey/ActRifMethod.cs | 0 .../VersionKey/EbootPbpMethod.cs | 0 {LiGeneralUtilities => LiLib}/LiLib.csproj | 0 {LiGeneralUtilities => LiLib}/MathUtil.cs | 0 .../Progress/ProgressInfo.cs | 0 .../Progress/ProgressTracker.cs | 0 {LiGeneralUtilities => LiLib}/StreamUtil.cs | 0 PbpResign/PbpResign.csproj | 4 ++-- PspTest.sln | 4 ++-- .../ContentManager/KeyGenerator.cs | 0 {PsvImgTools => Vita}/PsvImgTools/PSVIMGBuilder.cs | 0 .../PsvImgTools/PSVIMGFileStream.cs | 0 {PsvImgTools => Vita}/PsvImgTools/PSVIMGStream.cs | 0 {PsvImgTools => Vita}/PsvImgTools/PSVIMGStructs.cs | 0 {PsvImgTools => Vita}/PsvImgTools/PSVMDBuilder.cs | 0 {PsvImgTools => Vita}/Vita.csproj | 2 +- 47 files changed, 14 insertions(+), 11 deletions(-) rename {PopsBuilder => GameBuilder}/Atrac3/Atrac3ToolEncoder.cs (100%) rename {PopsBuilder => GameBuilder}/Atrac3/IAtracEncoderBase.cs (100%) rename {PopsBuilder => GameBuilder}/Cue/CueIndex.cs (100%) rename {PopsBuilder => GameBuilder}/Cue/CueReader.cs (100%) rename {PopsBuilder => GameBuilder}/Cue/CueStream.cs (100%) rename {PopsBuilder => GameBuilder}/Cue/CueTrack.cs (100%) rename {PopsBuilder => GameBuilder}/Cue/TrackType.cs (100%) rename {PopsBuilder => GameBuilder}/GameBuilder.csproj (91%) rename {PopsBuilder => GameBuilder}/Pops/DiscCompressor.cs (100%) rename {PopsBuilder => GameBuilder}/Pops/DiscInfo.cs (100%) rename {PopsBuilder => GameBuilder}/Pops/EccRemoverStream.cs (100%) rename {PopsBuilder => GameBuilder}/Pops/PopsImg.cs (100%) rename {PopsBuilder => GameBuilder}/Pops/PsIsoImg.cs (100%) rename {PopsBuilder => GameBuilder}/Pops/PsTitleImg.cs (100%) rename {PopsBuilder => GameBuilder}/Psp/NpDrmInfo.cs (100%) rename {PopsBuilder => GameBuilder}/Psp/NpDrmPsar.cs (100%) rename {PopsBuilder => GameBuilder}/Psp/NpUmdImg.cs (100%) rename {PopsBuilder => GameBuilder}/Psp/PbpBuilder.cs (100%) rename {PopsBuilder => GameBuilder}/Psp/Rng.cs (100%) rename {PopsBuilder => GameBuilder}/Psp/Sfo.cs (100%) rename {PopsBuilder => GameBuilder}/Psp/UmdInfo.cs (100%) rename {PopsBuilder => GameBuilder}/Resources.Designer.cs (100%) rename {PopsBuilder => GameBuilder}/Resources.resx (100%) rename {PopsBuilder => GameBuilder}/Resources/DATAPSPSD.ELF (100%) rename {PopsBuilder => GameBuilder}/Resources/DATAPSPSDCFG.BIN (100%) rename {PopsBuilder => GameBuilder}/Resources/SIMPLE.PNG (100%) rename {PopsBuilder => GameBuilder}/Resources/STARTDATMINIS.PNG (100%) rename {PopsBuilder => GameBuilder}/Resources/STARTDATPOPS.PNG (100%) rename {PopsBuilder => GameBuilder}/Resources/STARTDATPSP.PNG (100%) rename {PopsBuilder => GameBuilder}/VersionKey/ActRifMethod.cs (100%) rename {PopsBuilder => GameBuilder}/VersionKey/EbootPbpMethod.cs (100%) rename {LiGeneralUtilities => LiLib}/LiLib.csproj (100%) rename {LiGeneralUtilities => LiLib}/MathUtil.cs (100%) rename {LiGeneralUtilities => LiLib}/Progress/ProgressInfo.cs (100%) rename {LiGeneralUtilities => LiLib}/Progress/ProgressTracker.cs (100%) rename {LiGeneralUtilities => LiLib}/StreamUtil.cs (100%) rename {PsvImgTools => Vita}/ContentManager/KeyGenerator.cs (100%) rename {PsvImgTools => Vita}/PsvImgTools/PSVIMGBuilder.cs (100%) rename {PsvImgTools => Vita}/PsvImgTools/PSVIMGFileStream.cs (100%) rename {PsvImgTools => Vita}/PsvImgTools/PSVIMGStream.cs (100%) rename {PsvImgTools => Vita}/PsvImgTools/PSVIMGStructs.cs (100%) rename {PsvImgTools => Vita}/PsvImgTools/PSVMDBuilder.cs (100%) rename {PsvImgTools => Vita}/Vita.csproj (85%) diff --git a/.gitignore b/.gitignore index 9d86576..19aaf3e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,14 +14,17 @@ ChovySign-CLI/obj/* DiscUtils/bin/* DiscUtils/obj/* -PopsBuilder/bin/* -PopsBuilder/obj/* +GameBuilder/bin/* +GameBuilder/obj/* PspCrypto/bin/* PspCrypto/obj/* -PsvImage/bin/* -PsvImage/obj/* +LiLib/bin/* +LiLib/obj/* + +Vita/bin/* +Vita/obj/* UnicornTest/* UnicornManaged/* \ No newline at end of file diff --git a/ChovySign-CLI/ChovySign-CLI.csproj b/ChovySign-CLI/ChovySign-CLI.csproj index 3a86ec8..88eee96 100644 --- a/ChovySign-CLI/ChovySign-CLI.csproj +++ b/ChovySign-CLI/ChovySign-CLI.csproj @@ -9,7 +9,7 @@ - + diff --git a/PopsBuilder/Atrac3/Atrac3ToolEncoder.cs b/GameBuilder/Atrac3/Atrac3ToolEncoder.cs similarity index 100% rename from PopsBuilder/Atrac3/Atrac3ToolEncoder.cs rename to GameBuilder/Atrac3/Atrac3ToolEncoder.cs diff --git a/PopsBuilder/Atrac3/IAtracEncoderBase.cs b/GameBuilder/Atrac3/IAtracEncoderBase.cs similarity index 100% rename from PopsBuilder/Atrac3/IAtracEncoderBase.cs rename to GameBuilder/Atrac3/IAtracEncoderBase.cs diff --git a/PopsBuilder/Cue/CueIndex.cs b/GameBuilder/Cue/CueIndex.cs similarity index 100% rename from PopsBuilder/Cue/CueIndex.cs rename to GameBuilder/Cue/CueIndex.cs diff --git a/PopsBuilder/Cue/CueReader.cs b/GameBuilder/Cue/CueReader.cs similarity index 100% rename from PopsBuilder/Cue/CueReader.cs rename to GameBuilder/Cue/CueReader.cs diff --git a/PopsBuilder/Cue/CueStream.cs b/GameBuilder/Cue/CueStream.cs similarity index 100% rename from PopsBuilder/Cue/CueStream.cs rename to GameBuilder/Cue/CueStream.cs diff --git a/PopsBuilder/Cue/CueTrack.cs b/GameBuilder/Cue/CueTrack.cs similarity index 100% rename from PopsBuilder/Cue/CueTrack.cs rename to GameBuilder/Cue/CueTrack.cs diff --git a/PopsBuilder/Cue/TrackType.cs b/GameBuilder/Cue/TrackType.cs similarity index 100% rename from PopsBuilder/Cue/TrackType.cs rename to GameBuilder/Cue/TrackType.cs diff --git a/PopsBuilder/GameBuilder.csproj b/GameBuilder/GameBuilder.csproj similarity index 91% rename from PopsBuilder/GameBuilder.csproj rename to GameBuilder/GameBuilder.csproj index 1aff664..4534cb1 100644 --- a/PopsBuilder/GameBuilder.csproj +++ b/GameBuilder/GameBuilder.csproj @@ -8,7 +8,7 @@ - + diff --git a/PopsBuilder/Pops/DiscCompressor.cs b/GameBuilder/Pops/DiscCompressor.cs similarity index 100% rename from PopsBuilder/Pops/DiscCompressor.cs rename to GameBuilder/Pops/DiscCompressor.cs diff --git a/PopsBuilder/Pops/DiscInfo.cs b/GameBuilder/Pops/DiscInfo.cs similarity index 100% rename from PopsBuilder/Pops/DiscInfo.cs rename to GameBuilder/Pops/DiscInfo.cs diff --git a/PopsBuilder/Pops/EccRemoverStream.cs b/GameBuilder/Pops/EccRemoverStream.cs similarity index 100% rename from PopsBuilder/Pops/EccRemoverStream.cs rename to GameBuilder/Pops/EccRemoverStream.cs diff --git a/PopsBuilder/Pops/PopsImg.cs b/GameBuilder/Pops/PopsImg.cs similarity index 100% rename from PopsBuilder/Pops/PopsImg.cs rename to GameBuilder/Pops/PopsImg.cs diff --git a/PopsBuilder/Pops/PsIsoImg.cs b/GameBuilder/Pops/PsIsoImg.cs similarity index 100% rename from PopsBuilder/Pops/PsIsoImg.cs rename to GameBuilder/Pops/PsIsoImg.cs diff --git a/PopsBuilder/Pops/PsTitleImg.cs b/GameBuilder/Pops/PsTitleImg.cs similarity index 100% rename from PopsBuilder/Pops/PsTitleImg.cs rename to GameBuilder/Pops/PsTitleImg.cs diff --git a/PopsBuilder/Psp/NpDrmInfo.cs b/GameBuilder/Psp/NpDrmInfo.cs similarity index 100% rename from PopsBuilder/Psp/NpDrmInfo.cs rename to GameBuilder/Psp/NpDrmInfo.cs diff --git a/PopsBuilder/Psp/NpDrmPsar.cs b/GameBuilder/Psp/NpDrmPsar.cs similarity index 100% rename from PopsBuilder/Psp/NpDrmPsar.cs rename to GameBuilder/Psp/NpDrmPsar.cs diff --git a/PopsBuilder/Psp/NpUmdImg.cs b/GameBuilder/Psp/NpUmdImg.cs similarity index 100% rename from PopsBuilder/Psp/NpUmdImg.cs rename to GameBuilder/Psp/NpUmdImg.cs diff --git a/PopsBuilder/Psp/PbpBuilder.cs b/GameBuilder/Psp/PbpBuilder.cs similarity index 100% rename from PopsBuilder/Psp/PbpBuilder.cs rename to GameBuilder/Psp/PbpBuilder.cs diff --git a/PopsBuilder/Psp/Rng.cs b/GameBuilder/Psp/Rng.cs similarity index 100% rename from PopsBuilder/Psp/Rng.cs rename to GameBuilder/Psp/Rng.cs diff --git a/PopsBuilder/Psp/Sfo.cs b/GameBuilder/Psp/Sfo.cs similarity index 100% rename from PopsBuilder/Psp/Sfo.cs rename to GameBuilder/Psp/Sfo.cs diff --git a/PopsBuilder/Psp/UmdInfo.cs b/GameBuilder/Psp/UmdInfo.cs similarity index 100% rename from PopsBuilder/Psp/UmdInfo.cs rename to GameBuilder/Psp/UmdInfo.cs diff --git a/PopsBuilder/Resources.Designer.cs b/GameBuilder/Resources.Designer.cs similarity index 100% rename from PopsBuilder/Resources.Designer.cs rename to GameBuilder/Resources.Designer.cs diff --git a/PopsBuilder/Resources.resx b/GameBuilder/Resources.resx similarity index 100% rename from PopsBuilder/Resources.resx rename to GameBuilder/Resources.resx diff --git a/PopsBuilder/Resources/DATAPSPSD.ELF b/GameBuilder/Resources/DATAPSPSD.ELF similarity index 100% rename from PopsBuilder/Resources/DATAPSPSD.ELF rename to GameBuilder/Resources/DATAPSPSD.ELF diff --git a/PopsBuilder/Resources/DATAPSPSDCFG.BIN b/GameBuilder/Resources/DATAPSPSDCFG.BIN similarity index 100% rename from PopsBuilder/Resources/DATAPSPSDCFG.BIN rename to GameBuilder/Resources/DATAPSPSDCFG.BIN diff --git a/PopsBuilder/Resources/SIMPLE.PNG b/GameBuilder/Resources/SIMPLE.PNG similarity index 100% rename from PopsBuilder/Resources/SIMPLE.PNG rename to GameBuilder/Resources/SIMPLE.PNG diff --git a/PopsBuilder/Resources/STARTDATMINIS.PNG b/GameBuilder/Resources/STARTDATMINIS.PNG similarity index 100% rename from PopsBuilder/Resources/STARTDATMINIS.PNG rename to GameBuilder/Resources/STARTDATMINIS.PNG diff --git a/PopsBuilder/Resources/STARTDATPOPS.PNG b/GameBuilder/Resources/STARTDATPOPS.PNG similarity index 100% rename from PopsBuilder/Resources/STARTDATPOPS.PNG rename to GameBuilder/Resources/STARTDATPOPS.PNG diff --git a/PopsBuilder/Resources/STARTDATPSP.PNG b/GameBuilder/Resources/STARTDATPSP.PNG similarity index 100% rename from PopsBuilder/Resources/STARTDATPSP.PNG rename to GameBuilder/Resources/STARTDATPSP.PNG diff --git a/PopsBuilder/VersionKey/ActRifMethod.cs b/GameBuilder/VersionKey/ActRifMethod.cs similarity index 100% rename from PopsBuilder/VersionKey/ActRifMethod.cs rename to GameBuilder/VersionKey/ActRifMethod.cs diff --git a/PopsBuilder/VersionKey/EbootPbpMethod.cs b/GameBuilder/VersionKey/EbootPbpMethod.cs similarity index 100% rename from PopsBuilder/VersionKey/EbootPbpMethod.cs rename to GameBuilder/VersionKey/EbootPbpMethod.cs diff --git a/LiGeneralUtilities/LiLib.csproj b/LiLib/LiLib.csproj similarity index 100% rename from LiGeneralUtilities/LiLib.csproj rename to LiLib/LiLib.csproj diff --git a/LiGeneralUtilities/MathUtil.cs b/LiLib/MathUtil.cs similarity index 100% rename from LiGeneralUtilities/MathUtil.cs rename to LiLib/MathUtil.cs diff --git a/LiGeneralUtilities/Progress/ProgressInfo.cs b/LiLib/Progress/ProgressInfo.cs similarity index 100% rename from LiGeneralUtilities/Progress/ProgressInfo.cs rename to LiLib/Progress/ProgressInfo.cs diff --git a/LiGeneralUtilities/Progress/ProgressTracker.cs b/LiLib/Progress/ProgressTracker.cs similarity index 100% rename from LiGeneralUtilities/Progress/ProgressTracker.cs rename to LiLib/Progress/ProgressTracker.cs diff --git a/LiGeneralUtilities/StreamUtil.cs b/LiLib/StreamUtil.cs similarity index 100% rename from LiGeneralUtilities/StreamUtil.cs rename to LiLib/StreamUtil.cs diff --git a/PbpResign/PbpResign.csproj b/PbpResign/PbpResign.csproj index 6cca5a8..ed087da 100644 --- a/PbpResign/PbpResign.csproj +++ b/PbpResign/PbpResign.csproj @@ -11,9 +11,9 @@ - + - + diff --git a/PspTest.sln b/PspTest.sln index 35f4e67..9459107 100644 --- a/PspTest.sln +++ b/PspTest.sln @@ -23,12 +23,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChovySign-CLI", "ChovySign- EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscUtils", "DiscUtils\DiscUtils.csproj", "{BF155D74-2923-432F-8566-26D83B15EEE8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vita", "PsvImgTools\Vita.csproj", "{75BBF537-8ED8-42A3-AA8B-80A730F1B3F2}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vita", "Vita\Vita.csproj", "{75BBF537-8ED8-42A3-AA8B-80A730F1B3F2}" ProjectSection(ProjectDependencies) = postProject {63475285-AAD2-4DE9-B698-AFC9FAA58AC8} = {63475285-AAD2-4DE9-B698-AFC9FAA58AC8} EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiLib", "LiGeneralUtilities\LiLib.csproj", "{63475285-AAD2-4DE9-B698-AFC9FAA58AC8}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LiLib", "LiLib\LiLib.csproj", "{63475285-AAD2-4DE9-B698-AFC9FAA58AC8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/PsvImgTools/ContentManager/KeyGenerator.cs b/Vita/ContentManager/KeyGenerator.cs similarity index 100% rename from PsvImgTools/ContentManager/KeyGenerator.cs rename to Vita/ContentManager/KeyGenerator.cs diff --git a/PsvImgTools/PsvImgTools/PSVIMGBuilder.cs b/Vita/PsvImgTools/PSVIMGBuilder.cs similarity index 100% rename from PsvImgTools/PsvImgTools/PSVIMGBuilder.cs rename to Vita/PsvImgTools/PSVIMGBuilder.cs diff --git a/PsvImgTools/PsvImgTools/PSVIMGFileStream.cs b/Vita/PsvImgTools/PSVIMGFileStream.cs similarity index 100% rename from PsvImgTools/PsvImgTools/PSVIMGFileStream.cs rename to Vita/PsvImgTools/PSVIMGFileStream.cs diff --git a/PsvImgTools/PsvImgTools/PSVIMGStream.cs b/Vita/PsvImgTools/PSVIMGStream.cs similarity index 100% rename from PsvImgTools/PsvImgTools/PSVIMGStream.cs rename to Vita/PsvImgTools/PSVIMGStream.cs diff --git a/PsvImgTools/PsvImgTools/PSVIMGStructs.cs b/Vita/PsvImgTools/PSVIMGStructs.cs similarity index 100% rename from PsvImgTools/PsvImgTools/PSVIMGStructs.cs rename to Vita/PsvImgTools/PSVIMGStructs.cs diff --git a/PsvImgTools/PsvImgTools/PSVMDBuilder.cs b/Vita/PsvImgTools/PSVMDBuilder.cs similarity index 100% rename from PsvImgTools/PsvImgTools/PSVMDBuilder.cs rename to Vita/PsvImgTools/PSVMDBuilder.cs diff --git a/PsvImgTools/Vita.csproj b/Vita/Vita.csproj similarity index 85% rename from PsvImgTools/Vita.csproj rename to Vita/Vita.csproj index fa9bbf2..8820e9b 100644 --- a/PsvImgTools/Vita.csproj +++ b/Vita/Vita.csproj @@ -12,7 +12,7 @@ - + From 537b1b1229104fec7f29fc6c16ffd5727651882b Mon Sep 17 00:00:00 2001 From: Li Date: Tue, 18 Apr 2023 15:10:03 +1200 Subject: [PATCH 17/31] start moving to other lib --- .gitignore | 3 + ChovySign-CLI/ChovySign-CLI.csproj | 18 +-- ChovySign-CLI/Program.cs | 4 +- GameBuilder/Atrac3/Atrac3ToolEncoder.cs | 2 +- LibChovy/ChovySign.cs | 30 ++++ LibChovy/ChovySignParameters.cs | 16 +++ LibChovy/ChovyTypes.cs | 14 ++ LibChovy/LibChovy.csproj | 29 ++++ LibChovy/PopsParameters.cs | 28 ++++ LibChovy/PspParameters.cs | 16 +++ .../Resources.Designer.cs | 20 ++- {ChovySign-CLI => LibChovy}/Resources.resx | 5 +- LibChovy/Resources/PIC0.PNG | Bin 0 -> 47322 bytes .../Resources/PIC1.PNG | Bin PIC0.pdn | Bin 0 -> 160022 bytes PspTest.sln | 13 +- Vita/ContentManager/SettingsReader.cs | 130 ++++++++++++++++++ 17 files changed, 299 insertions(+), 29 deletions(-) create mode 100644 LibChovy/ChovySign.cs create mode 100644 LibChovy/ChovySignParameters.cs create mode 100644 LibChovy/ChovyTypes.cs create mode 100644 LibChovy/LibChovy.csproj create mode 100644 LibChovy/PopsParameters.cs create mode 100644 LibChovy/PspParameters.cs rename {ChovySign-CLI => LibChovy}/Resources.Designer.cs (80%) rename {ChovySign-CLI => LibChovy}/Resources.resx (93%) create mode 100644 LibChovy/Resources/PIC0.PNG rename {ChovySign-CLI => LibChovy}/Resources/PIC1.PNG (100%) create mode 100644 PIC0.pdn create mode 100644 Vita/ContentManager/SettingsReader.cs diff --git a/.gitignore b/.gitignore index 19aaf3e..327a72a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ GameBuilder/obj/* PspCrypto/bin/* PspCrypto/obj/* +LibChovy/bin/* +LibChovy/obj/* + LiLib/bin/* LiLib/obj/* diff --git a/ChovySign-CLI/ChovySign-CLI.csproj b/ChovySign-CLI/ChovySign-CLI.csproj index 88eee96..002e97a 100644 --- a/ChovySign-CLI/ChovySign-CLI.csproj +++ b/ChovySign-CLI/ChovySign-CLI.csproj @@ -1,4 +1,4 @@ - + Exe @@ -10,21 +10,7 @@ - - - - - True - True - Resources.resx - - - - - - PublicResXFileCodeGenerator - Resources.Designer.cs - + diff --git a/ChovySign-CLI/Program.cs b/ChovySign-CLI/Program.cs index 6588983..c30bb21 100644 --- a/ChovySign-CLI/Program.cs +++ b/ChovySign-CLI/Program.cs @@ -3,8 +3,6 @@ using GameBuilder.Pops; using GameBuilder.Psp; using GameBuilder.VersionKey; using PspCrypto; -using System.Reflection.PortableExecutable; -using System.IO.Pipes; namespace ChovySign_CLI { @@ -225,7 +223,7 @@ namespace ChovySign_CLI if (discs.Length == 1) { - using (PbpBuilder pbpBuilder = new PbpBuilder(sfo, File.ReadAllBytes(popsIcon0File), null, (popsPic0File is not null) ? File.ReadAllBytes(popsPic0File) : null, Resources.PIC1, null, new PsIsoImg(drmInfo, discInfs.First()), 0)) + using (PbpBuilder pbpBuilder = new PbpBuilder(sfo, File.ReadAllBytes(popsIcon0File), null, (popsPic0File is not null) ? File.ReadAllBytes(popsPic0File) : null, LibChovy.Resources.PIC1, null, new PsIsoImg(drmInfo, discInfs.First()), 0)) { pbpBuilder.RegisterCallback(onProgress); pbpBuilder.CreatePsarAndPbp(); diff --git a/GameBuilder/Atrac3/Atrac3ToolEncoder.cs b/GameBuilder/Atrac3/Atrac3ToolEncoder.cs index e29aec7..421def3 100644 --- a/GameBuilder/Atrac3/Atrac3ToolEncoder.cs +++ b/GameBuilder/Atrac3/Atrac3ToolEncoder.cs @@ -1,4 +1,4 @@ -using Org.BouncyCastle.Crypto.IO; +using Li.Utilities; using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/LibChovy/ChovySign.cs b/LibChovy/ChovySign.cs new file mode 100644 index 0000000..9579404 --- /dev/null +++ b/LibChovy/ChovySign.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LibChovy +{ + public class ChovySign + { + + public void createPOPS() + { + + } + public void createPOPSMultiDisc() + { + + } + public void createPSP() + { + + } + + public void Go() + { + + } + } +} diff --git a/LibChovy/ChovySignParameters.cs b/LibChovy/ChovySignParameters.cs new file mode 100644 index 0000000..9047a52 --- /dev/null +++ b/LibChovy/ChovySignParameters.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LibChovy +{ + public abstract class ChovySignParameters + { + public ChovyTypes Type; + public bool MakePSVIMG; + public bool ForceDevkitAid; + + } +} diff --git a/LibChovy/ChovyTypes.cs b/LibChovy/ChovyTypes.cs new file mode 100644 index 0000000..5824ade --- /dev/null +++ b/LibChovy/ChovyTypes.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LibChovy +{ + public enum ChovyTypes + { + POPS, + PSP + } +} diff --git a/LibChovy/LibChovy.csproj b/LibChovy/LibChovy.csproj new file mode 100644 index 0000000..e78df9b --- /dev/null +++ b/LibChovy/LibChovy.csproj @@ -0,0 +1,29 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + True + True + Resources.resx + + + + + + PublicResXFileCodeGenerator + Resources.Designer.cs + + + + diff --git a/LibChovy/PopsParameters.cs b/LibChovy/PopsParameters.cs new file mode 100644 index 0000000..d02bb62 --- /dev/null +++ b/LibChovy/PopsParameters.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LibChovy +{ + public class PopsParameters : ChovySignParameters + { + + //public int SetSDKVersion + public string[] DiscList; + public string DiscName; + public Bitmap? Icon0Override; + public Bitmap? Pic0Override; + public Bitmap? Pic1Override; + + public bool MultiDisc + { + get + { + return DiscList.Length > 1; + } + } + } +} diff --git a/LibChovy/PspParameters.cs b/LibChovy/PspParameters.cs new file mode 100644 index 0000000..804c66e --- /dev/null +++ b/LibChovy/PspParameters.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LibChovy +{ + public class PspParameters : ChovySignParameters + { + public PspParameters() + { + + } + } +} diff --git a/ChovySign-CLI/Resources.Designer.cs b/LibChovy/Resources.Designer.cs similarity index 80% rename from ChovySign-CLI/Resources.Designer.cs rename to LibChovy/Resources.Designer.cs index 9624b0c..76cb0c1 100644 --- a/ChovySign-CLI/Resources.Designer.cs +++ b/LibChovy/Resources.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace ChovySign_CLI { +namespace LibChovy { using System; @@ -39,7 +39,7 @@ namespace ChovySign_CLI { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ChovySign_CLI.Resources", typeof(Resources).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LibChovy.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; @@ -61,12 +61,22 @@ namespace ChovySign_CLI { } /// - /// Looks up a localized resource of type System.Byte[]. + /// Looks up a localized resource of type System.Drawing.Bitmap. /// - public static byte[] PIC1 { + public static System.Drawing.Bitmap PIC0 { + get { + object obj = ResourceManager.GetObject("PIC0", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + public static System.Drawing.Bitmap PIC1 { get { object obj = ResourceManager.GetObject("PIC1", resourceCulture); - return ((byte[])(obj)); + return ((System.Drawing.Bitmap)(obj)); } } } diff --git a/ChovySign-CLI/Resources.resx b/LibChovy/Resources.resx similarity index 93% rename from ChovySign-CLI/Resources.resx rename to LibChovy/Resources.resx index 58f7389..f1b5140 100644 --- a/ChovySign-CLI/Resources.resx +++ b/LibChovy/Resources.resx @@ -118,7 +118,10 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Resources\PIC0.PNG;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + - Resources\PIC1.PNG;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + Resources\PIC1.PNG;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a \ No newline at end of file diff --git a/LibChovy/Resources/PIC0.PNG b/LibChovy/Resources/PIC0.PNG new file mode 100644 index 0000000000000000000000000000000000000000..fd87f844e3c7995d9c06e376e92ee2c9cf61bf41 GIT binary patch literal 47322 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

    Lp@`DA5Xum!_06ACD4=cTr}C^B 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¥{5p6=_!pqfQPE^xzM(OYexML9$~5A^ zOwzwk4>bk~v-rD<{4V&mu@=H0JB(+>?Wlk+jMXm$a*CMwJTgHxMtl5|r{wu(Pd6i9 z-YfDsGU3RU{5JI-WBoKa5&y5GnXAe5tuN zGq+@TjCWja$$Y%`J~~jCSGIK?$W5I|Fo$s5Pzeyj5o1Xu=ECQl@O~q}!}6Cq@=?us zw2kYgz8eh!g)>=Jg=B3!F6Z7e9w}+k3r`J?m+amg9;@`}E^Q6@>`+*;R3a6n&t2(! zLhQwO;%A5M&t6|C2{{$AHKCO84Pn~=LFS!rl#oya2bOH#t_==-*|fo&aD#j71~lKG zLBn-QJ!fR@{_ebr@(5Y+W(V;kB9(DzI00OSAPNXkK9VPnL zvGh1J7kTV#$)(Bbfa!huuf%m6-Ga0;!;g%kgL{O1loWp1xeh(KaS2CvkbmflSy~U*FFC-_vwJgcCn+S25)=dwzM`5Fir5L9MdSQ%5|D{0m_%oS2!1O^ zFmZd=M?_n*=S+v$oCGdWzowAV`KARB!xgJ0LHDHEIs}O~Xj{PGcu1s-06#jBLJW(MIqX)=L4}2N*!Q^mXtI!t( z6+p7)mk;aLH1~BxUpyT$7q>wXn6Coak0)H{!2;1WHQNpil_@rRN0kmUKEQn z5#iOlcQX@=I}xC2zlT4_n~x)ZKd;?^zoKgBMX+Ayi-fm35)b{aT2a)-jRq%mkR`t+ z&FO+RAlv_XhD-+Z;8P{rkW()mOby{{WUyWx#k^p0Y%_AM29pp~FN^h7+4VMPy+XUc zZ+1oc^RV6$yWZ1UZ;V6`Z!mA!^{ykcu*ynM%DBsUftgS>M?sFb6q;&W?ha_9E51OX zS@qGk8lZJVDqc|VrZb2F?0C1}(p20Ts0N7$mR0wPuUPr1b1@2ecgHGCW!L^&;QNsL z-e#S2X4D{4ILP6msad7J16NV6^BY>Pvt4f_R!pNl?_!0(sIhs$fiG|8M-RFyE~Nl; z<5m%gAj%?#??0Q`z8u2{$e)1bv)b_!{}x%MaR^m@C~0cPw$3?`V+h8+dtYCng1aSdBe|M6Q0k@jOKZEgLc|4$H{ z;i>~L!AoVgL+y`_DR1w8x1ck&`zCC6NA@BPYI%!r`P-+rZBlOYixhmt?9P;4@Tv$j z6932M)p6V$)u!T9kOaX#%1wR)N@kS?ki;x6_yi`vIY_%JUg8pe`T=E2@rcOw-wakf z2!3>(abg5S7PATj%MhSuZ^hWy)DljC8Trvf11*%!LW$@EW#s4QnI zC3RvT?SyZ>yG@YaT|uqOEZR)vOv2I}=MF&h{9<%|H1FdM{xCG=of1a$6Rk+(rUxbZ z?+)|Ec149LL@?huS%KMz_BTBdUqAINu4~B5f<}#{PD2{ui_NnmpfThOEjiTa>tm)J zkd;0i1I0=kD5NX-dT8iXnY??!k;v@P@DVgUOG>&5+Gn+08JxLQqzO=@R|gI~jF0pQBxW^NCIewwjte)Pb;S?n66t zHCWg?tjn+cAcLNP$HOt`{v31*qUQ!o#xdkM+V9f3Z|(ELqgY}=#b(UWEWJPxG7xu5 zr7d!l>MOM4AQ-_H0Z-MK%w<$}s(BcVd)R~x|1PutF1G&`%ipZ-Gp$|-&c=VndT?pK z650F@lUy&g(>3eBj*BE6-^kOuoB&RGU0=O{9$- z-a67O>29&#(-mM26shU_)Y!&?DPR|{@UQ0>AS5)$jKLljtmXV8B7$@NJ51q0`;XpF zz-ug!4=)z>BTOZ9Uz*Laq8Ut-yz16IxHmsE9i(No>5AUCH@(wU!r=QBL+}v#is=}J zyW%$P5YMLYp6u$MZ12gEM|iB9(;3?{^=V0h&&W>eg_hvv+-W>fLaw{Q&wf(gi|jN< z^gN2cJpl^^40)htvd7*A{y2N=pOU6-((hJ9%CR2{Qd*_YvFyDMHxSWph1DC&mCb^OpEpvEZ z6sdKPdHw!x8$<0eJQ&N{s>Lr(s1P1HkHuGB1gHX8@$uN+7=4?^blctkk!~rl=t5!9 zZcK+v#b&)%{;Kt^kE)l+?b*eyXS3cn9@TmmM%5EmUWW|!$2X<$-G5dn)K0eR*(~~U zltx(e7_jI~Ip-kdv_6>J%&zB)s+Yn^d+mDv#8RWb(f`!`M%(ow99Q^CV2lSG z_m(Xj92oZ}9N9DPVo3w}%F4GhC*>@THOE8xScpzf{#rXdv{eTQe+rQgXe%2t))PGS z40K2b`-_>H0AlbfWC9ktJNpRbS0M%FJlL3;`S5CnXTki=G4Y_4_mg>g{IiQ_ zH8brWZDKK7iyBaBXfa!X5*pdIoJwM|CT!j#@96ukn(;p1vIGi6JL^Cj9}5%LYvJoC zv~7H6F#loX2MRN3<2zk`UkFj96Xp?mOloG)*2jW-Q4zgjfoM$(bo5Pz`Gn(OOJ$h-fe*2EuyD?eQ_N5|rEy%UoNwUU?7I zrTJ}`o_F@IgfF$bs~;g0cR8LF~p*=Onv9BrqOw+H2-tYbEQTDO$`P2w|fO_B_ zh&yFPM3o2byQ+!%b>{aE6!&2)!!0@7&)iD2We1F9_hVOZr=Cpw!YPh@(7y4g24@rP zq`{STgOG07O$CHSoq6=A20K?cUr4hXa8CHbz!!8#*Vzq(p$sO$t}@TTh{nMLSrx+^ zc*;Q4m9%$NNLI4`WNT$&PuM>z`ucun$k_fLu zA2gq?+J_H$d$R(_pH?V3*d|=zO|8)C*b}St^Nw?3rR%kPptIjsJo5#B1PGnj9@g(ZTd9$iSQ#2$7slE>P^vTFWk z_As%A4F;B(X3q51=^0`mZu@`2TPV$Zn5dIY#`X|g092vo&?SKI^_`A?oB2lLU)%tr zfA=q+Al=+5{SfKU9{d9oO~D@s97tfN;MvT1?}!JQEF!&wQr<;Kb#?>`R;AaC7Jb-) zXr96Zwg7BqY)s=yVk)*GuOX0rc3`)#IoFvLzx?jli#Zx^76Of4b)2r8aljZ>-SfCT zSs0#edDhwZx|Z#FlLG-o#h1>FlmlzzELUz$bn0*K47oTFt!_rn2OsDs*c7&h-D2q; zII%s-Jh4t4|)d?|(<1%t{BRi|CWN?}4TMnLatR zV^o_wlN@RDXZqw6Tc0dys!#s=+&|JM2ci^umPr_)CYofu<n}*`~Zraj}*@kj$_OW+l#iHWA6g`ql6xA9c9mq-0yfzE9redF{6A1Bv#WV*#FI9 zCuuO(*Qi7?4pu>`3}h11V2sVL^RJO_l zwNCE^5FZn9>%My)E~5l5&4RZAMPQ20i#kw-#HK|AlWN`|tJiyfjs`4hK=>b(4<76* z@Pj?>@tQx-2X5AC&iViQCcm{~Y5Sa@EUULlKSm;uoj8oc^L7Ey%G79?pJrwj2_Ys zoPFo9YwvImp2Xzu=RP6&VC{Z27s*$>Fyk~RYQ2QAKmqhJz!%9pKSl1^MEu_s|Hr5E zC!j_l{BsP1^=^HS-5n#0Qy#BK!QjpKJ_Hnw*#xz8GrL){hARkTi- z6k51SC2JT?!sjvV?H=?r=I3|-7Hf!*od_cCJIxaq6y6%GFldx8kW5Pt6r6u+ujd;A``h=Yv%et5`AymA=~X(L3a;+ySY#eruhI$Q|mCn z^Hwt8#U7KsR*}^W&daEdqp(YCd8@@svYR2*uL4GM$HQ^?6$a^wZ@H?cDU7`tieBGyx>`Td}mu9ss=mEqHUa!cM`I` z1ZQLOo_6!jPFqA2UdQG)Y4edzetY|zo%aN0S<*E~j$DY@Z5EKj>;kZsuAwv|3`>K? zD8pih2?em?R+$z@C}I&#JLFy6)#fKL+V~!6M82p-Lr=wL=Ic0cA7nnZAV7xhNyP9JLNfz>5Ty|M0Pv z<5)WEU*O!|5}o6oo&r)w2!{AF^`)>VndZpsrV!&(e+dCK<;Xj+f&ANhl;96uX>dsG zBxv)cyXLgQ@vv{qYp&YKAQ%AIFXg1~LPmbaHkL#zVLHf0 zWb5_2MDk8XUswvcflb!jrX{nXcLLDxn~)ptBJ`huxd>U*+tT)_td0L?$vi14 zYaV4?Fs?ZH1Zy9fiSuYOt7vM0&)Oeo)h6-*4!|nA9VY172ZTO^X$CrNd?+$$EC3 zgC8dl`E>pEc9+rN=8U_77$R34qUc+-mP4MI@lI2DCd_Z2wW}z$b}VU*oQjqPZbtK-WKfBzuvFGnk!(dT<9S}V{0!ALp3JTQpBT4N5y=wQ^8 zTJjC_E)kjF|GN?=B}G6ocg1U9pr+fugOcVbT1kqcz1RJzpDmrYsl;O8e(ds1NNf{om1t21K8gc}zwnM2J7rhx)W? zq7TJI+WeV51Vg=?uMwrA&extroY51Xuk}DF9mv78TI)TbPJp@DKadyzqaru&q|oqF z0bO#=$O&6C|2y5@1Uz4RLt0jwRY!1(fvbHvs4?$W$l+2SXO;N<9Ak9;)Qxt$5huV=zWBt7q`q z*kVE*Cqj{&cDTEn>(7Y?2&~ej+Vzm}I)pDPbO_O1&(f~pAwKN6<#yNSv+IG;T}#ft zagP#RSwxUh__zo1rwGu)RH zAbpP0^(s^WKzL#5hyU249?GugM|UkbhjC8|qsA2=bge5*P;w|g0{y<=fp#x^brUnX z6CO_NWm3~##-f+8?4@H=FP~zo*)#gV)}}TvE13NEM|DP>?Ow(}&{Gd&tBm$T;KUTX z1+eSi#0eamj^x7K@bb5KVm`YwuwZ5`HcKwld`N}v6;hg@i#P=<0s<0De$XE1fBvID z8wILnJ2~f~`yzCIhtquyyKjr`@jwB)pN>;jT%ak`?xpl!xM_+K@rjC@u)sNb;P+4( zwo`|p5FceQRDNI3RmnNzjf$rHK1>h$+f_geJHwM%6uZ(s1UJR!Gnoijd^FPJcES*e z80}4R!jOEt|BCTw76ciaP&5 zomz@;rVf`B_?ai9kB5r+2g;DRHFyVYUe^4bh=lME zJY4P1oYyo53OsTKFy|@FQG!2^IYG^tB6ZGV&h46mLvgek#GLDxV;{#3Mh3AC4x*PJ zd$FdY1j;aeeb_}Oto-&r^)|mv6M43`%ClzwPna>b4(t(C=JcQUHV#_?u>1bNl%)85 zt9BmBPmbKQRe0O@ujMqp?gCatEs2(&XAchke!-^p2J++Z94xJ7JG=>ALLsKZ+nut6 z8%4v_YWu?)q8IgwSS@a8nKl$y?bym_rk-cV}RJ5Cq8SP#)-4W zQD?0u;qHN05xzMnh#(&7jwwJ^&5@77`Y+@HI!O5;tA2uT8s;BtscQT(Bmq|A`ALz@ zDqSbbw8QM}rhxC3r{gH48x0mk0O+n?8|yuTi@cA)GI*O$qV9}_@%laBB;`aNuA-jH z+nZA2L#MGjF&*5EB$|NEhj=ykJt#a;^_wxwzz#E8)?JQ*W*~|BTv+(cAYuZ|3$sB3 zomRDN)9dl2W8-?7=9|4DbB&lSh^=lSF zR(s2uU*MEE*7@dLVc%gbUTnu3<<`Bb-eoUC?41GB?0iT zY>K4c2XwX8LI&K(CF399K9LrPC0Kh(`aT-GI-pV-KQI@h->IQj5>7!82gU*Y7F7y8 z89|Zi%pjnF1f5ZXM8wF2_3dNS}iEmW}<^JY- z^qN1QoKX!%sD?`;i+yiJ{Ar- zX3#Np25(g3O&;JF&N5&d0vVQPO3hR2L;_e^bw*zI;aH=-WtF-9MsKf?Q15XA$b%zV z@SQ9GSg^2|ghT zN<&-V(bcg)23$WWX~1Dx+(CHTz#6z(u*#v=UTvZFu)-e**QSQBGvPH9Z3weJY1_jF zYIExQCu?IUxpn494_n`NSCA2t-%JW!mw^4{X#G+8eqE9bTK#F)O3NCvX}?2zw*%s~ z&To}&B>02L#Vol9x=mZA%h9S z&H{*n$>*X&cf~i*70o4w*;h7oSKnmQeKmds6D*WNqHDDn65Y3~HFS+5(K&%WTCZih zI&(=qjE>y}mkye1&#G)!}^>#&AkPXoJ9=ojpYT%C#UzBk za)HTT#c5?OSQy9u+6a4@dD0inUptb&rh|w1uC1M`jP()0ga-3jD9lL@wv4bfvdRyR zW|c0o+Gti;3?DUz5wiDVS!MoXa9Fb0@3E|Mb5f+e!z$a3JRz$T5G(xlCdlGyAeK;3 zl)rJq8xGU)7U58q{Y`&IKv1& zbVtqVn+Z{~I*8e!HvBVDdk=niXvonw-I2^hQs`?5Oe$7sB`XAzfBuXnlR7gBfMmOu zw3ERos=}Qk7Nyxm^u|N~nJ9V?4!bN$mLl(LWP6ZZiol`FK2j(bo~IytEFs5p=e=)h z*_T?Dg|aCu>y#H5GZWeH;mSEq{NV!$Em@x%*k;c}U zd!~{z*O}!7I6-n$?I`UgI^G^MggKaqnb1aXaiqT{mp3vQaPQn;Sh$*b9N}vEUWngH zk!hLZ+Cpf5{UmN<2>Clq4U#xBqpa}haC=j?M@{N**y!82(Gd@$Hzyi0E1p%Y4;&Q| z(+;!Z0(Qjv_G6wGqF(wi2oxFwAK}bHro2gmIy3zn5&<^oW;eK28w|1=6xt1@$QK~b zqQOcWuEY#$#6R;hR#pNK!{#&HSK@ySYedHU(^t-9{ufLqucTl(jD805TlrU4eRrB` z^_E}BIsX2f4)RBuBq7XYGW0yr=K4iC+#S#8{CWyVc9^*xKsJbD<*U&IW;zV~8oQHM z*$JB7rcIwjQ{Fx92_m1z*EI72Q}GTgH{d-dV~qgJjj)`79bFZ=g-x4Oqxt^;Lbb8l z-=vyVI$A$a-9uqN%pMy~&jt2RwEwoT%Cs%nO;rTrdhc&KxsKZGl1VzQ7Iw1oWjJqZhzS5i0?i zeg;bUmudk2PUT;sc@DZA_7EyrWfDCeS*0*6_}2~lJI0A)zF^#e7hA{FwqJ1gAIPxE zoy?i*xssDs(bRls71IsqoF_)&Y-V1iU%BXt#dbV}OVLI6# zdB!eB%eX+-Nck^!>mcv6%cr8eKVHkR=V3mPa@>)}S0RGKZ{1!5Xc-jY;g&brZy1A= z=O3UgxM$?35-gVhr$)JxxeXsdl`c-U*#M9_2i#zLwxAyz_~8?_3a|?m%+(KJ7Xt=R zP@n>Q%B-=-B6W&)>)+kAHVA8_08sy=hIn-h(PTw4VR-gT(Jk)-HjFI$+A1w4T7X1-yqlOXFSVqS zu>jda3f3s*W^Ny<*^HL!)>SMUhc>v&GMas)?+cA&7l=r-TG_y3e2E6_Q<0Z)HQU#j zg?3&V@~rWicZ=lVwhgOd5`a6NOJqc4o~2RCOc9$476X6*Dc*tjEkc;A(ygokM?`y= zFIDl=60A7Y3<(g|?6()m6uDw)d^E7tcB}L!Fdtg0TazxQWJ18=3m_aW@rVP!NnrvD zo1+k?Gbo3A5grUdn2O^84(Dmeau~RWF6e2rJa+`h`d%d^eA)lsZ<~M3&&If5noDVtImaxe@>IOJfDC1gR*5!H34SnB~F-1MJvpA5ol& zFDBe021}qe-zTc@>vmi~=wKp6XwV~O3e>qO)8`E({$0@OQl3#uEC#l^Q(Ae)u*LJh zz!#e#f>AK;KCpkSL}@KSD~T1&g>IELqgq^wQw>zhwsVHQDBovmo?*9!XnA~QbD3h6 z|2@Y+$c)|%ksOvwt5g&x9%WFv;oVM}Nn}C|n5Ow@6`mX*NE*y_pR3X_jeF*C{7By* zW@vczWQ2TUN5a^$6s}hA@)1`n#A-m6x061iN4~=Tmgsrx(#q0Z)+31PQMzkxXN2|O zD_Si68K1+b9Iqgqp#*1$vK3LFw)OL+&2;skpWpU{kTlWH=?{-l${T_Jr|RdqhZ-XK zxyGd^(B|MVDJhtao&Vz-wst9*Wz{kdS=9ml`_uH~U_Z6cbLYFOHS zuSZo{;b*$}%qc)g?K)n<&2oB870{$FZw$d?E)#I5tnSnnHz(?<7@{3*V3 z0D<0ap}-&)DeNP+q`CucniG3y0_lk!Wfq8$Rl`RgF?)4xL4+$^2j^d1kAfIk5BG`` zOq!=4xU8lsaah;tuT>D!&u8>n>-e*M`)Ct?w#a(@OY23v**-7<1lAz*G>niOMn1r$z4Rhm z{VKyR5MFwU;VujVpasVOP|A_seiQNyR0nW;b8#8Tn0GAn3@9rIF3-wN#gWr_=|d}~ zYQ>(UT&wcsGqgZJfNTkygSR>Two3`m+jHqV`;u2e6BMR81E zbnTbI7SXc8zu6%n{qW27f!V%9h^4z?8yjx8M+ATf zT12Bnl)cWa1H!B+Xoz>R7-KM&E8ivk-(ikCsMP&BoJ0d+9ZDJyW(wsWHnfoo=3cAz z6#Pu9*PrkJMT~{U{-mU(~kuYX|L@4TS84p&aLtL;UZFt zOX$hZ%00&&W`X^2Sf$p;ZFam7Iwl_Xm_rYce}mDV!oGGAPJvPImq$)l<`jhpIq_QY zM#thho`0+EkMj=8+CZ+J_}EeBd8Bdv&-gapP`Npw_I`-JFf7c#q z%*V@=!C?b1XgNcDjzjDri>p^!pGSt$uTVQ=J3B}JI{hut{?xuZTZaI(B;x4!?<<-* zI@;Yh{{P|OnDe!93I+9UiNZhNi6>+s%tBfmjA9{82BiF?g?2}NG8j$W6_x10y<#`g zW+!yhqxs3U-vk1yszp0SiR`z{lRV{ z`{N{y08J&a0!;xcdw;wX_=Ejn!>fl5M*QD&2m-JD?G#>EF_O_RytW-U5xkuB`{-92 zA3h<_GSMEMp#rpGqz#dRR8C@c54k~)i;Ex7-AAj+!a=I%t`ZAC6SQ_h|H?WkK2Nj1 zhWBfiHnm@Bd3Lyb(}Tw=KTFG%U#LJGx*y7H^m;YZI=WsTC+K>8wk=H0Ml5c+UMKmF zUN1s1vR+sH77j=gkEW6JYCuCu+hH~rP0#H0@-;Zy%}mfC$a-BM!>U>dlx(tIXZ`SJ z>($-qP}XY}+Pf=gO|+5xXJmxeD{!5z*XH{o>-EV%U9Yxc`1rH+T8?L7!5iE{5#1FW zZs-jB8x8OKwT{C3nzjn>8$S-i`@?&h!uy$pN5h-lM&OMt4%Q<+lnhT3 zJl@NWmJgnU^A&Vpx!RuoSCJ0}9alc+F@^SEtr)Kr+vyc|NQRBR7wr2l!v34!XJG|t zp82R=5i}6@`W#?}zCK7T&?@DdC8CNk4F2X(ic6*UD*SLaKOZw-FzbK^Pp{?|S!LhL zw8wQ%bys9CNt^QqbMR>eF%9H!{27bTIuM28R^83OB@6KlGX)C^-)b8AVv6$7 z$PS;Kte>+={U}RsB)hfBcw4}OXl3XP94vVjd#bpBJx#kydWvUHbeJXEz7)lRL^rX< zN4QSN(>FCF0MCY+r=LewFyU;}!?-+I5C-@pt6A{^@lAyR=Mb*!MF<0}q_JaykxLW% z@aV7*g26V`v!(r+7>JpPLXogG(qA3kAUsGPF*^)yIMiKNWX`vvDg@BPe+U-_tWv?F zjL)pHa^$^EC8L?Q#iD}5;x9$vfZ`{yFj#Ycu5{R@9k&!DN$Wu2hHm~otCWY@VdFI z!A#qsz8|h zenbZ>%XX<&X9m|wiK^}piDj5H=iouY4QA)a??_tiS%%ty5s7%q8E`+yrojaH;{d(5 z=b=S8r*$9RHnTkEG|UdKRbprTYUXak5>%mqX~Y>mTKRcGfzUH>Rb4rl5buPQa6Gpi z4E+juwHaoUHYw01p|8-y%_d*}%{d}<8obWs1Wf;qc~a9jK8_U`8*!-3gxSrmpxv6WPTigk2@CyE~!T(?2|0e69 z^6W^w7OSg4@22-FP^4`rZ9;cP!-DAeaktw#2Z!FA?uchcW8a)8A?uy=Q|E~pHNQJ%H z3!Lu{u)hy2rwKq36(FtVwNw$E5JBk1f)t4Kz0kKrfxeo3_Y;F~ex?ZdQ<;;AuYr80 znuRYPhKkb8ydoo3ssCEk&@MxrQO{W;Tb_CDKp1|DaP=KCB>WOj3_q_nFnO_rL5&%w ztw@;fybC}i(Rb_Png00-#PbQnJi$tOkQ0XUG*11(?8KUE4_d?s4BhBJV3>~_b3N9e zDGZ&l^RLR6dw-DOhn|F)GP=|$uh#Nh|K{|qAwSqixRSPiUdy46eR@w*`P5mjQo9D$ z3$9|@RQM>yNi(-tPPsBq`h3au5eL;R_zohH#R-1ct7(t-yzuu{o#1;Nw1(QNeu1a3 zZ5uh6_NwHtrOLKRb#7vl3QrD+Fg;weUqGa{L$L+TGvQ9H(q{l@>61=(rAzQj(GKwl zG2vEXi%Q>Bn~ei!n5BlNZv{zAf#G>bPKqxb{3#XY?lGxeoRDw833=HDAIM^LYAxR> z^d+&2Ec4alSVrF7U9v_Rc+VBk!_tE*X4R=|pXc9T?%4VRQJ21q3pV@^G4|9p3!lG6 zi#w6|eYH>KM{!l057)daXxx?JLZ_0iTuwM%5)x3sQ2&-(9s5_LOT~u=wKLj}Ahjf1 z3nyJqpAN=ayDR9wXZGB# z`?Rkd;|%r}ffc|2bg%lfXjD~)mri>U zM;2iQ8oj?1U#8Uxs6jv4E^2DeiCPc0T?yVLXW_z(8X14#UgI&re%W-VnKt%a8cJG# z!$1r#nwSo~O%}o+yr{(bX80Nqhw2N`!hoi*P(8W$&#x-Uh>0P=rwu? z=gvXD*!02=hXS+gl@nESMn(vPXX>%3;HBmJ;NWBBXfLA+=h^2BVN+pME$$k$?R@s7+3v{1}TA!V8L z95K(AZGL{3uujN9cfg(XWmv*>T*3q_ZDiMS^>@i)C!dO%0+YvCEgK*W!$c9v-lWd@ zto_zrZ?bMvprgsp3qZ`O!CsI|nt^X&|=B;WuE_is?In;E$A2cG0k`lnNeC!|xfS|3!yc`vKo)7h}vyfX+) zcExHLS}jgI1I0}}%E}N$&;GL42B~Pi!1FQuScA!!uhP(sHchw6=<(yov9PCKc}JU= z(Y(U82KnatyRr17;$@Zcj+)0Bxn6ARygXrM0NaHI5$VLsEQ$f#3(Zg&0Kfu`JKOeu5N(4AW&8xP62@!uX_GeW)rG$rBSMS3s%(R|GJC>uRNJzVJwNexk@)@7If1vnl4<7o z-xm*;kJLZ!-fO}(n<-`y`w|+*8xDaHU7?YA`vW7&=HoZ565?Y2A$yV-5el`L>7BELRkSpYFc2ODmkRySU+b;uSZ5G5ueHR zw@}MW8~G-*auS92P%M#BttG%R_QmW3ZGEA zOyT$uyL>ghKh{t?;W|$)xxK5)hm*|afOm=~a7!BGE6C%QX3|5e2m;q&lAKH$1(Cwr z&3W`k$>S54bY>)aU{~$ob)^f^C5}g1PTiRT-AiZ~%%&Xl~Xh;bBR~5`gFF~d%L6&E_+m>>4hJ8{@W8Z5F5yXxle$StkYOJ zAI$J}P9p}>z<`t=k`@vw8doX(SnKl z3t^idZyvu5C`BX81DZ%N6JGd^Ij{%YZ%{R`LFp_suw|m(kYUS2Z_SLD76iu5_p_Yu ze_(pOr6R;+qBfq1!a!^q(*ikW!p-QJ1b8^h5nevArIk1)(Ml{t82HI}(cP11B_Z_r z*c2eY8ZExmd4(y4BP6)B_AW)Sc zq!NPkK-!fGH1XgyGA-S-g4RCXW5`L-Qi{zf+Ji9L?zq@X!#W&_gK<~FF6Ums+skIw zCPFA1bt=7G(P1X$dLfG|=nHAi+$hDFw4%a@6C8FH$|4KmB^0!D9RYnZ&X%xUr+y1$@Hwi5BH_{=`o)EcXuyCYuF|bzp5+~4Q0b|wfxy#F@W3i zRW4X|s|>&@eFxR$)q*Ck67Xq)O#YMR`GWxtu?I-k2YPv?kvOK}E05ixUpZgD;)SC^ zlbJQM8$Q8?3?TD=5%=cdQ5DM@a9F|~CM?mQAVDI6vV@(;Vg?eJNLU19QBV{F6od#9 z7TE$xfN>mfLD1_0ZixGWO8~tlvP4u6SBRj3BK8p3^a=!peD7P;XJ*23@9+7(KR%yF z=H#5}?yBnQ>gww1>I%=ZA8UI8%Y`E9=OpuFK3+0ojru^&AfA-KWQhdblFheB({b;A zrVVb(Qodfn27ykYDZ=uaCE7_-f@AM4K=Br22VhUEv?V4mfE+!T1bz@FlCcWTH+TM2 zlr+aC*~Ig`o2Tc@Lw;^#aDdI@Z3|YjDaPL>Abyf+=)huavQTQ$CZRG?>DTWKhQ`$% zsZwycZ5v@77Te-kT2yu=3UcNE9YpuQ?U}RxZ1qAP+@FECGk+=OKX{eaKL4$Y9$RF} zHj*TaybdV*c^;a!UFy!ipQOD|q@z!_)LbU(S+KpRcLGa+I{yPQ5V-qLvIi5*ZrlsO zF;i}7A^I(!CCoSg(Y+1-;oS1;t+0LHQ(>%quel89sRDunV#yoCNla7flKcZ2LO0b# z_=_v=02%T8?kl?<-^@uh?v~b%+Q|+3>QhPXS);{G0Kw`EfibMqf9u=FH^ZY1vef?HY{Zc<59( zFX;$#Zs?A%?{C*8!uEX%8WQ_{2<`g;wC``A`icKm0+V~Feg6#)o6N3e+n{GpEc`Li zGJYGVg>P-BBZ&UJu$}La9FA`voIjc*eYQ&om*dWeX~&5ioP&&hYD7pphWp)O;&&EP zFOL%-xjdYM{|E~+2J(;?`Nbf7v>N#mO&>f0BN~Q?;p zG$f%qrXHa=E|*UzPBgCSMsC$#OfkqtU1TCXq+#-Zm5&?DU!NyR4isTT(u0&$tB5k`bN zzcx`mc9%-SIJSy=*Cd9q|9NY<0`JBy22uXN7NVCPf_N^Tj@L_yrmc1Ra_$8`=7XKE2iC*}C5B_N| z9&sM*%}~O|SL6|AvyPjnA1KO&rb`Xw8uwf={d0 z>tz@a8-$jLD`6`$C|={ zmA!__%8jrGkTL`v`xVD&=9hvea*tt^GG?cYQ)iX(mK|`8eLwaa%SeeoG#v@eWkgGT zNHF!D2wSbNcB=qyakZ``(w)W$ZDRh?n_^cJQ^uaNnh39F{`0kM?+a7>CQ)|fUWFk5 z8GaQGJFC;)W8F2zpl*JT=J)@#o}gJ+r~Ep?WyXUDFl8R(^}=h2-UnP%|5c}xv!L={^$imtZvLrq|GCS^8cIFKpTuhXEnfaj7>*$ z&wCBk4y-p(Z5`O0)($Mh8}`#l^b-unq=2VFPH9Eg;@|K$cOe3CS69`tyYAMq*s{O) zO5));S{)C+g;o;Sw3NkxXZ`TxPVKRvi1H+B72c}BY(P3|hCV&j;GCpQ{eN3CRJEuy zLIdU24CBXZD^)B(=dKxc%vKu22IQJy`$~59IctUqui|BR&9Ln?aL2A0xMw0uvuF7b zh|7_C)Jx5ycPR%i3vvGuvBGg{C|Qr%xL&w+v!XZNLT7{)3y~`Ts60+IKMSr%IHT)` ztM21Eq7x`KYez!}ZMESwfn$1~UIFaOnckE-&c2yDt?9iFFEH(Lmo^`dQRc)KDy)I% zjI4{<+g~Y+wYTHRdMDhx-DPTTFPorTKpNJq$a{voUD&P0Tx<>&ABtAp z4mHfL>%#W>KXT;sxM`~lz7Ys~Ai4RA&5rR(K*&}o5&3wJ#*_C^OJOEEx zzY1+O27$IkFl~-j63^PN{68?Ki2za~Q2g|r< zEX7UsdaNtC5Vuz0(Att#RNt=L7f~p~KMoc)CZ;jF&@>hX33y4<0V-0mA?-J_){r;z z?iQ;!lfOjPwv6^UWhR+qMuS5Y%Pjj1t>W(at32r#%rekSzlg)bBDwpx<*axYKfQHVP zW{So*E(u!GU)lmYTf;qFt7au|xUJk&N-2iKVk{o|7}0pX8P#O{Q_|6V3d;_Cusln! ziMavT+$8t|O9vb7_OcIN2Zpo|!%LQk^~zhH2~V6wphS%X){;MAk0BXyE3RB0NC)3d zSm`3frNEm(vp45df)`P1r@Ca#6W8!wAOTOmYtc$#lc-o8@d}n#9s!md_@-L~mUit} z%D9U_%q9grc^IJc30fxtG#f8S&<}VQCP>{8CSNO9s1>}&3i4RNPT=5hi(vZs@@V(pJAA4dMh}==@}h|4#pGdeVMrcg&G!W&53!CsY`9O@mpe^3Etz&E z5}B`nq(i+U4^IQZF#9;8IPq`WEIMkwPG=j`68G={h6gH#V7l|^IfQYFP{glCh;8b~ z_gJTbFHE}V4_9u2%=U35Q$yp7+R)rL9Qabk?q^+UkJjNT5(AH(XSk=~qKGjMqiKTi z&R*gos+G$UGgH6h74nab+#n5f&qFI1oGq@B8G8e#P_q-9QJoZ`pB+cl^5{ooqS}Sr zk14s=OqGgax@krG649xjvzGf&i^E4}ln+HcC28h*ZM(V_c@0IdY}mj_(hNDT?JL=~ zZIU6&<{>a5(iD{yh3${HPgGx>(KveQtLN?w@lo_uyJ5mdM_+Xz6{@e+A{qb#YAOnV zCEJ^V646%$K=)iq9-#DydeEHGcK$1};_xZzY4vi=s30yMlc2#+eA6@qLA;qw={!hL zZAG2doDL7E>fd0qnnKHag|y!Y?L$SuYJ|`x;WPZV?C2`)e5yD!(O6M4y@1bo(&X5STem2A z@c#wx2UkMmhxW&ww-CNB&8%WLy>N{Sw(zZKyBI|{6M)<3?LxD78S#`I@DKROcM3|S zKo_myP(o=$<*Bz*JLTzVR2-y5PXI@MBsvgZn$q3U{)}+@RnKX^YPkJw|EKo1ZjO-W zJ6E9nvFEh^yaNIHwf!CdbLhYS9Qv1r>F1~aLVswy^l}=IdG;zfJmSp+pKqNUl9ARx z%G^9yWn_pjo9;mK+b+5x3t^&h4lJdVpf`{r1YJ;QS}Y<82+@Gcq7~6$qBc2aR0&m! z+PhySS6aL9j?1F}R1x$b7=hpTpX2|+sj3uK|EfZU zY;-Qt{n7JML+(+eyhx8V@1~tXIv9_C>1Z&aE!dCK@MhnOSfq^E4NUf!A*)0&4fTxK zLYG(XjO2YY(lh+IiS+MK8%Ad&Z_4WI@h?wY%{RCb%Wj;dCU%gJCMH{9GSZjP6wL2h zSTJ^g_L49NIxxF%NjM7VN`%SzH`OV9mi|IZe*)SHda9MDpM5TCmp2p);k&rfS5EAM zcpphr0?XErKo0IQGwo?1ur6uJMy>4U>KVyJIGtZma#@HTKEyKJ26obNRs@GSh(}0h@19{4de(!~zRa-voG9;&naA(9jkXhPbo_FEF_;*i6pI=^4gz18Q~x5_HXtMr_tsq@WxSNQwCE?w7`cmBt8c42pPE|d z3p0u9&cM^eLR`DO5h7x7(g2WpW*ZP=^^1XXc?a$?M-|jHo8fqaypHqu7xSge_r6L< zMbA!+@+O-0i|B+~q;<|9%TNhWoh2eZaWxIC(1miTo7y<{(<4pa9f zP7yfWS&)Tj6P|$VUWBhM+PZ_@>jb~A zB^srLJae!8kyeW0BSVBBy;T5l5Z292+8FebVt!{VlM+Z`$A0_V+>in=QW=4NDQ%ER+=JI6PMA_-rufz?E-Mj0mG? zhw{v5_2`|#?CUu5;#e9&_4N2ZyK#843>(RgCAyigBOab%KIsL|gxST#z% zmtRF_e~klxJ>FOQgd3hJNN;q7ap*eyw&1VR#R}ia!beVRJ>dr~H?>I-g|8BVXFpIv zqk{|o3;cKMt!P9{opqO8y#6&I_;CGqJbEE`${%Lh1n}8pYN<5ejttmFJgW7X$ynjc zrvZS7V<=Z1%DnT*RTtQW^m>q(?AkQcn8AP#i8C<OW$55oB;*TDg zK+=DFU;CY2B}Rd~mmIQVnG-atG6O6mt{ro-rnJN1^NnPFEJL}4&wt>&{~;`#;g08- zUpL}f*Yw5!koDe3EXuJR&NTfU^2%l=F=`|0hV0o5qcF~LHZ{cHCNOey1t+d5hZxP0bVyG7b`U`pbeh4mA7X9wF1}F6L>^acVLdhnV4Yf zAf&L=v`yr1&Ol7iF8GDCF zjmt7|fZ2QGS@~3yJDJ z^Da8tz%-d4)*>%SG$XX`>$Gk^>!wf6+)HX{@1uddJDmoIT**_8x`RDsl|?VqEC!XX ze;-zphIT2Gd7shXOaY$V`S0OhGc=V|Z`A<<6+QtlO#uUryYs0M&4JZ~kPBk_aXVc; zs&TcR-#esXJ-O;mq5f#Bt|z6BHiDXSonVVWNl63v`r|l|aUaAdTyJAJ+Yq~7w_;%} z{(M+fZ=zW&Qe;K?OIuxH^i9BWLKheVAE&K+inel@*@WaA9D(C0&SL;k z=1ub?|24h8@o`T)5cBn|B4^W{f!?f%na?}}ZZY93`?G2zr$5&LCyMGA@GW>5DogjZ zc8dxL*1}&I{^t|fAs+v$k{FVU-^_l=zafvXyzR@my2ZAdQRd^7suFnOFK6}yME2c7 z)Gn~fL3WTsSPF5Gv%N@c(`8J~YIZV`ArNz!BEA8sG&VE^;wAU^driQ}{a7rP5>kEp zQ5r5mQw7&qJfR8#x)&RqjYs~Em`-;N$ zC2THXXGOwRj;baTOe&d&QItY^;r+KOA`1nFk*D0E3wYu%h2F4~bE=9fcN=+eD%zDN z7e8_3SSyKvc zB2)4-khzh_G*GG%Og5lX{eITUt_6Z)ph1K6S$Om9!V*T+;{EF^JcEVzqEftg3n#j? zGuNVYa1_v^>DT{CnVWCJ%pyX|xVC-9gLNUOHT$k&sJVRUxukLvij>Fy2m!e;jwTw% zAz4A5Nin??fxs<-Kr14U3Iu{bz^Ym!~8g}&QBk})3pIQgs zXRk}W;^Tr%XHPsml@wB^UCd*C$7V%NMrgEm5?8Y!G6$CSI2pAVtef$YgEe5nm;0rsD8;>%&7$Pd&fsF;cj1Y)B{=fM|+Q8i+0Z z3SU)Lt&Bn3tl0HRPeMsRAG_*Z@%rSgyFF`74>1*dR*b5xLr0Bvnp*IG{P zzQ#efY*+~0O_e3z&>#FJd?I{v`MU z@ko07^$;RKgS{3O-5P*^%c&@q$V;AAp*h2 z`BYKMWoWs5XHc#)%UyJMhZ8YDCji^RNt@<_?n}r!U1)*CO4vZNmui{ z7CAChuv*C4aqkiCPelo^Y04O;VZ zt+_dCZp^Ofg$X8NJP$@`*n467sNItD#&Zhz;Ed-@_5a6s#u{fh`F0xl(NQrLY5lk) zp*e|_&gU&d19~9hgPj505%MmJP==mmz()B5RnTTBkf!G+MEUznaOKjHk(s%>JYYhm z6vD?7F(e1Xnkmg4L0fTCNYHLhwnI{YVY0e%Ysh#WAmf75Mk?UQJq zshrJVwv_7Wdaq@Ie?XEM)%a9dvT@}-1rWH)63m=sN6XyFn_YRlTeP3=YPi*8B^hkbkyLd&5G z^K#>`kUxU(UlRT^z?U>O%k}e*`uRG3#)O0YAAEL(kYmfu z1jjfE2GW)*S!+kZ@N%Y{>7^A0w89|PKO11UN10n9Dtrxul)Pr*74!}Towx;{GJKOF zKskI%7MjBhjtInbR?tUSD|`|-p9oNqhe-k0M7B4(wQzVqiV<2KOre!nv zy^2x6XJGt9W8{zt3g zbXgm#6Kwy}LzOw>d=HZ2Z{g~|v`=~4TmmSdq8kdwaSn%`7;Ktg zk30&e}SAChNvL(QxOmHFiz@=s{(%eH^=dA4r;m!HChzir~_m4GKHKiuq2`?AlnxWkD z*5{Jyz=5Z0;4cewZ;e`H2jD1Fl(~%+6)lpAP)uqn!5-s=0{$h&xqJOKG^tetf9h0j zZ-oRo^t|)D(t~6ek1x=batCU`K-|Uw7*(tgX%dM2r|r=w7};C4R@k<6=+{|?D#F-= zwpVy~QTJBT&8XgLhwWrB2FG_XHEu$EftVp^272t1C=BuM*v^#_z@a+Lnaq#iU63)% z@EnvomfI2jG~k594mPA7d0O;LZYI0PW$G&c&+TUFW=-!*oUUme<-NizVK3Q}xv=qv zHRmqI*R#rR(p($fqdwywH_Xtw;>sI~ z$y?UZ58$O(z3e?yf(WpWocJ#MK%z?b%wHez-u;syo@fu zplKG%-z`Bxm=P4tK+JRfq{D8i3;)+|MB-k=Q(RBr9Pr8i#+$_G43@&Q`T}W{ zh}~ia;8HeBvjlbCV1j>Q@zJsth%CU{W0?GLp=H}`1YTa)VtdY(vnA6_F@r4vF-^7o zZ|k6aS%Dr0Ua7dl2~y<2^*f46EhNQ|{Fc5gX0;YPlp%;eiGsY3l3lZb3wQITRsekw zosJ4hJ_HsCrUnr5ZOJ=4vtQRYWHow+8KxCIqZKWD5F-0EE0`h`EF+Zl8!f}#?23r2 zLt8WCZYz%GEyt)&Pq5cf_eCd{gPyoK9^6<@_m%wHBl=B!zxh$IXz>`VKlF266?#`6xly?ILtkOWvRH7D$L&k1<37kMHqh1= zemOoMCt`4tbUtb~gBn04$iQb&ECX5;G9zAc)b%q=(C){vYgT?(`rj#p6j_!T??6`(;OKnNOn;qKko(}wZs@PRlqxC ztAp1!cvDi(KWqZW?hFhW3N*(SFnF_$V(?}c#Q^z5hatwK&>ZcnaQlSiadX8lT5Og? zz+rC|=cg^4D7!%1yGymc(F$jygK=aarCQFZ%H9St@)n^^|4NEI^}?^$nX;0rQ~k^M z!ktt4&-A|ToWe@!V+tY3nFEn-$zLBHgyUwUG?mr2WPoQ3kH;p;w_2Zi3insWpNBID zI;2iDqLl>LV0~IQm_nGc3WEDK96jcJgjOQ(f|F%E*l zubyq|f|a?%ta(~B74D-(gJJJ%I9_OQK2CQ=nYk9wAAL(gNTuPJhI2}YI*a?BtL^WD z_~rH~;xNb=%;70~AV{_a$+W+t?e9SQn__>H>~DhojkUi~_V*Y_Vtq6z`McBp7TDi6 z?eA*)`yhUaNEZi@O#5N9{T+y3;+&0N1x<25Q!Gf5{Y|jHvGzC0{vJaC!O8yaw7&)R zmr}v+tL^WD_BUI8%WeM>)#iAONrPcmuH4tLqT-~%BnwaCIo%|ik zdfY3uRcK%7sDt2cM$6&&Rd0R0oUb{#COgWX*CyM zP)J}L55lcM&ZhEllW2is2^@&~<)AjxN?Uj3a&Jo%L1pjNa;*I~&V8r@c@YcER)BE; ze}$LOEE&OtASA<|iD-Llmwh0haXA~Hx-ewcV`YuGALz!FeW#Z-tuJe%UJ{GAty=j&D+(z(!Gb7H;XVwxnETSJNBJvPBSUiv%HZ&t zaW(Ga-Gp#HWRSY~4bT;c=}(NDJiqvQGrpE|GHo(Mea-4EI`$#%$bJn-(+80k=swk8 zFQBPle@v1n*Bj;1VS&bOh&l$i>tQXW`T0ERrLnGWS=ZlaBU}*;*=?vj z$SeKaCsGsRlU%v=*hhi5vp(&Em)eU2xN;LgCDJQoRLN>%yW%$KijjiM5Ux-QP1g?E z6@563LzJW|%uR~Tsx+bO{f@xqCt`CCuz?m@Qq|cm;flp{CPuhcOu7)5G_Iu`Q1+7a z9K$1X;U{9B{I0L2;MGoCL%>(X`f4Jc%i^9qSW_fA=hQ#*$C({B2vgz}jU=LRmS`LV zdLag9lEH_;1B@IGv}KtbEiY+~?rYz+hOP(s38q_FKL06+;h&f!%kue;8xBxT>Eg*p zx4^Q(ax3T5Sy$e}_{$9z#wWP)zQeQWt_Uqo6~3(M0EDVw1PH&tmk!T4d?8Y%F}M{x z0?gRw31;;?-3_4iX8ueU(zIRuDfqnrPlHw$b3@iM(XBp(+;xNJ93IZO z9`lCQQQAweslz(rSw|7DWM7_BdUME!AjZl9v=pTBLtorcU1jHcU#Sbl{yh^v>%(%&o$4T8aFFVKT7k$4puh z+3OLuG*gm#vkN>cDo+!hNuD12)q1#(NJ6B9{*mAc%p3!&Pvhnol6fc!!8Yg5V&Ki(ld1*>BiI zC2?tJD+$=(jd*uVm1!%xDt>i-u7XnBJf?r)gd6F)sv)kXPH&B`Ddw*%R`HL*f49P}JRDcVuT!lnLchNk z`kuf#qk|o%mHWOT^xe8LzMWX^`}ELvr@T|YUH>GY6U;z`!i^2$AVBJc9i}BRf>6`f zBR{Cz=_?eovH~BN^#=Dw?l!4VR)i}N0STJ=U34M2hktVg{bwhvpna2LeYYY+oEzWf z#*++2aph8{V_o_tP~dPTw5r#ADW}T1(=zrp09@WC02K9>=mrTv2`)fEyrB3=6PbXg z%tI?~!I2vQVnN=6GDJW&F^@^y>7677vA*`N^inyY9^l;St}8S zsE}Ab!&US%M=;RXX--o=^iwvdyC3_5qEC} z|4qR43Rp=2zaii^1pE?!IFtzeh|IS5ha=>vJ{Te5e$UZT+pm}EpJu7ISn3I_e)9de zX7dg+N}*mD&Q5g!>HNucHf2-)ppt&$z5G9gn8Wm;S2Ftq9fxf<67)pRzlO+vpZ!bh4Sw+=_x)~8@8D|WHsG}bXYC=+H%PwABVy9sm0AVt-NYsuW< zU^+S_FXr4%X$-ETlKC4=v=$wpMVqr|YZh&!Men@++Av@11g&3suQSYw^!UQ8+M8#-?$x zw+OFFMtFRm<*X-aW!{;bN@0TFL@t;=c^c&vHz{C40LPTEy^MKqguKk^{}IRZ#)nwL zrKw1*U!gK+KUJ31@go{@8^uzed%X}^lq`gH#GHar@EEAKRO*SV@gUohXpc>Wy2SA( zk6jw>3s25^F*CAml?@FXAL>+#?(R=uYK=lmb}zq4BlfzpYlwi>V?6|!>lB$@uYl|O zC?3_q_~4dtJ=y&Y-U&)hA}WiKJ!b$;!2NW(EN(zCT#(3G+YfnUH06N#{Io>S&U9ywZ+O-mxrs`!4~(t??`7%!VL)5L zWTy`ehspLUX=Ao_7f!`?k#2jb1~@eoGoJYo9MpNk;nS<=;B@Q~nN6&k{>}RbOGGU) zx%WXf6v#!Ay?u7qC~s@?WH;f|rHaG9u&miIhSj|T+#DRJD1#M26;~pc=ZF(_u%~Eo zJYMlXxDO?;Ho*qm?mlghX<~FvX(exOW~j+~3P9}kYyrm7PQ0*8^)#wU#i$Zw77L0= z=Tc4uVO%nS0tNt)2sJPHwqF&V2ZKT-`X&qK@RHDKhh!Evq5Vz*&MBN@L~_GI|}X3%>V{Z zu#RgZFyN+q$xOtd&dKoSCO(G0JpSd0(xd(=5Z;05awjbuVoV*HK-X9jfY5K)?JYG) ze@fTYPA{sGg14e)q0Aq@PH3;MEw^CH&*QUs3m7@HbL*bUd@isNDNG3uiIA@2gF`>t zHB)>-zY|-9{ab8WjsX4Rg#Pv*JteCZX~+gRBBhXeq&6l;3>Sw64Xa@|z_W?CY)N;Q zBhCw2eHN`PsTQkdzMGGO1A#aoqD8%{;S=c5<4i8|8_+?2W9!@I$HGb1 zlKv(knT+rL1RB8j4<3Lk8|eWU^lmz$PgGGkQdf!cfn!hR^t|x7CE2FAE1vyyA?1Mn zaOgX*Sm=|DHE%NKoj~-}r|87^l1um(5->o3A`WV<`HW^nLaPtMtUXLK}`x`9lG#wnY%b`(?J=JA`n*` zCS-vu=EsgWzkNDMMjnQQJAW1dHpd~}z76)BO2X%@w9*_^9CZKoI8L(N#k8 z?m#iNhvg$())q66H8DWtE_(`mga@a3N3y>;j68llcnJU;1~16!MaB50eCUnz-ixw< zvurIeZ6q9o@Zs@K7%6%%eE%aPRSt${AfR2`F`+P}RA4H$jD{)ErABofj<4O6V@YxN z`Rr)n=Z)kiE+9C885sPW0~P|ouENhL^CS705a#Dl_=o_7n-0lOOdJj~`=Qd%U&KJ; zTe_gAaXT1E-$#?;c^HahsGb{4vE!j6Tk|!&UxnV77DSL z9}_4A=bbNqPruXm$MfYg$$9YuemKbb`D}H1J_GkBKq6MNra4;DIbCk!boy5gZ3c40 z(VdPT#CzWccE~`K#5wf$y@&T6|5Dqen##Dn@&m%tUO>X`A&CPs(EA><3Z|-KS4osy zc7KdjdjEb!PE<(TkGD!6-YKWZ_jqsQDe~bsIuXG}5w%pFlm2fahItCD@?bg6M?NHS z#)IXQGalh$*cWD}2h=8k8~57<5-qe~^J;vwfpH>pdY*hJKEp5~{}TXUPC#C`!f3w9 z(U(+!rj3F*r^y-oRk>1#lbnB113g~wcaH)mxid6@@;b)jU*>cgk|4q@O@?(dlhJ*E zNzERRp{L0a?x0>1 z1uoc3v?JAjX#O1w2zsbxnFa53_ubc0Lqa`H1pmwnvCyB|>n1s!u7jnSe1E1ly$9Bw zC=&fZa`nU?A*8o|n~ibkUrGgGZu#t2J}=?(06vRf(4C(}U~m{3tqU4+GH_f*Q zeqq**Bu`wqM@bzrQUg;vIYG5fpIqmoy!5q^BO_ZB+R0NQS!@o=@fCjk4?19UR%o!@K^Tqri6R;e1r_4opTbkZ| zfR%5NiXebn2=Gez8z$A_AzDXWd97CB;&43GhltyB=QG6DG`gGfDi6K8^6mh1@F%$J zoxbC5sQ)U>ll~I}&y~9xc!VOSu19t?&fo=?gpIs`s2xV$aQx87%eqE}!6bC|cKB8a z{ji0`9WpCNTd>!g30NAyCLZM^UTh{>FvJkT3mRfn4b$@urf4iGsSF#2S!!q=FKr>^ z|H~P7PK3yJJ6DA<5Iw2wIrgN+kBK10S}1??k+YB$o_@vXEt=DbPROmhS|`S1{-|YXs%qW4dvPPKVZW9 zd8if&A-Y16?&qcRm$3X#;j38q$f>$6-MIxJKm~X9|L)WJ{XX`lPVIyK3;dc8yig&g zz|R9NUjOtEe7OF0@KK$QPspBZj(#>hv|PCZ=9djqtI?JF6G3X?PwJnq@Q3UX@Qfhl zJXdZ3AJB8f)2Uxwxs)jXtBKisO`D)Fi`Lq_Sl{ckexdq!QPiv0LnUwypkSxvoU!NW zvCnwgtQsoaTkiP!L;yktaC|-Ethu#kNY90juM5fQm-`Xl?Y5vCUnlp>2o|#QTcZ@` z_&Q5PUfOQ`ZPuTiiB?U*EZb|*XHVPlj%{nPKP1{G2q4KcAPXyGD*J+qdh8fZZDHPN z0Yw^!xdZWsSb-eai<0=rId5_09<051KNZmZ)gwLVZBkVru1VcCP<6pEMU*r)!O; zcva+YK*;PvOCY{El!eylY-+v8rcm>;9>C~%Y}xt!(o=7scks-vG1Ks^{2}{+bfrPf zP{Wx62SrpuhwPIy&=R}5*WvUDxF?>vBs1rCfeRn9pM-z{K;<5VZo%*82lyL0WS?aI z=7*vj;=})N$e!pB+Xdh!?xgVLjK&M{+1X>oRP%>#r7ej7KCOsF&+6 zY1x=B$tJt2@+)`}{0-2d`E=cM6*k>63B*0X6Qi_J-1+yg^w-fS4Z6}`0dXE2bAM=g zjLac%qW~_;<7osQdIy{8U5y3evG-0A67Zp@cf-N`+DDb7!>0#L+x==O#oc;$zLy09 zRj_P6)lB-W%~!f?&fpli^FATym|bg~v%dQ!OX-2`g8PSap|`C#i@zZDFH9Ai ztg$TX9=n%{0{Ix_9E7uUYgbLe%ym^9Qmb}+rVyuWZ9-SWVtw$8!l09(v8uV!(wU?f zcZuxr_Qw2-6-ghDf4TK$s;LMrMSGHWX0@h`&56)S?~um zpbY~evX7T?{`?|(_B*dIqaUw&!yyAKZ9M)3>=brTbu=vtOZbf3vIa=N&C6-fC-WOr zw96Dbn}q$Hqsf5zS}lkkbs|Lca0I?!hJj6n`oVE*rQ-@|JVVP{6Ai+g2%hu9Y$8El zMZHXR0xz4zp~*kYJF=p-cN_WN7m>%VZd0tVk#!f-srhs`eOEeZw3xmg;bTV0s}%>E50}5Zil@3mr>D~m%AM*GgMWwOX1L7-aj8Vx|ZMT(FwKRDa z6bag2bftcCnU%?Q76O3v>(`&AaY*(;laRke{S_ia4^0&zig2mstS5u80B{nLmYRRm z69T`gaMPZxnT{z?Pl;aWK1T5-%tS?|wKf5Ps`a0cK=Gsx5x{gQaz$so2eM>a*DH)2 z2Lvx0LkelIZYnSl?^7t%8m&ayd(F+zUc#?Y{z-|=v6?}?57-OOyrAQ>%-bCaxG_$9 z_zuBw;0zNPq1T2+rc4GLo-LVNAuTR6Jn?tf7nRA?a{j#-p1Yw}X6o>Cd{G=!%3mSA zEx>35`lDTcmLtBU5HE?i?(@Kt?XdkG%XTZ2w+}sv{oIuKLunLw5r_tfxWVZMhxV4{ zX7mNVBtZ+{=2uzJQ40upmpfj71R)QKCk>pO3F5f^wJ1 zUWewdKd0CRnbhlx%R`e_FII3#87il`8N$+B!gFS`OZX97Nfz~vC6_(EKXJsFNU6&~ z(Q`7xO#zOqg^qc0N*BViU=~1GFux3U27$!7x>$C&xGvN$q3m#zFr+z@xW>2{D}40F z2sjDE2(Y1H{$<$&2*mw~C{1_%Nc>@ro}tckmfhZF=xhP1e(;;~sZHK52VbR@@ov{EdhCAcXSvDH(3{#93<@^Yt$pR0d{uw-3`sYIH z^<_WmdMmanbQ?(LrVKS+#Cmx=GDVZ;4yi4lvl(0J|w0Wr5*Ab<4rM?>Q| z$uhtT`qV6OR8lao1txNVn`1s{_I z9sfC~xhMEdE=+4W$!am{FLw$pmqJ`*nJ5%0*%2wt&iZ@NM5q7fG%er%l=mdiZpJ8x z-yJUtLWG#*-jDa_=bU|KeQs>M=vq7wccrv=oP{Qz=Gc-vPe_(4gBgQB`wWJJ2Lxvfi&9H3X`rR$}Cwo}YTm_T`JTm5Duk8yn0!KeM*;|u|OZDeSIU`>G z#RmxZqqB4wE-az>_n(-WwprN!D11AZ4sVN_zZta0=G?O<4T%xriq9C zY`qaH#E%tsX@5h(V^cdFcat+ndNhlk<8g_wmu}ZW>fF%ff$82`2EkR_UjjP)rKU?^ zCFzl9PHf^%4<<_8p}#|Y||c!^=4`(8ONiWze?`z5WEL%x5!Y1tm(k(^&2 znL!4t+LrFnIkX`FZP&_ANMAoUK}kl3E=f~0z%z~zrKxCXeFC1{`CL$&^(WC5yL%nK z(@?02WMS4Dj)^rMuYrTQeVcHG&DjKu*zMKhd>^uVJG-QAI^0rJ;nGJ!JQ zz~+yfYSDf8nql6?ZKhab;;(`%YT z1ooS~{PbV)qig%kw04ZYiG48sn6u1wlGA?WaQnsgN&9X<+V-zLr~MZ(Z`k%n{-4@k zGtL?RPa-jSklvJY+Rs%8gj1Li+Wu5YVXpKeinAR3;r93P)BmPF)PCV0(tm#YmBZ~9 z&l37q0@9)Xoc3Sr9;W|)v>!2kpSe?*5Xw4dvhAUi-+>M~{BE3?wc{X};`Y{m>^S(G z-!TrL=eiR&Uls1rH9?WiC?Z~x9xRZ=Be>Q}0#`v5V2`<@3a}n3^CqRbTZ&*9C-@J)mq=VF^;>9 zvB)L!%pD5)BMb?EE+Xh^g5DPa%7B)V!I+Km2_=_kx$`Bs?j5oWoC(C0`f5U=7+hD- zor|De@un)OMxE_YCxlx=tDBISNgip;#J~(}dIa5bK zXDMhlLHDzYIB>}6kEM7Kd<5mvi>lJp)b8YId!EL>kso}W^(8mKMWBBM92nIE^@+p!1yBu^1w)P_>nN1`n`j8!vHs_8=Wi z=vf&QKLC=+6EP?}{#zmj1cm~vHKd~9a6reW`hM=mb2C9Rblb_YpjfKEGf*H9Goill zZpyb{NC|PT44yg+q0|Kz;w=fV($8U&0;GLggInGeVAuMuwd%9vSYK2f9@oLo&fDMDh?&G&o%w|Jt%)l;srKyNL^>_{Cz!AGp*b4w z>sWnP5v?20vP@;b^J=`%StZyWub~s1`D!!LM~Z(6_g^D71AB&+D5Urd@`BhoiwfC% z|GOO}EBXBb_r*rJ>I}!##sd>vbv#&Jc(CiS&0F0}1CG_Ep?jTDN{2@w#tLmCd=(OOyd7bF50mQT?!2<^cpmlPe*NYT*} z#>fCIf@OoQZc&Pn!3{q%22nHNe&)Dn3u$1A4ov=q=9`mDQQ^wvNCYu4J18Q>CdOi5 zZ>tx^)iA%M&{dB16@305B_ZOv`z)M|kwt?m_d@`i$F(-{ z?g%_ipm!Q7xcL_4f!|hu&5-a__9vu8}Y3Gznr-gt!|mNJ*<48wmackbLwr z?cAe(Xy?wSUkS-3+OhA!*!L@|Ec{uKvS8yxcwz13$F$&kwxHan8_H6Fcn(`|0}Db( z%-jU37gxFH5EBpQsK?hwXR~F}MqJ(JRSZUI&92-_!AKZxoRG^Wwtyl7Qqc9p^f16F z_UU*@Pn0CH3MIB^uXVpXLzJo6hb1^f>Lw7CV9sFx3%qmc9h%`)i)ZBCo?BCS4>Uev0_85((c zT)B?}68UvdH1~&eYU-36g z7ibaPjdNS-b?uV`m|AXa;TqdQd`Kt^^aT333rqR>P;Olo_UuumI1KxN!q!sQZer$f z*H~Qq*lWrdCT>mK=>z_YD`^w<+JCUtu;bs`I@Hk@d+U|g7vcHhS%pdUZ4aHXGV_o+ z1engG#X5~U+LOswtRfLM^6Y^Ml-7|{W16@NK4UHJK@wxoUKVH_Ze$bFsxc<5VTKYu?vJ+FWl zPLxDTc#p1ywfoAEBkPrrkGX7!5GBbXWt|eZ7nL42MTYmZ$VJRQi4y%XJ4nki5_8DtWye@*Jvo*an{8zBp;=O+?CL94aAsB~`H} zhL9z=UOvU@!@P3(9Di`c;Va5hegZMe8ypfRnx{htpmzgsFmA7d)&p_BSGDV`Yyg8= z94cM+i?u(U`yZSdKvmoafVToEE5>m@qdrLEH1U&cb;VY4#rBPL)xmy!FJRpVrY6)X z2A7(eyU;wKlSHU;ipV1jN|Om>&__vPJFe?b0W3Aw;-zK{%YTHV!F~E`f22aip?sQgZcO3}?Qkox?WVb<4<} zQZwbOD8B!)-DdYAF44x(q;{ zT7cYk1(@bUZNU32W|H~`-m~R#fBZT1JNjd}g@C^uIi>zUL|Hso=gz+g|C;yrk+-5h z>T>+`xl{B9R20lKe2MUEh4pJstNeS zPf@OeM7jQRoXT|u1Yr*jsRZKw-C1Xu%|Ye;)j2b(RnEfB@IW}#zw$Q1P<;!h1k2*S zev)U8vuXiJG#&LJxgs${s$QKzDf>O;{A>3Q6kh)u)@WJItk@`Ty$d*HGJMUE;a54VV47BPojqmwnBz~21e&`bMh^QsuyCe5pjfQKZ(oT z`R2@$gII(HO`M+`-@?Zl^2jNk(>cvBR*^fL06~go_0_12-~I*?8^p*#v30%G5<7^O zNPUh?NhI)rci<3~Dop*|beMjp_kiZ1)cQ2CVX&ru^lz@dfjKVcIAxxD?pJ0Nko)C* zisa};L_aMViK2?ZEhLc+x%N^yLUAp}Zgb0a_NGR&;vPR%ndGgU?rWK7=k{D>9$_Bg z3c%Mq=*KVeMtFMyao@g->d0n46R&s;AWGKKl9*{+xwqqWy03O{1b&IyVavZQ;A}7z zFO8p}5rLT6$6!qcqD!(6JQ)QZn&%K)ajyEFQUNiBwQW_;+N$o8P70nx<}5NCVI@gs z1$It@zv2z_2Tlrcsx^c9s{-NWEUw%Icy$qZPXO>ud&BbH;#h>d*E(8W-gk6RdH?3% zMdiH^C-=(YetG==EbpQDbydIeqOSYi*wBl}v<{h`@ELF#vy($mBKWdNTk@w!q!H2M z;@RJAUInafH?G=RayKNqW4iBB>FE-jLg7YzAZ8)XFQ6~-d5oW{e{(7N$mt7eY&fL8 zQP5!u8X#yJg8nYS80BZ(d=$Ufq-*E1cQaIh>UN@8a~KrCRg0V4p>f>#V@Z)rW54e` zcfU}3*XvZk^;#jyS^YQ(CC$n`pbNAjh8}Ynu6Kqmz)oz$_2vnt*Ae!zT+_SIX;#i= z-iH>QIq7`=hU)JVeO0y!C|i$4ZprHR@ANz7kI!qBpFbS^{*J9drnkOSm2O#kM&Hwp zJdimq^T0)AY*ZV{*nBL_BXp{Ct1v!{x8l<~gwLdN@X_zh^m{*Fq1(69IMjofvz|0}(9@=aTs-T@q?@-%% z1U*fk0G2&qd^s~sBt6X{=_w^W-&jb8p30H*6ftZf5c9?lLeEo=f}YW&=O@rvo}Sl0 zp+nE{iWjD5R&|G-+qPVUo<&%{!`_JOpL%P6<>V?Cre~%$nSG4}L3W1iJsY7pWIrsehLYZCe)nnM!1awtJhl``$X(^oe}G z419Lltw^W)^LR@`Zh9*icbuW>%Zp8@wb8THqokqQSOt_c!kf`}qxXqb9e7ZV?XW}) zN)VB(Xj~Xvf)!$^Io0yGM1Jsd1KYxZm+U0In5qbtz}FArMIi3pT(Pu~yq@{C<}oz@ zr_-f%Sq=*MGQjNVCNP|Be?Z@|T6zk&6yxWP^K2Ez%)jmDZ{+gRpZcI(GameN_$0e$^G%|H)>65SJQwnouEmnzz2 z#X@1@hauPFDA#R(cKPvohJ;xC<1S;`yaMmJ`lmX!gH`{$QFxK@`d!+2^!!OI|BZy^ z7oum4!Ye)72sy-3?$DEKYZQ9=oRA)Cqx5|85a^jedTs-q<>_e^LC?KsFH8^asI>I_ zRB#b`s+>>Hx;?=1NQDd2^IdAh{NN)blf9F2hn{y7Lg$AW-24y3+_qc#r;w@1?s#YO0;5=l>WLcU`m9eNJ*(jJ%o zd6AJBftY2xgq}-C&qUJm5$G&WPXH7;^nCfxh3R<-3nrDf>p!|k|BSP(N6<5E7qIM) z_=O9}TPKBAc}pkc0SoESQ$LcPT@0fL#C*hT$AReD%RtX0(sLAamZ#@KQ0UN8;Ue@D zyBvC^{QDyGtmt|E_#u|_$|7T|83WZmCiaCF$TFRaN@!A@N zp3V$*2*fm2din=I&lJ)#0Cbk`pGFb%+3RyBuv4<^|%stE-DzPpSK9IMGk zDtLQ)(CDqHi6gmRH#`CV;fR{Iv+i;rr5_f58G%+S?n5eeSLzRlFF50s$1`veo6nd0 z3i}cPY=O9EW+B=3ptgI0HDGnqvPxO>s~o_&gMajz!fVj5EIMxTpJEv-J~xk-ingRP ztufE;Atobo; zgQavIq%1j`dcU`06cVsxpY1ZI8DNjErho&GBjpMU%VennD1t)=o+m9A?xxSdaEv6Z z3k&xrcv@h2$Z!;_6+NB`TRk~j5sU8j{WGq$t8p$Mkze4!kcM zfs&?apmKJga*D6CMPXJGMv(>LUi{r+Y@Vn^bmz@Yb-m}k^(@v5u779TiXFFBcO&|! z3^}C_`L?)S|K3sdRa)Kw%#Ix}68gDPw`j;6>#Bz|U|cNRs5Q0EDXM_VI@}7kbp`ha zpe%~CNcsst-#=6MXqp0An7C>a!WH+>Pv^nsu3ykS5On9?hjM1eXAoJZy9&FayBK6Y z!5Z45>=|!IR{87b+YSJx2iv#9e?teTyX(xyZst8JG#eUPHN-F=w73G&2&rOJ*0P$v z>Ci?0Ejs}Of;{L8MVaBR2*@f^1=1LM=KU*t|D_a| zoi(H@d+>sy&_|#BxK2Md!N+bQWof7<4yC7CfQJQOK0-$${p;n%zxW-$PZrKPIji7K zp$mWfG~AOiK&o&pl^7D9Q+NpIdID3>(NOlwr=#ZsSN)09UeE_Izg3TNH3m_;JtC_R z*Cn2l`#pt+DtQ8xAIASFp5)zG*HS@q*@!j+yuuJ^sK&8Qm8l1P2-uW3nvx3%;17peXS8&qaX`e~yy_WY~BP9mLk) z0ks@B3FC6<$_o4YX9C-ju*V78FcQ|;M*IyggQbXLAcFr&5d>Yi$I&MD)JOB}kTm-? z1GP{O2fF(o;^e)xVwMXSmZaj%4!i;Wgdgj+@V~MayjX`9KvL8s{`P&$&Bgvb>A2uV z4?;N5K3(cJ)FqX11_Ff*UBn$yxBP{TPL!rzZ9+J@%Q$xEZ^&pVkc{=~4;MM>I)k~G zFv9`k%6(o4iF>-0g)249EF9CA=Nv@zr5_-{BK)QC27#D$p94%qGXUcWl<+<=!-}8yDQg? zCo-&V4^*ccAbr4+wH2~%p~!ZfXc^l8aO54h(|L++ko;5!hL>!a{2FVa9wqI%vD1W- z=PN>M!tWe>A-}J|>yp`g%0_YW>my)|6#Iq7h0L^IaEmD%vC+?5VSA*|jKgL$`{GeT z9ycoy_9J!HR+V}RM;B1wl?I^#9cA|#{pWVA+8ksE4~d2kLvEhKW)s^lAkr92DX4Ne z(#@jjpd5mL6cm_0urbay>!1*4$bsBZNBnoG8J@{<*t{w={p7KTk3IQFa`q7^vPIG! zVhd{$?Sn%s?tyrvgU~f#FhSo1C^1<9OuF+<_^ccF=dJ|(yaI0keQLbm);)~dP{EB7 z*W4wEw}AR%$UlX6Lzw`BlZZng?hU*XC@j)v8{LHX_OyOu z#@mVZ3$Hk*(xFWzINt%Bwezd3uy!!_;&RSb^j?~Cwu<-ioU;&g21;Y3_CQXq5=>0p z2k@aP9^&z!EJA3`1W=8wJ~!H{S;O}2<1C2=fZqh5k7GgP+}={>mPg(bH@Aqh_aE-{ zPXjwsumx;mGPOWl%ez9v4K!>2l5_VI8HYq^gYEa(eKytdr}nlSmW}M z(u(07XdJtmF!jtBGzM(#hiz%&1v>t|6~mZkK#sZBCqQsaOW;I{ zVz}%DH{q+hHt8!lTRsvoW2s1R=Nv9~<^jjX2=Bjr)lTODsCjJPOR!2`SxbE(YpK~> zOSy7)f@>#w$3o4&iYIdq0s^2TZX#NN=y^{;M_~IG@ehcY!Cex`fZ1Sukv$dk8U-Cp z(2fMXIReyKaW(rDeNWR!H=8{<^9Euc*)yj1Xq@t_Jgq;P{OlyIdhTX^XCy=*<{MlR z3k1$QiCSSPA^tm}R`Hvz4V8wN#Jq;mn34-K{82nSSb#3cg>B4b-OUvpYxq8_d`P>u zF>a3-nBng~9AK|EQ?PaR_K=$tsG|2u0m#{k+vDOYH5VYtx8K6G0-PMV3^v7+!~XK; z3uHdNp{?{o8fRE!5Z{i4N#k{>E@EPqF>-+!8i%cZ-*Xa+yu;o*=+++v`8M|-Vb14G zW}*CUG56tD^z2A(iSO}FM~WZsy^MaDKf{k!xK3#9?LrW&wxFcG zHzNQ5O^_&U4C7j3i2>277)oNm!f;6<=R3p>4w?n{rM{TOzk#Wa|27vM|El%^aV1QO zgu(}oNuzI|!nR+T6@Q`%ocspY@R76CX8DdZf4~GV8}NgC)(ZA;80;wv#>FCVLREbg zW_lQAmWAO-0>Efhi3-*&3^vGu9iaUAN>Npm!i-RqiB)S0QvjHa5FVmt*8dSKOWx!P z&;bD(nOqITO+cb6-V-zj``?^(xlNw4iJc3Mzlah5e`SmG^~0Ptx5BXuF3Vn@i`M0~soj~w`XPUf>5&uar(XgbH0!g0Y`uTT`0 z{sZo&`A*UzfjSwI^LGly>L()r6zDig&^_sKrJjP(@IVxtfPuJ^iWc}!zs4@?!onfK z@#lE!Yr%RRGMNfcbljP)g&xt$>BcqHEa* z6bRz-CabD$!;_7l$huZ1S6N$r{XW7UJYW&dzJVz7*aj#!jQAQJUs6#@H4ZIqzsbRS zTfSqsd`@A!S%hmz5VaSJ^T}BJ2jUi>Ms$rk{~gpPWrJfjN+5>%+fQ7h%?F|zwkRLp zAnQRnuipjDn(Y>l$J%gmJo<%KLo*LlRdo~S!L3(zd3y<+FEz6_A?N|}5{UEb2_e`Y z^lWX$o4p?}{;$*wlQ&ovF}*)*Jg&RN*h`QHtjS=&7Msjs>Pzor#wpdIqE6tIX*cWJ z;X-e#lc3kh#R7HO91{{QwtTd}*SnVY5-K&6Ic`=Y90O9l5z4MiI$faocOqs>(Rn$g|7Ds z5izLW7wLDq|L+GeF`EMLqE9|}p1*xKSYU=)c-UT@OoQ)bT7B(ZnN}-Lhc`Vg8bEFL zoa;?5!<)7R!F8x2!><<~I8m)OR2QqPK5Hv_U%~LJTW}gY!ErA<51mX^i2;zo)2BRh ziit5gHdEgw;d!%thO;L6@Cn@7<;KIZkHS^dyI>XDr-Z9m5K+b7e^EfuJMQ&WNbsYR zG{N!k$g?Qqv+}QZbIV_EwYFrix9TPd%hZ2Qe4Lwg)FxI?_)8Q%lHWNq!0)q&Igji; z`qOOwjbEofMd?_U3&AGqnF)MdV4_y0>^uS0jdf-y_z#GM3AGZ@%F`K;w0cuVU z>=9Hd-X_VL0cp%!o9lfO5f@zBx2080rwR0XENXj5j`gCs=Gxo`>!i7-r=q#Wa0-+x zGqv^0v-%~MU+UtEX%>Nq1X@hR1j0j>5bx{uQ4>mzkQN+l3E($Jp*GqOjmpr%8{kya zOx*u>83JF+O71aH`~Jqjn1_IL3_Yq7%!b`wgo;NZTg{l#0EoErbD^P3rT58;ygezd zRYf>uK6aO|?9|&rLsuU2noqDfpujN&9b?V|>^YGnJR2+v7DPDb!~Ne(Iw!I}>#;vu z|8mj(tfxhwvyQ)bZhsbiC&=BR$lXac$~~7q0ci(0r$1jI%botb0+n8@KV7*`2xH6j z>4zU{wC3ylr2X;ok4}HMa>bE&UY{&Mp>UtLavw%v^Yu^c4A)Zr5!oTHyb#$TOWq1~ z$kcbk9TE(%ik;sI*<%5NwNdX(M|up{;gBuTQyJ1@o>k~h3LQ^qN@4IjbP&X&huQyw zV6<#9p}h)SO`-dqbo%>&h`OEr9!}XnePKTs;CO1P&j20c>*brq3$4nFRxl#z`4$h2Z(nMAkKumL04iDpl(4 zA%N#Ib@$W9pszau^o@vO>n>QVQADx0<5ow9r)cgp!P6iZ{X9-p_GlbC*<`$5|3B2d z33OCN^FEvafv^sU!H5eHg9g_`7D2X*BruT#qpT_jf*@{)FacC#F$ppZgQ$r5x&h*X z`+^uyfq)QDK^8#}7gUxWM-UW4044wDslIpS-dPgf-+9mZ&c``qX725-eyX~GNkMMG{^40MLO=$UR z6x5T4F!}EIHL&^G-e08CTlHbvlhB7oKSk3A^h3BW7qJmepm-Cuyd1O-qqMz$ZL2WT z=^bI_=W$@>e5%JvFljavQ$4&?CD?~iwD+e#RP4hT3I%o^D)JsmL$_N1%l|3{;rf_j z`c4bJ3mJmrtXCL>1kfJ-1UQgUrX1`NRHGs8Kayi%9VJTOz^TxS3gZR1ZV4rUXSDmQfveEJ+)kqfF#pG zK0LiyxVcngJ9a;~Ns;~^m`!d>eRznvW9q{p5It>uczkDd`tV1u==$*5OICd-!o_z+ zr#$-I>h<9b7lo*ohaG_WmfAV6e~69;x0Qq6p-$K`{>LaE4kiJb&sI0SK>9HjGMvX zep0#u**l70iCotH#CmZJirF=bYj8q6w&Nd;l(<6>!TQca|xgPIlqqjE1rg z(ovLxci7w27?Xa7XoDB~89yfzc9lHNK?F131k2OTNR3 zRxJ570L2R+oHFqGMv+VB#Zp=4kAhsJ=l_*0+pAx;&@bcpQe4oNAxu+fQj_bD+TiQs z?IFJ0u(2|&8|0~Dob2tyyuY2FjM~-lecdB;wzSaQw&Gjg})dWCyVtzRl#{QS<_HgvsEqn$^i51Lg`c_y|3mAp^&+78&HJ z`CkZEZC{lNYc|5HmJ)DlawXN$iXQM-ow~M8t!_7}*MAJ!r^T816vP>C6Q_>%ht5YF zjMJXkafy9O(G-`wBITdUVagShY6q4dwvetCBpua0#Wqd_lV69kdRS}k6JnsoiCL+y zYHwwO)oasyQB2Le8r2E6*89>lA3VsWi9@80s1I-`mDx1?I8~0;$5F(!Vz3fw?v{{= za1?RrUAXBx;~%X9tEIB-rlNMWc-p=vc%Iw`S9pw+EE@w*a&t?`k;duylhS%4jMH2g z;;Nh2wYtQwQKDg--ow#arUYo5(w~Gr^rTU3zg{%$iiy=@*EKmSs7qbcH)T@7C|$x~TDT1A((foaT$f5vYDkyp$8vFp zY3bq%hQr#uBQiWFbx~jK_&TI7b)x9YNSOGlrFCX^;!EnxuIUj{rE)MRPvw}P$s7IR z>+;k%5g(%ix%_NCAEKXBf#X_Dgh*JA@)J)5s8@WObjObd-8QVBTnk;Z z2TDer{de`#epA);vyMjzRVf%Z*Sq+VmZ7VSte*tmqZ1wZ|=TbjY=ZhSN4KeGd7UUbFeilDwsUJOl*wIv~2K&Gn(JlNXzV2ot zFj+DXil&z8_b}51_0cY-;u(f`ZPaL8dJSFrCDe{?ti|b=mA5Z^?n^wY0=I?7X)*lj z_y$ZBTsRES5uMa0uiaOkY@{ z!Ww`382AsZ7W|LADENm^{!Zer9fAKFJc)sS+3O+vU83S2V8Q?9O(FcP!tk%eZvUwG z$77L??&j_Cze3|*jVs3BB2J_HuOB(a ziU|l)7#oh4YZf8rBYv8 zpBH|+u)eyJ-~Nz;o!?YQAVz)VSorlp;*tCwu<(1*s8RBp8pf|93cu3c*Eu5Vi})w6 z68vXq{6kn@S;RjNeA)5e4vu2r_q-aauZ=(uiT@bAbt9P9$%6lOY+&Vca~9?LJ~8U6 zwt-*oFOqo3I5*-UhrSGthv@p+fuo6{>F4Z~!f(sxq`tny5opV^$?vO>gPmVDBoKq& zn_da=`vVY1^81?6rfA=K85+#`ntbQHY{54|AdiXRQoosL3I4RKNI=? z@{Dfp^mARrKN5V|<=-|o{*{;R|Aw}^#*2Nt%5>p$E3>`%4+ zYy4+X{yC!m;LDER4US^S|Jj8h`Tqa}k@&B+^zXe44Ws?2iS1lW&9g&t*6GQ(g@G z^Xf*&-O{M*LH?>DrA_1sfs_ z`ziYKQm+{J`{T5SaQV~zG}8DNJ}LN1DgS)Y{|Nj?@gxTRju!dn8NAr#|FO0Ig^eL* z%714V{-3Ukfq!1@==ghMkpT7o3Bms><$r_dKlrlCzin*%4_M^yGkPjJ{xV}sA^Hy+ zW8zN^!`~tn{v8RC_-TJ;FV*_5@h`X$_-_>b2VZvlZg3Ps{;iD>8}(PhwW7ez~4{wAAH&6-!?XWzhPa7-)Hb* z$3MXEc_@D`Y>bJ&*Jiu?Tg1Y@qedit@qa$l`mgaHru_Sh{(~<&em6LZA^&9-`EN9M zvE#qm(*J&mHfrL(F%18Z9x>$KEC&8fA87rbE%HBu@*g1j55Da9mw=-f_>WuU-!Llv zp2nC(^xuO2iLdSQe@cmn*lMu{(3_2+F^ z5K7lB|BDTsC;rA0|E=LOQ2rOh!oTp^*!cV7bct~Ji~pnXFPtg(+fx3wivCC7KZ++Y z@IPz8|28a0B>s;r{jVby{$HH2@z29K8qx6gen;!SNAUNj{D+ACgD<=M+s4Mf%dn1= z|H15N_{$8RpYp%{%NY98vI_h={)xmd{?FT5|26&#t^@vI#Q!Szvg3DyqZsnvV8Ndk zRsOFTT1EV;H$>u>)kniJW6Qr;4E&qk()vF` zJq?>r{54|XU)U`+{{APU%fFGvzwlAPKZNqXo%k<`z<(4^V&Km)yaVdbMi{|J{hwgL zf9?7h^8cl4Z2a?ZC~~;|Q-6BDsr7%l;GahMk0Ab$;L9%mwz2Vh4DXEi8%D)H!0>q} z|3}JV;BQ$4{vF37@r(cShSqqz_?VFV-bUu@J0@qe;562Gi= z`=wKC{PT`Q$KU&Pt^b9B|Ib|Dzf1HVeA(sSHa32b(K``;!>IVnvNc!4e-isC$zh7c ze-r#ItH8gbJUaf_uW9|)_`jz7?ru-MJjv@b+(eWd$8>e>s zrSV(l%L->=zHG&N(dWxvHPVN#2e@^S@cZW!;rBTw_#H!j$3qTwezPC})YmOh#%py= zer=nx3JuQ5F{66w5!;_{F)bTP+mW?&@fj6&$ba|1+|_Xm4M9S@2(_@qb2r zyPx=%f-gJ%B5)J~e~!_c<=gOIXjm@dPpKAvLHjE4UlarXj|&C=*vTUQk0}4K#9t1+ z?D#(iM=|hw3~$1YzmEm~(3RE6|D$$Q;vb2#Yohr}=WF~QJS_OXrToVcfBOjhwPNGn zV0d$O{B4Y$oARH(qB{J4x2+QYtA``;(_Y=WKdmFT4D+z)=kON5|jP z(nrs#7Js{{@$dg5I{p{uYyH>wk5T>wqW|E_j=u;T#lT-Ts{AiBJXgy9tIw*Fe?gmA z@@FGg7a901_Ugh%x&P&@==Q4oP$a)>@9(@M{9gNz)K^v)@H?LTu7e!x{LY00V(@#D zVSVlTwhi4Kdm8fF@6#&y&7{35zA6^K2>w9-W8gpZqTqkzLBT(=JMd2+{@M}vzrm9j z_@6a;KRf<$a92(IPkmAy{d}rrH8y^~(UaTpuYt#B z;(vQ-b@-F2#=i~+&O~ePlb;p&pP}(*Q2vvNe;)X<Hf9z0&{`aE%r;7eZ;NOcU{}uiZE&X-#YVmKsq)PnLan41!{t$mxjeqM{ z!GAC1KTY%>^M*KwmKvFqD7^mrz}dq1dx-}tL?{2}*6Bfve#N5Ki z<0Ai8F0K-Pf1GO(P5zBE{)P7m{@*G8V&cCj0{>AwiGhEa;R)LDUx*QsDgQ<9Rfm7y zMOEUThihTN@w2`6ep2f{wzok44^#frMgPHKq`rKIihZ;A+qDgUqEtxom^;{-o}^%Q zLSW`zA#=~wA_yzA=jwm5|9hrvRI}~>e#W{9GV1>CHF|ej)cxNZs@ufXVmzu0`@bQ) zX#2kf5chvSCUWU~x5(xEOCT4^{%`%Vg?<^&mwNv!+M z9kInS1N+gd-jwDj5PsfyGq}Yv@3qiwFxmfo6ZUq6@Bc*krqNVub%_jDNw4V^87^PV1hI(Xhw<9U(i zJ>?EIkCsS*=ClS7<7kPd_fi(Y^Pb#35m}-}+Us5OxQ1Eaz!aHzT*E-B5Vo+U4@BjJ zo%a;npWo;EU`y?!nrZ59j1mbw)edr)#|pWE)QvN%%?dezXI1Pk=doM1{rSxwFskAG zS;9iLF682i&1!g7J+0qds^2u^8>xnO0n032s==~9f6ZKTe|qHp`~hpL)9j}%gf;Xf zwLnK^>`foH2%23kCmXDjL+Y?U|BwC=+B|7vcG z^RI5UD(H-nK?NOq7nw26!tt@mZ=NY^-;Ol!K6(NUxoC4msS5Y^*2Tc@+nsV%QK??q#myS zcvgk}M?GJm*?3(|PfIn)`4TmsF!eY3`3g_+kg_Vb3~xyh9qxzT#?aw+7g%&SPi1h4iNH_2MG){4Yzk52I!~&yp?a6^j=N--`sT=YKWVE|+os z*P|lQ#Nkqxzg=L~>Rf!VWqpUr`;NRE?)_VZ?bD^(-YvI%~7^X*LngT2ZwnUZqQ1 zi4u+TFy_Ayk^r5xlzb?JoQH8yv1rrsp;FV&w=lJ->-11fi#CPM!}#@jWR5%!Bh>!C z32GABf01tgn*M}~K>sCa|0vhg)?oWbng5gaKP!6sZypCMGBPp`g2efMZC?+ye_8EV zgw_^1|F8TX+<<^KpN~WR>YW$0{Ug2E{-?}0+W#N%EJ|nFc5bw6=UK8_TT$7+W*#It)czvG8Ww`WxRYT%9tqy0-;W@NoWEbag7Xbrh9(wptS;(4?E{~OA-Eje1Y^DNo!dLdT(*Xtk8u(kgwy8Yjx z+yB{Sjm?6jO^JrL|Labp{cj4^8rr`uW6{l0#t9C?UY7RHGQ!(Gp8XHo|MY6wKh7ts z+8J)E+uH-u`^kA&3+IKjKT7+T602oNx8JfAN~{B{w*a(Y5(uVX#drg zIQFEJc;<7V_Wx8!0@D6n!S?^KZvO*y`#;OnrmmAiH67mmdq!>ldOo1`S3xyuuQp5) z;)6Bu3_2IDQ;mK`q4s#_yDaW>&r@GLO#AWZlM`w;(XG!O4_$>8W41|mQ~jOs&=;OX zCGb4;ZWa=28b>4Xu!Tg1g~XfB7;V*vhh{rYFCKdEi=bef>b-|VFj?4E46A!06@pnz z!K{bC!rRwe2rFv)`ahqC`AC82vo#(%r7#pXjD8;Gzi;bC5G@{h1XuD@-6$QmiBoCi8aoN21+lMtpUG*MMcJoL3kLVQIw zvDU&;KX|p2uh| zcwVfWl4u^!I&7?Q{fnbGKOTbOY4Ru3ps68Mauod&Wt{lk8@h??i84;y1Z!S(6Zw6x zsMOlIXd+%x;JJJY`NhQb#r;8CB~pao`g}?VSEP#f5KNIO-nCjh=W0CnJ{eT;n;)~Q z_;*gI_%s~1r}m7O`f}eW_0_+L*)%IAgz8J0W-q4xK&S4ZQ>QVtwDR+ell%1iGRKqY zU{ig#Rx|_i2aop*kFVaLNu9hw3LcmYQg2gHrdSF#6{Q!I%8akB08!DJ)0scGZJ5|6 zm#QcbK47S zH`H{j`GcmP2U~6@wZknEx=T}E%+cvPtdK(})$~ry3R#X4tB9XQ%pZI=)ToC0bs-75 zkOPg(YIs%+&~F0Rugbs`-$*swXDME)!7_hPILRC?M9v?ag&v~nX4|hmTBtASr>BFl z*EX}zOv2_5-t7tj#+X0Yw%XM1_p`$F`_XYC&T}+BpRz-Im*RX0e41)yit}z(tf}8W zf@m!Ljyiwv@DOdhEb|9sH=BSL9eeHP&uuR^#gshsm~7qs$)+)2YkzB=yb)hB}1q&sgxFW&WV8PQ6~Iu0J=Z z{}$Ro{iixn|2w1lV$B~kEw$?Z{et{1P5oeVV0lmUAEid=>-8$ufVi z<^fZGqt74oK^IW94nLkKI-G>A+n7JN&tuWyJQb(8nu~2*sE_gW&H6~Ta3%F2^9NhU zhzv&R)GnqDn?IP}3G$3Jf3Rj{P@WysbWN+drnTq{(E5<_d=ZGk<#`v-{4e4MV<(0L zYMVa@a8Mr>KRAm1(TpERv@WZ*HGZ&Eg#rOzP|OkY2WKL^>5FxnWyBBOJs8S{>lC77 z`vFogvy~tlzsHClF05gVAGEzsYG?g*QkQSkHS01Lk~SqObs3sJIMNZ-Rn7cCLtN?= zT<@`Yw3KnXE@LxCl}lJ_3T2z^*ECy8P;ONJ?f*1?uyj1Vq1fkX_rS7+svE;mzl@&G zeS&_E#1Do|5BWWj@q;WWv8wpN>iN3OMw>ra`f7D}-1`Mj>dc7v!50Npd49Q4YNY3v zRc(n^QO|uz!%(6TKO9;dk^tied#;8;$o#?nd!(ixbcr?%s$*)?se3~;E!q^CKX{p$ zBj*n|A0>COD6&V{$T&Eapf-*jyRFeS???=UuSdX%&BTV9!bM6GS_uM7-Qo9-654C^U!$*OJ2 zh4QktvOsI~vQB!nPI@+z(p;)fEJ@AA@+ibAT}B7FTmDw(kl;0LT7G>>#sH<7g1bE- zzFgjpu!*wMyd+%TIp|@GQrC>2?yYf^)j<|;X53CO9FO(wmkzoy1s6EW>Y(w)1)B1v zf)V@V4vCBL5a;Vx%2j^!I^_y(xV`zrLU2ue#y{}Wt1djhhC)&*Qa=cqe$w>m$W~d9>##0 z5XVp#Y6BHN2&REo6=JMQYM#|MCjCY#qle2u{;@500ljHS3)TAk!; zPYXZoE`~t;2b4%lSW+TY>GEuriy<4%x`tKwGmTifMY?j6V`*jI)QRRra-b_0$^F#< zepi|H41GzPvx0uta!3W0QHd)&*5O{DbM6$CeHRJP$^VI~eDysrZSD%Gtmzn4;u$H* z<&9&3P%9;_>Cl5W;x9=7gX-^(1-@s8zKf;ya8tSJLbV#>I`Kg5X{Q(|!13Q$ZL`X; zbd|n>6a1%aP6qG)43!Uo zt9i&;ys89Ka&gOevYR7oea5PeM^LyfH3(nsiwND(eT62I=h>uh=4P9kN_hK{*;C`z z*5hjVfc(&?u)2t^qa{TUuJ+9_7A6swoj$j!;eA`=Od}hh?0aH7c^22BJy>9hFmO z*?z5LY0NC?%yLtdEUl;oXy*-qR4P?20++TNk`2Fw)(=QEHC(C6P_YnZdFRw|oOj;DQ~x4LsJ<8>cy85M zAMJyz?=$N!AZg;+f>L-%_dCc+C_R2SpuM5A(CtTB?xuauM-KJU5^dk1h40}vVj~6Z zgO=Szvyh3$aro>^^Xe>|(jd-#Z5H(uH=E;r*`n0>c(1#|E3&;k6KTmh)2O86;#(%P z((@gAQFs3=wDZoq5;!wZFQYur!?}zk%{%#cV>c`folj=r#p~CCGHwK>#yfNesqQN~ zjQfi9YP#cU%QtAhpz7jHZf(JeC>n41vFI7&KV#Sre)ysAnuXh+XvWZ4FPTW3?^(8>K(5nNfyGR|M>Bc8pg%q12Uwng7 zhl=3X)g8_{2}J=jeL)G0E$%k7(_V!q8@uxhPu4EzLz~cf_W@{0AV=w^yZgzME4!^muxi@FN4pBKwj z#>;PIS+7A3Ix=oH5+`iovC{Qyf{e)95MElN3i^zwWRJ1SOC(nmr=UoxblVQ)O zbJD)LF)jS3Q2R=16XgB>>-P1_KWKjT_Vve+W0v-H{}xO8`UWRyL~CCm`^|Qt@{kN1 z2TcU+^(wre2ukF0u4!uOT@@5Xeozgd4sErmnUo4!BQi3ZL>6CUl;`0VgFCX}@xa(* zrDjQkrLl2|<#1MRWxHirY++fnIIch!oo!4oTiakZ>Y3*|Bil41TU)ui+%!_Tp4)?( zW*DbePDTiC>uRfUYPEwwy>hHyFp<^?Sgfx|F;J!11r@_XQ0O@(T3jm8&*GWzjNC4b65KRkLxGl zL{P|P=W)g#R^^_%DOUYl6RKLXelC`*tRLSsmik%s_UYEo((O_|m5n3o2cy+KC1Na_ zfSk4Mu#=htg>-ov{&)rJLcH~m$u!%F?tZVLVV*Dg>y&O7DsFVcTE(Rs&SRr6w7Yhw z3x0;h*m~bAd1C8*o2;@bc;Tf-#8Qe;T#9om44cT_H{`yD^#@lVo%9D^=Xutd{Xx7i zw}<72m{T=LU-t*yBKw2OLjA!B`a0r03<`Dyo{Bse^Z8dF(Oto#gQP1sr-O6_f1OGM zv&Wae02bBrgTuRmyKdE8!NN;4gSsnVx2V4EWa|o6y$No@x&q66QBJ< zD+^S>JjPqO{M*TG$^JpE^WxDuP?F8rAo+uuH{5X|` z;!W?|bTS$PKwzl4Y1m0U@Hp26Gd`xQN3F&{=<-}$7RN7o$k0)3y-6f6Qd7R0l)oh9 z-%w&W<)tW%?#qk7EKyJyZjni|K9;;5tdGS$TYWruJfc48Y%}ZQvxY`}WN9=z+X|n7 zF+hX8Q*mk#&}dicdtgU>oTC%Is}rtfLTqk~Q*D9?=OZEOBc39PtPee2uNA_3e3lLG z7ZnkB2S6FM{xmf4&K)SyOVco#62=IQn%)9Djn~khyLG}!Y_r60@3;Gq@J=Qy3??-3 z-ax^}!rK=oVHo;e_gxI&;l>*J-aHcTYgm0~_$Q%K20F|P?{hIPBUiH8r^=W^RQ~p&_U1yF4`%M`l`vzh$Bcg0? zh=@L}leW@H{r4hiT_#;@Ny?xyb+Le2AbvmmF7=9G0W)u1W40eRp0R#`_;QSXC5U#z zKv!Yunb4Zm8Hk20t6(+U^8^Tin>H>_9q;&oh~d`LFkVC&Bz=J}jxuG-Fc?7mQ$JyD zoK8BHNl!58F_5q_N5pH8UFyPW&CL2(w@A>!Q*Ny9`%2`;bjMS;fE|r7E@=L~GNR?_ ztfVmj%NqVC_VcWB*ah{ zix6@Vt7*OsK^=^zFmS`b?kZfd?!Mf!7UNe&FS`uZlV5vwta~5_vnot}J3t1p`MqRo75wH63gdUyg(knN&kyn20Wd=Ry4qOy?F1&O z;Ma(seuRTB#5`?+^xo-QFK$^TxN(;BAWu6pKo2^LQGR+^Caz*ZZJ~`zC#l=_cq6+n zX)N+OOZp;-zx1m8m1g@l-zR+ka-v1Mg7MpSX896+_9Yc9;BdTD{dkz{f zZ(wn!1B~m{5lv+>yq33(^4us@S<*?W^2AA0 zD9*H1IQf4-*{_z9d&u5ba|~<-d>h>LqvHkBFnohvb<&1@(g>F zNrG{@)Qp49@>TJUqG@=B$hqDQtlDg&pw?v^q~_ixA)xONFXDg+53t|Kq2_;x03A$= z7el~5)h`33sbw)rSy+a!+dYgp(w>-|4fgTeixmro8eTO94U2Y3bvOqt5#H6|HGfok z6tpojWLiSwYewo+-b+$3h<}u?sp-xUxI1#t!4_@(BY0K3I}5X)=vx7Is)pMpM`P>? zjGTAoRL(EuYvK#A-+NSiT%az(cW0?RcK{KtNk5`qZSF^$_h)r05mf+DM4Z=b5oY^; z(bE3cUWWFcjFO!tlkr#Wyj-{cMri-=x$k_7<0BWOWY_?c7C&|S8)=a9B|QLI&Jxa{ z`QN_+_P~rUr$A{{H=T^pAa6qR6vM_Ar6%Jgr9KMT80;LxPd>=!mn@I7adz=4_yvjH z%yS)CJBxR^J42EXqsv>D(bQ3l_Y6{pZ_!hIP?IEE`ASmWf_<)X+E@B;{ch0oQ!fKbrDie`;~v)lftnKOy|_n za63PG&c>~Gh4-8hSj-ZZ$ zMeTVBHKso6Wy)o!7gG;prRte@>_V&%ZI6nPD}9kK>AmgfF6auf_H)%89nou`9x`1X z9U^fwhL_=JjG077>shH&!KqX9_BbyBZ48gzdvSg=1Te3HqB~$v2CdOI- zU(zb5OR(&G=dwg(P;0Fu_Si`P!Dc>bZOr|P3!8eKrOZTdXD_pY8BnIeCkq=x_os3P zOaVNe{6HOxa${WL`=kmG|Ro4MXP*|B*(1Nw!+~sYA2@9-ef!fnL7(`u_dvqsl z7Gg0NhW%-$OQPHCOUedjtNj_++Gc-pUa!Lb81(j`Qw;hue`9Bnm0qHq-c6@L?2v%5O!gjo;R%L9f_K@2N0)uiNQ$JPms7tn{+O z=nb^f%Q+2t`{r8s{TusXt^6k1>D_c1^q#WPdnJrs$%Qug-FX`HvaR&`h0z;jr}w~V z(EIyI3%?0r^pfrLJf}hL6)U}uurtUizx6F`{62dc^!i!pjR>PR(N1siY0yis(n|`X zmu9E8>@?_o^n^uzYg6p}?!3Up?}pQ$H^NGbm4t9F})1bHZ zaSOlOv1Qa+zbeJX@A1>1S74=A97gXMJH5JZoQD0g(rXt+&t<1~_G!@DKF7lEzVq$; zRKxB1ef%Klcym#P zj3NDJU3$z63+wr@4)i`R(0MSj%f(7YE=>Fa!^FRu1VeG5g^3yEaNFNMmn%eMN-|Fd z?iuAA2c~6g$k@t==(O{=@&o&QFfvGUc@x#1&*^jbO6=rJE;ySjb{>0~xDeLnViCC4 zh##1XiI$K!d}>*Wc;GQeQ-W$8#HF>ymw+7SzcK+A#v1bJac@vQ=U|aSquy2A`LBI6#D7$L?QHbxgwdZ+ z9L|3j{brMc^gsDGntZq8lph%n7z;%Ab&yq7O)=xRYT9wms}&?Kl*RBlevyr}ED7`P~2W-JL?9H{*mWk)u+TY^Ws<016 z{Tt+bNsoh;v*db^@!xP>Fuu~SPe}(3`F@-wY@-Qfa{vdh2_H5JhFQ{)1Q8RZvKjd* z!+820E}`Loo1Ywc4+UWiIG>m0Y7+SR&RGI!#i?=i{FPW?;wWCgU)$q3Av7p;BG=rE zl9?KQyNBOWIAEQ~k@*<5NB45c-@O+`pKH|j$XLAFF`J7dvpkar=)?Xe#BEXzAZ{b;)edw)+~(l2 zx}dpyD*j;VWRXrfOeg(&Ka$>tH|kSM(ueSXhGIrn#Ov!|<&3*1<6y>aj7Tpk@d_HB7)ci)q`<@*Ga^e zl&UNUh8nSn)>aaS?IeI>51#xeR7cQ;PJOi(io$@mda@IAYPZ%YbHaWIlq%z(za<)r>aO#}>wI&W zPlBhnFrT4|rh;9JeEwHps00cD6m$KE2%&Ida-6#$*9#Q^9G7=)>NEIs3|PfFnU7Tb zwCRdZi(Hl7L!0I(dXJ=}EkdZe@$^k~OCzkzPiDXy0vZZ&xa*4cG9ZdC1%nMVmF|fQ zsh8Do6vYFh$ftsf(xi??AqsiWeG9TX9dlt*(B{!*4f0qRp{XMrgn5^&#EQ=G1y?e| zB|3Lz8s8KxmNjmE#<(~8pT`eK@q1lX!87>o!6V%X+2T=-8Kyal8jotM-pN9O|3y6N zl}C;Kg7K)!o7wI4`es_$jCgp)!Yb^wY5%SC7GQ&|&HmfzEvt&2VgC(szNCJjW!nF< zX#e%@rp8p|J=0Bb$!-E#|Bn)b)}PIm*8jI|dWsR&ejqd<&NI3(>Z`s-74AU``9o5n zaZ)cGMSJj*x;aK3&rKbLPeV7SBCkYM`DZBAZK%59F6m}(MPN|Z|HNc<$$!vUqOcUC zr;Tbp=2#^SXLn+e(6_*+K$FdljF+Fi6{Hv1(fz zOPMsymJBFU@d?V@iBG93m*+QH?6Ls1wqgHOoWD{Q8qsd=QvLB$S0EOpS7IgsCR+3d zCi*)}p~xC<>Rf2z%kUvh{YRo;Jaz&0*V^>IsjWTvU#h-6C0OY#$Bco5-JK<^z>Z2Y z=+)$W6~fS=)m-NM)Ve|q$Cor&W*gFx&EMoqv>&dvoC8MG%Q{IeClt9V{h@?l%YhYQ zH$O^r1z(0Dw@gn-`!VLA;PB+8-lOhu>NzOcMFvL(9TWT@&OWw$`f#OqJ4iPMBPZpI z$0|GM<-JA&z+?v6kU4r8=ip*RN6}Ukp39z6>(@X%wn=KBhbIG7{gMNfUD#z5V4%Y` zcvp9sF2As>C^a9C`{Z2}7wCX}kKp(<{FQHLL6h*Eo7M)p(4R2ja$MO3cW8s^_!$W} zif+b3ZgkkpPju!N5U($3>Bm6_CD*6RV6Nd;S{#-6AVvL);qCa6dc9Yd-R-ek4S$6c zF0fKqZ>JFE>f{cGtcf1}XVQS4@Gc;_bk)sD;jo=Tgr}2q_a{c-Z1ba3%N7;}s`GkS z+HL8U44Dv;o#S*qK@&}(_>4XqZW7wu8VoYUJ^H#5rV*7!F7_da?<^Sy&}w26v=8sB zRO%gdz>-HAAeY|R9_a$vQ zmoTnHHvbW1ll}$kkx{UkrW0`v+nX?`MzDEg5+>URGRc>r<`-!nw6c7F&wP1-K4@e| z8G&%f(AO9;^yMrr+7=tZK&so;{#c(4=kB;#e-gVXF=jZ-f-oP2p5wUM#vyTGMg6gZ zxQ=$dyn<=vC)PQqk*aL$#LU@oHS$L7DwRkwZjV7xk_L^gVF|{8R3HpjX#8{Xbz-kk z9H-Ux=gNW`)di)pZH(tTY-YxR45WXN@p!m?eAIaCFdp~OkDK(>sVgv=C6Vq%>OT7M z&&K06`tj>VkzW{(FVv5_8MHq%9yisGw-`j8Hy+p2kFyLS#m3{mPy@jIl#zP0@tEsF z@VJ$cdVukGn|}P6@i^0XyhcA>JWS(mZ9M)^KYqY?e1`G(W&L=(@%SW$vSj{A{rJ!z zO=K?~O9zB^#cKh+1Sdho1q$Su|0o{7X1&diRe_0z3wK7*qfE_DqQt1)Gu*-9hc9YIwp_}Yt*E%%eO>t8TTbU z_JPr>)3o)t#L_v>K?#=58HQ;ppTLV6gOW1dH1c6b7qiw6W$lq-$=0NIV@H$PG=zN( zGoIA@TF4T5oTl1aGP!J-Iyjp2NJBBBmm0-T=kryo=0Six#@I3pMTUG$bAr`w{qrWQ`4>fH>|y)wu!3O?E{eJ+#~aVC_WV4~ zWDnn(i??X`FcA>3tX`;veL)zBa;F!{DhD}<_hH4SB{+t!hJRCFlqd7o_p}V!+lz(2 zdezz6_C|JJQabWFOW5`Lr`0#&InKVa3jHt?S~buqX>>gsVR!fbW3Y#{0eJ61FVCvr zKYpif@gKjlM);3T|KDwBaM!tf7+&%bmaPq|gWbXKypO0+<*=`njv2R5<-OWJ&Gp`v znCn?21hc#yp)u1j>)k2J1)V_)WC3Ha>5 zPnNU?pURPd4VF-&iy@8)n3TYhD`&}Fe6zMLHrjd@rB1~US(}lCerl4n76Luxxjzj? zOYJzcSD$f!{k$i*I>FcQV$#F@;%^X0;QA+Z29|G_D@)Bym_Kq!2Uy*YlXuk(AYxXL zy&|4YU}Suax(_L^pvO^!*;TMTh&r!jV0xK@YcodfC@NqAR98N#$&j^pyAYko*b5=N zC6(jy_=BFJ<3kt(U(%wtwJpid@hF>-jr%DE`>$G#S zo(Hw*s)g!8?~7>~(Wu7>Vq6^>NetZB%3+d*PY{zd?Q@z18FuMQjO^w~@1O5ySP)j7 z`U1X+48^dF3oE8zL*`cmZHVxj`1@W70wc9($xwjVeGgkZv;j|cRVKZ#x?Y?__MD={ z?4tR@Zq{mZzd{eY_{D^C%E6=rDD1jcW?7nmW1Wl?9hMR`iaqOiD-DrF)VS&^f%+ z1=ADFCp2~WYIR1JKFV2m;1re~$7gvu!NT+F?CI5z7QV$1{)UIs`{GC_p!!dc|*Jf ztILMhg+0U~7H8#lZ{uZ@V~(dH2bTE|9>P8rmlrh7DJ+9~ksY7g@oxm(#y%~}<1PRJ z<@s%IrQ_qQ)b_37(6qY5WkTDEn~1I@UB{&qG-l3{Li|<#)}#Sh10%-1F3nMNJJX?u zY=J5|4~cO*5YrV8WxDQ(V$R7#S@*{H3mbus)8)ZPVi)#0Ah`IaTE2}-WV@$VChb{S zFU}k1>D~dm;M0L26`atxbjPP^%0(Q$hRd+l0W7abN9}T9|XI)?%Kh;uR8@dMwT+7yd<`Ga`KJ#<{yKaa09|ChzGdAwts?9%D7z0p!H?y)=ns(J zj|CT~tfULlQD=DTK28&nm{b4C(v;nc#?ncsKTp_DWHs zaVR0aq`Q3W_P{+5=WQa>eMH%LdtrzLcTVEzFY}2nX_+L$%nD=UYBnBXC=4?<6+da_ zCXI!)lEE&Gmtp4y%K;8$-BVD9ae>WNlgIc?3ZAN8?>Efcmn!j6I6IKoRpRj^^+1u< zdOaQoBzS!m=fE;`b$Ou5W0C_|SXZ5i$%_r@D257ZNK0aFm;eH(t6q1T(wqP(+ohS! zT7@*vI|vcV3T!Thgzs1aY`Fj10Ow1%XO4Y8gc}UGH=4ae;OtlF32b3+Eu*O zQN(ePK5C=zfyM~dF60Ye(l5=?gJBf9sxG=AFxCZp=lLe5=XiUgW|Dfqvv8KMA5`bZ z@2|`$>yeIpj8}S&VDT=^>Bw_*$VaS!FKO{Bn&5qQf@vVgvBTe3pMWEfNN^)$34&?i z1d|Mc#^`iq1w(wt)%ga&Gjn|XGwFnG!~>d$rJ9q(<2qe=qHy9Uk^>3hhY4ajW>S)~ za5C60E|yGvVUpp@LDfoCSHy52fV(-fk;Z@-UZOHD_QD}#F|=){^&D>_^!ndM60?%Vqr@S6x7N`fj*{cxK7 zIh9GB7%hs912l5DF_poXuMZpAq65NSXJgt55nje__SGBVGvs-`LRels_XwTi-iV0f zOsf~!gTgy!!^*`5h;$xJ&_Tqe{=~V!Oo*Ka=#A9NH^Jrn@mDyM-u}sau8!a+S+3;o zoxGe@8@$nV`;vnRh`J+4tFF8ER(5F+pLo8bxHW$>zdsXTSQsdCo3=OMnc1v$?W8-> z5+1-$x=V9F6hCh^eooatJIPOsmGP>D@qH-1*EGK8zpBNXPG`Y-?#`SCSlx7oi>opgRHw7Fa2 z-L;B$KXAm`BNOw+jz_7A%WfmWmrFmxN8GrH(|(12F$GGK>-i01`O2i5P$E|151!h& z#T5ku;Bj<<#}PkuQm6Q_1!wT9w|6FbmxhJMYr7Kz?F%Po)^?{mRwNddZZ14tcS0*B znS>7WN}^K zqEhgr%(5P0Q~iI#4Uz;{C^EHUoO@`-D!4RRkj5l*!7g7W`CawE5uU_3rEpc{%roP0 zs8);2@b^~giWNdIw z?VO5XAO5*hGq#2p8ac-~brL3RUA`L$VCw^ir~N_%Q@h2vn`dkQzf&hU_&SHLH#~4; z+RuC)zdHU4*YNM?q+ox6te9wvnpKs&#DU6*WfIUh>;&qc2(!VnbM|tZvd1sVzEkbc zzp&_@@7Cj&{}o6mV|U@?6gW1MkzHMTuC#fjR~TG?fa~(dS;);;mf`K+0nHx2;{4Z1 zQTL>d(%SSqoCZi?xm4BQuzd) z8JbwcF;QVff}@zZrJ*AhH4hHvR-L%G5{XqYmo)eqa-9PVR--r=tkp;#7>*YXW=?pVLWqb9;7smeJ6X)bw0-QSfjX-lHVoGUUM2 zipN0nX!95h_TSi4o$KuZtNN$fCP}y& z4OuW|egx+9N}0iTUt>Ir>v5n`?bgpO;xqqH99yDxoh`DNqE{LW!)X=E4?&FnCD2Iq zseU<3zs%;#6MXqJU%sSYX6u(}e7S)y9~51Nu|72MSf&3x`6UMg(vQ6E4j>z$bJo{6 z5B-ar2bePxX@h+eznJ@>Z^x)uPxzttK^N?hz%9C=f==GaDS`H2Cpn{XImdrSN3rlQ zJBk7vdSLHBX5u*x7GIU=c&beAWwXwB81c8i@30ql7E8L+RX7=r@wqNv?_|{!<}c8M z#MO*n>DTs1M%k#-BZ^VwgopZeKjd!V4OBp^|6NEf17d6U2ri~`bvs-;yLp3gt`Dfm!&{jl%D7#q}Ld~>oy8*pITFb-*Kw8 zCXTx_o}XbKKj`=S;kdK)`4ZlW`hH#T{ijjizZ!gh+01D4rw8A6_S)au{L@jydb3WO z&7HV#F_*xM%i!fre980RpdTCpBU60XFv*Z(1u{`r&VY{OdVb9--dlhhDB!<{>aNN4 zz9owmay|dzlwSR+j=Vzm{b}kR!XsF0hH z?VePZqi8#nl%8MFSNM|a`8$udVywq^W~KR`kME$aKz#)b{j|zT%tuW*f5n^n2gce{ znym225AwAqx6|`4B@stL9zT0-u=$-SS{$921ATVmEsLYQz;8BSM5tZj z;T%@|jkjCzmMwqen^=hhMLwKk3lv@mjH<&CJc~p5(rd; zYajAeKLCUbi&r@vPpzejvox=OgIbYRFZ! zCKB`1wa|G&*WP9r2)(%yHUe39saxLHMm7gOmSbdu^GH_jt&~_R{cFI<*ve&%<5uwc z9oP}Lq=(ht8%{ub3iQZ}WRBv@iNdwjFlg2N>1?Q&4CH7OnlTD?f^BgNg0s-tI7d+q(uxxI zMU9;Gp2!dv706R~@h8-9oZv#DSWpU8d`k=kRUSn3h)m_7?7_I*f~jD$AnN04u$X8^ zQD4a=r6W4OhZSr|1WL&$PmWeaY1vL?<6;dHg2MAMjogt?rv z&V>}kN@y7te~lM%S}i>9T93jVjJFqy2W!V$Wru9ccWi-)8%{be6(KGX1qM>D3ly!UV`I7}gr4TXp>F;zi3 zlCFMuU#E3rn8*~AYK59$P`DLOc`_x*!ipNw`8)D#cuPqfWi~FEp|1733TFtBY?w^Q1 zXgbSb!|98wvjN#D2FE`XuT0CJKTD7(PE8+V`WHuws_6eaFzwAcZMJ7!j^`U5b%zY7ksF}*7S4Wh%x1rggz<+xNPTR|c368y5oe6D(VNfy8DHr8rRR8l$SGW#Ue=w) z6?+ccsh0&>1m|loS(Fq1xvPxA&>7i@Ij5as$L3tgzuAu5Q;20JXVH_?!5miApBaCk zBHbgiTc0v$d)pM3PkzTbx*s{mHGFe!{BMH&uv)nr*vsHypF`Xg#C>YY5B`Z%J4k1< zABX2%!KzMIpW{20BwUu&y4;atIdkGB51=D1p(akV+1+Q)P$IzH8bP#QgaAhHb+w6!kjmi!*#*Nx( zn3YOl@)>wr_z*&Zvl+O-8w|=HO3Uq7;lAJ5x{Uh-X77U};LPB)WA+?;_Z+&^o!Zvf z@fh^OaXn65=#`v{2@kp^XoZY7=c*%DQUM%A_mb#vJaNXagD$mR>ui0@G5aA_>hMju zHG3gPuatmO$J1z&&{q4T;6HeS#dnra$y8H_)+x#{2xYqByQdj=9veeSDBL-%k^oEi zC(Dsn5vG&sHo@bbsI#Y(bx%aXIBzHTC7t|7(Xz##V3Et=vBs&}cE|vHc$Vj(L}5_k zl;=KXUxEORP$ks zyaq<}4Msr^g8)nT(o}MDJc%7q>16V&0fslj9Z?*ZZB&cDVWIGD{c6QNIl`LWKSu%> zH5Q({eaa|bcW7G!$D?oI8T!V({1{$qnk#;NzfQ0{kdEIYRGADLi>UBIbB9?c_92b`)QQ?^uAHoa3o2 z<7$AFY^njk`%{>AG8LNByd;Ts7w2z@AKtE)POjy6>=b_UIpZz50J&+O# zJxY8Rg!PKzHV8FgaN5pWyHk%^1*&jz1Hf31>}5R~AU{6Dev(i0>};LC?|2_UYk>+9i55~VRLecRrgmxy@#f)Qq z6LcUYStwCGx>r109%)#nmbau}dWvA~FY76dw5+F|T?gm{ZOeMXK=foop(pX}7Nu9` zv*EBZRKiwt71ya12@)DiM0tq4ZutaZx@^AQQ5zA~O4-@C+zgv8#DbE*SwQz)BI8rX zoh7}PwEjLuhD2ICIWQQ{coHr!7{XRGgurqU0#o+`1#7b#I&+uTm8??035uNwddtZz zoIdBW;Uqe8KL-ZziqyHxB;Rf{nUJ!K>Cs^PFjW_a@QLSJb@4B{q^5{b$#};IkF7*4 zNoXW=#-a94_G5*uj(;qfWcN?>`*r$#mdAQDoR|##{#E@Rw6O~@W$HC??sGW3fy$dY zsRl^f=Rd4V0(Jh8I)CA6z$-jae?mjo@YOIJj{=Oh*0jj`KHrViNa z_X>>xVKKPNI^bE~Ecv)f`H3Lb`g%N9SaIb9Trl163R?1DPh|2wQL^|A1IfdfR5sjb zEx(6A1lQO73YH5`HEP&CzHV(2L^Les1T0XOXRKY zfC34yx`;%XXFf?lfy*=eTj7=>ud$+_FY_ zEduTMw!eOhg$o#)pHK(BQaUypxSW1*1%7JPP?ujMI(nvz%aB@I!(&LmJrzcjR%#r# zL}8=F!QHUcH==r~`#&0KimFNXLN)KJE91X?zYygG?QzZPwTXivUVoNf3^C57Ai18U62jL`IQJX<(fQoOaG30Ih^LqGhEnl5GIhSzXpd0T*WmYhY%h1jR%AN}UZ zN_G4cItyRJW`0=*I)*nyNhi_-YfF}0fV9Cb>?b_Yq48jYroA!dMt;sS8Z??MucJ|$ zCtwu`tRbv=ITyvSFLh${l=!Z!61jkb;5LGYt_|0W{rCos;w}|ti#HVD{L>@qO%Tz2 zPH@e+>1*=SHl9iY8FjOUf%%IZ&sT*HHjZ;&fKGSW1a#3{F3kE;OQG*@TQQ_@6u*TB zENIv>GSp{cr!6sD)?nf(NbDV*XsoV0IC*Radf+Y}kH;ixed(`q3jeJ!2^W$bbQP|x z{G>CI2bzUkK8!~T*ZkuuJo%3sCa<>3TT7v}Z^PD4N8PfONF3+>(X%=?{)DJi2ybmb z09bb^Kx>a&xBEd(J>(2Y#3m21v)AQnRr4mKZ3B+&lMKkjJ$OgUB+==Ti@qP^b{e1C4#_e+ECFN^wqUhw@r zQQwabzJDv~`~JcApGJM(KKTBtsPCHv-+v$VeL2Fvn*ZOUzTXjie;JyF+4oXQwS1Qb z-**msFZvsrZ^gnA7F38OF_)=)E+4^mv7g0I)t(4L)qg2x4W!%EC{0{u_FTfx8 zu{nhmwI(!I=jcp@I@6X3$aD!31n!el!*RaiRnD2UXE-yRGke6(a5~kO82!LpcF1;o zJh?_r>4Eed$JbbtRx8(gabD}A7;EQsJTQKc`uwhfGpf`|N2K{6R-$-* zQ|ARijxs<7@FZ{>P6W!)M*R~EY1yA(NU|=++s(1G4liwUWAk7_3y^|OKo@e^qh$Gy zwiq6Humc-w=h<8X_y=h5lD7toH62lV@Dtw{#-Iatq$gOBW@;HJ zuKA386D;VZjz>CDbiV83QS_ppAm$XjaV{fpRiI0jLEa+R-0fI@kiz$lr9CfjmF|J} z)g#TZ^!k?AUzYBw3%9p7e)hZ&uhLv~*YhnXw~`h6NS2CZDWPPokSy6qhPM~tZR60} zi}5xw^cM7le3s+mJ6h+I?u9~q_1RcJ$!-0sB6#X<1x9xNC^AHl%eibk zIeD{<$Pi1%5bD~2)VZFK9dhFj=%bRcz%m7*<0`4uIKu%>u*0*;)q0JqCe{!6=OFoK zQ$fYw2;ZcW2>LT^dT2$F$#OLUBXd+zlR_-)x@`rU(aJ^m6R5X>%DVC;{83xVsq*J_ z1VR8Ua1-b*c#D24nh?d72~xGZ0t4C&RDzPvneCDt`zlvZg{3OR6LNVcI&Cc402Z!uOdyGiSul?CG50#NQicIBU%8 znLoo>6N}+%xx52hSaFcADllpbJidYh@0DEG>SwUP=pwbhA{}E!wnYLMfYdDuHki;! zR|>~zQ!-5{nc)E57@!Nr`4MD~TX7eNuawPlAN?)n;5 zDsWi-J(7__&PS=bxTHY@8VNdSd23uk%6O~7#I4V;|rfNo?+Xxb&v!mk`NH> zne4!k#*JOr+}I2MA@3_^bWfSt^NJbWFPPc$@)_M*&g^;FjP4iC?0M;o?x{0-UNWP5 ztC>Ap&**;9%${j8x?eoAXU7@c(`NSUFr#~Gw7X06<_++a2O%Z_0p<~Kz5z$3HHV-T zvQQ{7A}z<&P@G7@@y>Tp)?i;9z*L3NSIa}^{vxQ;Ly#7|1&kGnKINKr zk``}lb9L5HshugirFNb?6Y&y;eFC>>N7d!6;rKXFEP-PKmUh-~d1|;i?kc#1hH6lr zw<|~UeRcep;~~XUb-+#^2wPo>jjjQ|TJo0^ajq^RokcW45$Y=xosCeCbWe8zH4XpM z&=&u9_@TP+pa-^WPNpD;0Do$?yD!XJJl4i}>%eBj<&^G$$yl#()u9+~C3m^4*NpAQ zc6LHl9EZH{yZ|OyVhO9&D+wH@6}(Tg>PY~?o5;&uIfY&03U0@Jn1LaxhDLnXcOsO@ zO@a7A3ME;oKZTck{pNPQxrDsLffqX?oA88$YYSnl^*OHIL}_3+HyCBIT>gkQX!h@B zfAyAAstlbAA`@tYMQehMFciyW`~t^Y*U8S# zenO`)*M|*`fpe_>s<#IgXIB*HcyBl;!-N|VTJz% zc&H6W@m=i2yj&mLJ0B5l@O32t&i6M(X~Kp7aq^2Iu9RAJ%ud4_gF{WfeTaT9$e;8x zkOuVKJP1ZOKF>cCk0MKQ6v1VPqt=rlgPFCpgqa0sLzQSlj_F(+>~BE=`pJZekes9F zE<8cSXu0`%otgm?0ujz|#k&geStA>Bg2k}2m}Y9>?}AU|Jxt|l_t&?gb|;&lx-kqC zBRcu41m2(oB8mi!nkZs|C`1u5 zAqj6_!XXjRWmjAgQ4tYl1UxWtLdeT7fQs(A>%p!U-ma?%tYU^M;KkvwARa*l-Y}v8 zB^(O*eZST3O=ckOx_|sWzn>q;y!X1gySlo%yQ;dn+MFx*`SBbZ7VhiobO&@!%J$g51N=Cfc-coF(Q0|VkVEg zjYTc6u8E)+N?(d#^zP*kk#?&-fUw%E>q{Mo{Z~%vSPGcCQk7 zV#X58fA)UG>jM-4Ao;*PheITu3>;;w!|7uhNtWlZ*ao6PpH_Z3pI^RIUv&9$0l&Pj zzMzKy(~DnL@{6*yEWZ#REZhB)_zdeq!ewi~N9kkdTG)ir$D^f@epr=kc`1H_ZLRVL9m0oLSkKo3 zv*GI8T}tYTlCq8=@>)lH)suW}@G}wyA-VP(OT=vK6h6c&&{43$XW3;!$P@4xLtxm@)B0xainB#RgMI!!~)DoqSafm!8nqk8z8fd#@;*j?2V1)ncaamq6{X(jVP49 z2o@ns<)7byAL9tN25G{(1+fW=Lo-v5UNy~1fCuD$^GYNHE>H61xC1;%o8GkBiEi!i zcr3S>3kaOG#9q5I(hi*hpky3_=E|3jB!;!azDUH(?o1BnN*zjnGa{vovrB0(_aYPk z5_+-n&?SRn3B=(;q>1~%unb~ht@cG6JMaO(dC1*S=6&S0c*GxJfx#KsX3`DTsko(p zA$YcL{>e#H|3_)CU`Ihl0wA1?k{ixgjDWfm`0n z&QdiLzOd;|r)C)8gt-2(28cdBc;G_&-fR9_YKWM({SgR<(yzIdmb(spVIPxzYB1M3 z*(4kkMyAt2@7$9pQMoJ1?Iv8fG=l^6i1{G1;3&&fDa-W5%DR&Ykm(-~{nK|kFgtXM z)H~6X(}aw*brpc3rxj_-hPm-)Z~;nss*Mz3UZx5@q6%KvTyRe$1P8-wZkR2WHFwmm zZ_!bh|HV&X13Z{GwJ9$JYa=Xf{)(O7%Vr;6252lj5~CFh5O%~qG(Tm8quM#w8|&zP zwg_Of>t6We9@up|H%pK3hl1fT{}@h;z#aIg`3|x{rh~3qY}9kx=W)=0B6k%Ma7rt` z?w(B`4SY7}H#UkJ!0(8yC*EDiLe%<+=`s&qDrizF4yFIN4~rBz)LNvL!I2*86T9yM z#5U7|*XEjqx5z#jZ;MeDmqK>icSB^y|56}LUG`#CmzEB)}1st*MxAw zh&{3C^e23YUTS{9jpR^=87jlnScZcrC^5qtWWZwOY#PhXEkp(o!mZ8*pSPatqqQ1} z7!Yf;wH`kWk50-1m~i^Za_GR?;%pjO0K1J7ctK3*Xu6<^qwL+^S!Gjx0Xeacu^#jw z2PA}^lRE76w#ceLCgaK|a|&f2L17kUX}>em)}k38>llz%bG6Yrp=PJ~Oo>p@2bB!E zIq;(WNDk-E{ktI78ewb_k-Z*h1KIp|WTUs6^Re9zf}VxX=z+ZstZCdNFrNaUAwned z!88y#T5Mh|S+4jYDp}q{7If^&Lgt!^T#0!Gu!pVHe=tE_K!a@BH*ed=(f?IKozqv* zAM^h*3`+`$R#01rVmcr|Xf9)wi}`4LXvLu9Na0b@a`}Znu>@x1AJ{L_zb^x-5Gyf* zp%Am;b51&p{esy>f*c{95`;!Zbey2_Fl=Q73u71X@Q{-d=SW5f@^^`zt_MfWqAqv} zOC8CZ#j(~xchj*T!EUIo3Zm(7*~MrMrr3(LY+bzkrPj|&nGSYCR3fmM9>i@*P4z+( zmEPsV^pUZ2nL`@;NPe$Ti}~KmupW6^IVE>QZ6ajFL3V;5>;g@o&Bc=8OZ`&wzA`84H#T7yz`&{&=NFLWmHdbbX{Ev>e3lWp3M~ z2tAUWSPl?|L$N~v!ueM^N#uRA`_4GfrHP;jwen|vL8z5#{OZAedaz2KK=&>j2ke{) z>>O=@IN=5RdC>{;l8j3gXdHe)W1MYL8u@>S`%9|Xq)R{b$~cOTGoRTWN5*m#*&@X`QOCIZ)&v6j zr$=X+({>6?TB|lJ%R?I;L|JAv+Gf*)7oghq2RC-z;1ri-Q03B7w(t zbZ``gS6lRAq|earp;UfW@3f(qAh^OrjGBxW1L80o&`Cw`XQ{9Wp^ zR!Lii3>^T&b)D{oksZIG^XdD~QA_y(#KZu3G>c{&=*Hf**gh@#k|-TNB$hm_QieY6 z7Q~?sIveUisNu1(cWDCaXqC`qPZ*u>SI77>zLv3B387u3W5gPtVPgsI(@kU0O05#k zRY0jy1$k488!TQ;BS$iE7s$p_MB)(#d}${VQOu4J$w_aUc^Y(df=dhum@j`PC%c}P z%-}HHC{BtzinqKSVvxZPUA)cvz7%fH*d_cs9+AFRwnYWzU%mpj?@+6neNpD5dKgT( zr9_!;9A><)5%1f*yc^-K!SMzz7m&%|7AF-Ji4jk*V+YL=*Gg^fMCi82`wa4mC5V%% z8GM`7#TIr597CL_2sLRYw>^--FBy)wl}GC}$iTk3Pk@?drNRrr^DUo)jvG_U`y-pV zte1?BQl5R^j8lV`vMj8MFuVoxI_6!@eup5>Whd(*mb`F$M(fzTgLi@U6Ja6!$-g74LsUi&_VJ#RTp0wHKK~64~ z7}1kRNan~N02kL7JaT=5Hkrv%>MyWb1@+}4M$5YsHKiYbbzlYOTemtwdvvArmsZL9 zv@!n773f^qa3d4Ffj*H5WnM|9F%|r}-CQw)f^7*lUXcC_(&;RX4Lh7lsB^;p7P{~d zO3K>`=gPnw#67BnF(b(i$p9Ev@LC*w{W)#skNWlFA4}35(WY+z*+0quwI1w&ZOQ%w ziXx!WoPs1jOfmdc5LgWpCO+fJlsS5m|4sE7`%LQd3-~-T$-hK>9%+AGjL+ke{D%5G z&iWjoj{;I=!Q6WbAEk;}EI)FsB&{k3fsk5ir#{XP0x(bkaMlGtuNVM7M7m4joC!!d z#ZLLd1j*N0B~3=sAz1;~tjUrT<$GYxG$hfNR-rRZk~YWQb1e!)-o~fEkyiepurTms%%RAeK*i>`65+VZqL(Fn(B}RPFooa@}b+`w4u`2%39kYta z*F`&;*HGK8>iV1|fH!b+qViJAqqoStQOx}Vq;lEIRzu`^tNvT;`nPialGVYzyeIHB z-GD&+qL=Qy4CCvUEkqtmiao^6w(?(K=YPP?4+zQcApTRf3;u5-JMbSs{2ylFm1vhO z1BCG*aycjnee_0jP6t@7`{Ca({QD#R&Bnh)`1d^it;4_F_}2<>XX0Nz{>6`AOaD3t zYY_$F!0c%}z)yo@TeD&O_c-`aGL1HFb#su%zF9IRQmgfT_jxLNDE01Z68$}zeQ8%K z7_9Sh!&$PSP^4G>9?e-a%(Kuu6p2cC9lBmZ;DF4L)sD!MP>_it_u?ZHPY5z$JvktastqQzGD|q*-=%$&?=y_9)i^bwGX%Q%Z4AE7K;?zjd5_0r^4o>4-|g{P!A9~rHg_a zq~PpuURINigbD2PK{m=VhD?h*f=p%#oDR@=6V^!QZCM3N5Th_fCz*RcmXs2e@+MOz zF~w_cRw=zy%41BK%9K9lGL>?QN(nH<&y)=FQI&FdwbXMHQ|2JWd>!@Md_s2<@<}-v zN;Y+%p_*6$Me$=?6503a^)#%xC`yYC#V zJKWCj0#|E0W9mP#ol!Cp_(5AN(d8GbMH~++d2Tn3{s12>8|Q7cP7pt#f>6qbJqdqk z5wJxzd;i<=nSDuR^cwF;JTV;d`w)DiOps(estsJ|d0xI8zjf|u_zPs*x%>@!z?TO@ zDQ=@=fH|s3F$DrfVchTV4Uq6Czr+13E^Qt~ZasfarZ$)RH08qwz(Bu18FQ$|m}{xw zg@*f-S`*D|EE$mE>%pz#+T-xpoCy|H;Gr=-tO=RsgUCRqr4fL1`7iS3&&lvzXuh;n zn5S)6y1VQwEMFg^K64#hQ`Dqz6JDcNdyP36&}fztD@MV?!P>)3ILhQ3BCiKzvXC>C zbZzenv#NQeD*9#I&xGlw2jXO`43d*>jum|?N+Tcj2hTOT3?~K*QT=sk&ldU%xs%NC zSRYU7FI!(7M~I_P$k5kbTb&^wEu zh59;q@(=pcJo)C#Ho?IImOA`X%{#UT*|y>q6tu0U2V|o!khuVD#;Tu>GFMK*p9uU? z0dZHZMELa0(L=#X))!LsTgsWbo&=8k490uc86+_RDGko`=GVi{aP?AL$_`0A2^u}44jmA7YIUM-96 z%GMpZsM_R`U+fye8FL0*j7|32|=gEEI9G{dw6s9%;;v`rYd`H zm5`wuE4v<1q2Iv?E{HTM73-KoE&A@x&txeK9(%KtXsHI*AjBG_D-g>cIdRn%q#!S03az zfM6d@Bb{7^&jNvU$6FSMSUr|=m_|AhBU)hR4+W%CB|BAhl^s4>NtR=S*Ri|Q*u{G1 zz5sr;$w@7EuwJp|S6KoWSzPf+H!M&hVmQ!|u^ubH6;shvaXv3KdPjDlaQKqXi)al*L%?hZO0=kPprxEBX0)?hE;e2o_aQ#f&OOQO>Rq8Okxb6@pk_NL1 z<85%Y@1my2Y-BL!se-=21t=KqeOS=dEGUih+|4R+y-G}H;xs1y05gkOsuG`5iH9K{ ztfZq)WGfO&;SFC!gm^E)3@B*f$T83e`)wcMOXiF=d0Qi|cWUjKd_m(Mw) zoxP6LxGU9N?dt$~gb}v+i2s1sP%~2`1>{m%P=Y-&8;F?QPM7%|;W{zDi(eYH#b;-m z2M*!zOCyF@zrbAuNxmX8{GRYkcf=`0FYY-VeGvrp&F%1kutghA+FX*JM!z+SzzAZc z&TXg}(+`72uAYBHoBJ1l$7X>StXV*|56$Lz(3G{dpxLdP)^k#|u zgChz>Q1*fqTw7vfxyoY<;>77M7H%9AxNAa^uS=n}tf(IfL|@LBs2|#_I}oIH5Z*7i zYx4Bbc!U@*j=invId#(uJv>h^L-*vgktSnXX5R*BuSel|M*;Bslz67b;c4^q7JL() zj(508pvR`JmZh_ORYF$Unjjt#*5JvYgnbgiqXy07$vEPT$k!~Q?I^q6i!F36a z8DN!9N`JeJLVNGas(D&)%rv&{nm42aC!wmP zDGH>{0&&@K26jWQ2A6pN^YGsk%#(nT<(}+!I{@LUUn0tETZPK+S9#Dsus@v3pPs8O z;7_+z1Nn36D#oyf{h<$kQdi~U$8^K10BiPRuM6Ak`iIyb#@B&oEQxtC2*SxX8>}Eg zodgh!Q{K?UF@KA*(}h=YYREP7a)~1No;Tl}iQ6=df0qxVx)db&xSIc&%41*+xq zW-6@j_+m5O^YqOKb!e^7nt6|{r>qty2qnwgn#uxT8yiAm7_s!zhai1G$q+*vuJG}~ zdNV;bH;Ncu?ztqi-KjuBztJu!8Lhde6igLL{R4lrqHwbfN5Sug zuQJeWOg;6t!7sDnzrjxUZz#XYhX3bpgCAqV2Yws;xiXXcRQ4Jbui_`@aRtIW-!-!@ROw`Y?{GcLdRvUu=AzGe;a`W1kKo7Xn70x7 zJZP1kzZLIV5u(=(cScKM&|4MsFa-@0df-T^@07riWPdjllEO^PkdBvm&si_n9W8MJ z2gSUIgPCVWR}1@I`X?Dx7fb~Ea``n|i64n#-y86O18Wn$DfW#GDy3h4+DYhB$`E0O zG}1j_3okg;E5aN_vb{yO2?(jB#%6QUmi@61dA6lQ&A%4?c%OzE5ANl)T#!0b3lCM* zqA;*AtDqU5@iWveQ<=&j1mAQL!9%w<;SB!Fa~g+$;5`~suXzn@=)WgM%ZCquK%ows zzTc0ShK;G0OY*s~WKd+xa>x3`&6kssPYO*#N?UG%`lN;gYWb`Qqz;btK+}F-ACL zHj)nQ7M2cu;|b}|qCcWTb!PIP3aH(PHwEZ6d$(Yq7>MPn$qW}Z|CA$9&TBXZ#Uzcb!Zau>X~wQ z7?EUaakm^8C&{Y(TMu1ior-A`tI6L4eLx4i+0H@*7C4zLgQ@k4Vz!+E$X2wk-S;v; zA?j)OJ&gx341FXD7k$HcOnCrS_Q@ib zBIN%z$a>~Ju6G3lm<9k`5RE1PXmA3MiI36K3mWS*t&;j*LgFnc93fP>d>!27BL?6E z*g#TR9((fQ zULhty)4Y)Hjwn{!2C)o_>ww2N<^euFSizcH{!*)Uw}qUA0}!yISo3D02+o2vos6et zx|sVlQX_luhph_iYB(8Ewa9lzTx&98t%+;FU#ULAJuTO2!7Y4mZb7C{$TZ9Fxdp_< zXG&-%0|#_I9B~6JT)uc2eopY8>5tAaZ%#u6^k+qAU)m#amLK-V0SjV03QRWXfjuCg zs1RObHAI$}B5`w(@pIrCjRxcEbjNYkBu~yZDCTES5zpI)Zdngi(i=ioiM9!w);9A& zeDc83Mezd4dV}8fE2tB8C+vh5NvUB|1IJ|R^H(9BHmWc8edj?M>6QA2e)Y{dm2rk z9>*8D<14r3n#Wa1d=Q6+QhMV<(IxVl3y&kW)=84BqPW?U>3++F40da61K2iv^Mqbu zS!96%D_GlP3`&r*FUVPdAbj{;o$1N>iPqUtCIyZ=XJlc58hs+NFl~{IH_ojE4fjXy zEi(28cD7*)b2j8{m3R|4w%IvTzj%D-0-UAnYFR-x`%OxDr(u6sTjov4?Bh)-sP?u& zYOO=hU+>?`jf#<4CuqIa$PK}s7HbWJTA`7zJ>hZ*r{V#k_a?CS#x!^Ciqu4eWB+SCB|WEC!DMR+rPHhZFp|;}CkdD%RzA zNy%d8TfHqQRw=5tt*K?dLW;WGMphhH3kMe!7?eYz910>0{AXHYhnx*@S4?9_Ig1)x*?6iALxOrBl<(F;+R?a%Af=YjYP@vs)Fs2+Nm z>pvn8C@4hpHJ?yW8479xpuEO1-mRe%yJdVPvUu7K~MlHpURBvx|q;QIm{OsQb(3 zi3EvjTdue`S00a8bSrgyr^+DR{-nxCLK(>WD|FL}LFrW|Q!0_86u&%^D&%TR{pcCR zFY5|~U!LL5N&Ldw6-O&Y<5rb1#xS4>`;X2^t3N(~GaRdGL7Oma*bG&`m>Mr^+KJSN{1i8A4Gx(110h&5GmM@qH z4(|Cf1CfKWeENs^k3q%(QP# zZqBw{Y(r|NuPvPE=uB2Zu^qh75~G)x>28*qNQIsFhHQTsj2|{NKHD3GfRD9#PwlU^ zIX+Ye=L|KzE&J#@t*pkvuKuYPCQ_I+lX+S{XAQ5s$p1{&3O}Q?a_CFl5%vT-aaKK_ zsqDzCVMegMZ`fF&!3!XAG zbOsLkuesx5^B^|*(e3k>ijtJH9(K>M?4Ay9G%HE1;CfB1cRRizAh2T#5H#mQ&-5UY zlcSAg6Bq274NYncuZN#4=1a8JlT&*~do->HYTPvBU4*t!PqP2B^&geNo1g42acj$b zPPn)>`X+hv*U!AJG?eD_z+~XbX&@~QZOhx(d_>9Jzi(UrI7k{zdj1AKcGi=XUFr_= z6xGD%o|Ptk2jk^RMjELkGtC{1dYyN5U3lGxQEjzHwI~ zTI(MWV(UVwod+cPpV;G}0C5@eb24 zJ7;YpT{j0$z$q}KeyVH9@0dE*h;&Rtv^JsSz7 z#s>4?PuN<90*p@(?V-Tbnp+PF&=WjZ+dPOA{l{4Kp{B@Q6lRYu+a7UgTza>JbZh*j z+XzFm2xpk*0!9xGpI{z)PrOgj6FheI1Kyg8mM44t2Wv`$Kh<3B0z5O6x*4u~q zld#^}_OukcLFhr)WLAhsWvrm+QW)yIPVof1*$6>^YIK+3+*Sn(@&5r#q*bz2 zd5xaHFRtn5U>bjRF>1l|cN(lSq15fOP<8&i2>`e6725#y%j0PDI1cF*b;MLHcrq3s zRWiK%ljC7;lfAN8c{@;Yrj%S7D;bwtrrsN0^7m2Q_t7AA6vQ=h$h; z85HdwgX2K7Dt~wsD$i&s1pYvB{1_79xXO5^1ii2KgNG;O>QG7(o~2 zk?xUy!5U#zmDkvEWqCjqoimp)6n@PLS~xx4z&0vo*%zSkmUj zyd8&9D;{l^q{LfcQ$#Wx@ex_G!?q06q7`HFMi_SEc77Fx6}_{;ypfWS@%TwxD)U?1 zVhxO}@xBYr!fL#i4=qcsOXl{tURRh+TH_MTWyx)lFbPhFp!YNHHt%(g9(_)p=sgP^fz!xEga5ac4BeA~}eJ|yFexko7%78o$Mx#t-??S}!n?q%4 z-v7nvaBj42yuW$>*To%D^KXimmC}tV zkFlbysOZF<*vHY6@jJ2D*aoXmlrTp>MWV^ZHi#&n8lF~ktYJe!>90;uO}%=NqW5$* z6NgHhn|Q}1W-o?O8pQ_+qu;BW5g9RC$kPc_NBihW{8cWLH8i_+V8BM-F z>Y<0XF&9el3IbZ?W>v~mi+66YO1V%Ej&$Wg8GwK0Kf$VjSx#Iv`Wz=?6ptZfa%JI? z_z=FyetRT{C$saj(z9uQmY(%J0J<G*MVj2^PQG}j@=0cS?PbL0vKN8u_$Yvy3a#XV|=mE%g7^6XV z*`dvO`<8ar=7s=EZIMF2hhE%ssRt`$A_yK5#9id zZ{8VFj8&kA7Amd~69fV;@z`aqGSHJ%_FD``hi>`?Fhr#9uErt2ACcgOKyFO$_zj25 zcJo|SCT=h%EDYGWEFkkS#jzaXssOTQ#gTHnYxg~jzvdTD2_t;+kg&}`+~vHIW;pZU z)6M#x45tRr!+b?SKdYeY2+jU6>*An=$yO7(LP5_~(2o%MJ3vPkGjXCy9Iq1ZW+JBF zq{vJpqH#P}xK$#PY&Sjgg$uoznRVS4fO zRNYwk1%Py89%+mvy46KE1Kw#!w)W<{ZN=z1LnuWLpbEhD)A zfYk|kz{@X^lg233IHSIb914Bo~NLnB{YnCNs+%J5w>{~ouY!v zEff@3i;GEhLo($zl>Wu_ib<{tT5aawVw?RQ&J1XCh9__w&ej2Z$5YC5cnaO?@?0;0 z0dvB@->C&4BOso#Lgwg$f_gV1lL2HH|L^b8sCt!>#1vZ2&7`n%hK7^{ldw5s3@NJ{B2odm@#m`*kKR|K`2HU<#mSuo z&q&Lb1V@wwcBTi8boM_avs~!fae5F&a>sUW4&v{iWL5!Du>nY7ghTkS4E}$_?2ei^ zU}e-rM&Sc-qUm@GbiCk@vZdHoos7?=IfsUZI``v5p$8XIB!jt-$A|NaB}o6mD*zN( zhe^(ws^n+P@?1cgyHK@^^rEcE9EQ1`{P2u?^YOn4B_Do3XnZ6C=8}s^$z`Ch%^q}v zjE=BO>4Ig{!^_eph)!Y-vvEf(|2(r)6rJ@r?QsREyJq*-gH45&vHXkQ4pT7*6r$;}Y0fP!+ZovZlzm zQH_*L(d16Myjo~XM(bGAx0r5%&-p+3S1(6KFlST7^c9U^9RJkO0;_|c(nDiG{vj?; zFb(IiylrrJBED;_e&u;ut6x2#ndY!i$^eX?25iVU0go7jPY5f7{-h?*BJRdcWROwD zdBm{l?;`HT5qj5;&PKbqVjcta(Ix0diRvdrTDtURjH~0!mhPRHd z4BiD($vhqKo%~3_^GKR@UlUk_!!iliuP)%dPu}aps#(HSc{|8d-xdl}EqDYqp(U)Z zd_)HH=|~)G`Oi9lgqpXAUam7}dI7jz1M z%8S~J1zVhss_-}lk1q9i6&_R7V;LTgqEG;g#^Vp_@k%^?s~$(;al3jP>4mGQ0>Ao`(k@!Dw1KF2sy<#XF)_^un20jvvMj|SoxwPhAQ8*>jEjG+>JW8KIiqhfZ$dTVh<+nI zNvcW9{BLGlZ06wtUvzoS0_pOfUqzQ+4el^Kk0(wx?DBN5NAx^KYbj@b4313jl`<5V z+u$7!Je{S+xiTNgBqAp5#IsbSi;QJ+cT`9L|37TY!)Vpv;CYzTL#eCy)1p1;b(KRb zg0v?s1sb;Uj%ARBB*(J-O+~>HXK)Nv%8@AUw&~E+X@{h1OfuqB1Y0k4ptgUPQ-e>R zb}xRkBB)6ZlPZA?$wuoFEljpOtFK+ZUAY_nSBo=Cj7kwvGbuTlGp z_UBKyQHJ;oIR_F@v~in6n@L?2vEYB@9g|PQ!{#aS6Lj}nK$%PS3KbzP&SBU)B`(Gt zVf|2vi`+yHrT?q7k_3hY)Ut_rp-u@uwI~Y%5z0mZuA1OIYVY9-PQK9p4g+AWxJd-Z zBA@~;TXF|`7c{QJV0jn3;t35eFjpJ~?LKh2jovJ!l#v%g>1(&CRr>S$EWzVLlqs9z zlq5fWKuMB|_z|LL=86t{5_zMIB}zW<7h%tf<_mkS4uCx=IhxO7wApK+h>~BCX(uXZ zhk{;A=#hYq{DX-@RN}WmDQYGY%UIO4aYfxhXb~k(Q_#y4w1?1>nK%hWtq`-qv^K8Cb zd$VL%yc~)66t#yTe7f9?twC_n;Bz+2xHmRmm{T7S8nsubK7Joiy$dCoRcL}uqZ#-R z;re10DCjS6V+$;;(+DlC4l(g;l~}71M>Fvne!o1fs9E?BoiQm~>zsrWq{DQqQ{-Sy zlDA_~Lu0V_nSRsNiYPnF6{igqsq9oeFe@X)KNi8WTtT>qmE_|1z{|Kz7UOZ4s`SeW zRLWMG>w%1ozb!cmKZu6|ybe-LOLA2EhksaOUayKuQbj$;qBxu)?kar5zYd)SrO zWT1KoygPkLmOWzTSa_$)gWZl!ax_4VC5d1;mxnT<1+KPKM4^^4C$7hC>vcen0B^M;Nb$=Oo&vJ0>eOS<+r=b5x=q1E(HWK47d{Tyd0)_<^hW!+VH6JSs zpPQq|g`qPBjR~6@76|F@K`UC{K;AlH&b44jdWdOZ^C^MBjnZsYDE$K!_0Ie~;OtWb zS|tSdq+mzNA8q|!HBA3T_~7GeBBDYhRuKq!Wa=GAEI;>~NF9F7WYx|m<_T5qB!h+V zJtAjJcReITxKAbio{60B%wx!8lWZnFAZpi21v*xN_8}t==S;MY(jKXc@r$iN*~~1^gjcH$DB!E^DBhUAYIwsVDG>lM_||Sw#SU#v>%_< zJTh<~53BcZ@b+i$z%#FgmKgXT#puoB<1`6p_^*LUSh_6XEclF=Nmy(wwi*%Ha2U3v z!HcDb>gP)L4g4brg(V&Fn?b2=!-&6wjbN_##)twi{oVKhH2{Z2zUS_MbC;;_q-@X7#ySjl2ugY-su7uv+O}> z?*2+??)r9ZdBMbOXqv^1>h6t!Xp`aydqb^Rp6pNbuCUNB-V4Gd8s5iRIBC6`mHoW*hkv0(+Wo5el+Oyrg` z*$9CqWyo5`i15Kyf%|-(oH|dS4nBGTwjvH6!NpQ>;JTJn-X1f~ z;7kh;=%OU}n~K_j)d?E)dMAiurTeeK2EPkNhk?Wm(;gg4Z3KCXVAIGFV5;eE(U zTC^?YnQxgBt<&L$NePxemwIA^fd8$~3@m%UXY)Kb9T=!{MtsOA8_Zby?fPv35jYpb z+2D+x71!_+JHnSn7?;sXOvJc*uGRw*w%`I4w_g9_E8rThKP`Go)`efypK`JE|Bw1p zsj4GEf675ME$L7D7s?P@P$5I?y(ckI$kFw-_hXbOm7`a|E|UK z-7r0?_I}%9`ubS<=Pjl`6HEWN#q_FJ`uiuO+x50cVv3_|dkNrzpk~R_dsD$} z)@v7k^%KU@ECjz<^(vszQ^`Bn)2)>Sa!4Zdr{58J_Ez(Q`7%ZxyG6#xIlVDPPD6Hc zSEzZ6+<*@pBmY=n*&pM>4qN->Z$K0y!Nfln6Q@~3-M864mM8_dA+GwT$^s9iew)!! z|4oXef8JtxNi6;27Sqp-rN7@|dh1yF+7{Dyf2Nv)@z-Md`dIqwC#2i`_mqNA7kCC| zV?C{?B@xsaL3?DtLyfLa{q{-cb(QmiMcAiD2hTucs?^7k5WUPiFDSiq;my)Z5BEYZ zor%KCLFheuCbN5~J$f#BHu4rCFZv(hhni@?S(A2<#uR9TCF(GlvI5z1g>ApE@&9(a zq5%EaHtBU01=d8NhwfMH*l*rG1I`YiKNWC|4Gnji*Qxhf_3ko9;r%2tiN!B-?c(82 zt)pnOGjy_+_@&vt4XcXIMh{h4A>ydLLpg&W_`w+BJJ+M)8hI+#D?Du>f#-U8{$g|F zIh0ZwdWPHGrOKbUH1smD_Jm%R##9_fQo-~qwH^gH)K#KRIzo}|PrDi{&r`Uzup&1Nn)wH&3HaKrzoWE(XShW|PLl?nw zs5jb;*4|AU9kyX{AN`wXWMCbH!(Df5wXr+l(2vbbvGwEOe+y3F%16^%8t-?+(ifkQ z&i2Lbz!{;y#CNPtU3Ay$b&)Zyflm2n?BuT?Gr5=dAR(@sgi!;QfHVd-r^>7)Y&a~W zf5tB@rCfa3vi=k&As0eqqt~b?DRLH}U^Q-uVI=azJ+Qde#ohn%scIpv<9_6yf>It( zN^M4|H)=EI+@Mf8O;DP*R4{-=8Amu=(Mu2wA1UaA4=LP}*!` z_yDS)FykugyJ@D^iqBhigtoDz|2eYRif=H`W2$K`R8x!Yabe0lG1W9P&iO7f^%ZEQ z&&`z{$@fW*oHiOgat&%V!&SCsYAc)z(RZS7#upMVy}`aNW3=ky7U;`%pX9H!T)vJ` z@EAYyPe~jmi~H+>CeQ{Uu-eGDwh7#3F!&*`8W{Cyhg!FoK*&5 z#&*I*WwGsqk?=Xj^yT%2{>BB(i-}!+o|E|^?g$ zwop%A*Er0Y*R?;t!gY-+8P+u}Z&=r?<&ApGpqVRa0YJ=6;K8}{SIT3Y@+0Oj6b7S- zmre!+1{|jGE4RLE-O_vpD=}JI{DgZleJ(>j^i_;LA`hgdjsA@!N~F%X_{ABd`_C;R zGs?=}_~d_~&{9&U-~Uv&BG;oRndE{&zL`|dZvItLCD3OU@H4vsSm*=aQ}E7@qhf@a z#x)O4>jQ>|dVK)Uq@@L=T5$wN9<|Edc%|hXy`w|sBX7~3F)k+m;TjJz8#oKBCkz%? z$6LIbMF~02>NO7P;eCh@$U8xBD|nIdWBE62jL*ssw9%Gr)t=mSLVWhk5t$utP$#0oFp?Mp3s;0U{{BW-T4oT+4Pc<~*p7D!T-V2sAwtsXK zr);vO26mT!^nK6OZ9grj?t4*b{y|y>|AsgPGILk6v11jddpOA zF7@V8Z%OJcNxjvg?V4*LxEVixs+S*e3(-0~(IAk2s+XUIG(Vz-TKw1oYD{HS>qlz~ zdqm{!khq?z^xf~P)`e30wwS&?mfowy^k-t}SuLhl#nR7cF?~`jz4Hm_HvNk%h?so< zrU2S?=I;X0l?_0&$?h?W0d&B78`vYM)w}Vd6^5g2@NySlPTp6y=Uhvk`~+Lh(%sP4 z3P4wLBK6x})$gCNz!A?kttI+aS?SoY9uiD8L^Tc;rWh4#(XGKkr%|y%9@2~ov=d)k zMg==QSlHR9fS7}a45MP3Je*-vz*dq}S3$<&f70i+)RizF_D8DOPSp*~NTNc0g!Q5* zD4BVP=^jdo#qPl2K{E#p3#GM2fSU&7@*r43Q{L7|i!jA>>XXu?I^|S7bSDnqf8tAH zWWvDFL7@Khoo>frPtGrhyMS}a#YPoTLV;Rk8#vgY&;oyVcb$ACqV7OdJ$@X1C!VlN zljJBe>I3yC3Hpc!$FV~%Gu={Bab~4p`kh6=Ta%()nkQLR719RX@dw6l4gUa?n~=*z zPIl?q;KOdmF>n44!q^^d$U4p;2qHNof{<~Dq|v%rm4)0VOLRucK^#W>rO3E7sd(-_ z|D%E86i4)7hQQ0EA^&oWN9fNa3#`nDrcjlMX7bl%{U%wz1d-ywGex%w??( z;_#mnAuMZU+g=Z)T#iHb1EF*d)GJm?zWn#x+5@!+Z-Ae$p8pa4A}X!+r`372#g!Yh zxxG~0lV_19xVWPfcX8*Ay3rZVP+^3X+~D7Xkb2r& z7^#vX6Hx%OHRz#{4ID<{ND9ClZexQx02^E2Xf6)+Z$*SfM9b6g2;j+h5U!N-HCI{=a-s*)a7Xt#7eW!M_Ej2aAL7U$ zP|Z$QAQ*J>&?mYBabAw=`T>?yfrn}M*YISIlhrW@Ei zQxLSm<@RZvD(X?@$%9#vtxE9cj1qex?$uS*jK`Xt#G(|haUN0Z^OZv|pwv&aLu7Xh{tL^z>^ z9_L{T(0B-7({)P#tIXOAc*qKek>CxkAR3s6@Rt#tcjF^`Dly*FBm$X?zJZ?55Nx#} zAmlb}?q?||F!cBmmK%Dx96xnceBpn{TAPP&(u0{}W3!upuK zk`B^Mjwd$4gAX>Rgq{NB)=5djLMazLVC2<#w=HvIlJT zVZ@Jo-p~Tl11l5>(o7cJ_yQDrl}SpLOuwMD69%IKvTiJf7~jmt}4dM#utK&iwxs6jtRr; z1NI^nOcl>am_$u;7;y_Ns71A~j7$cEM7cSvVm?Kuc#JPS`Cn;^*W@+9IabbWMBW0h zUBWiAElY4bE@Nr|so=2g2!^Dc9btsWUoT%+57BN-bB&v&yiCrrvgd!{Z%1i8JMulS zA|8%?y5k2$J$fYn@E_@PWs0KH%_!KSc#KY=mxZ>$Dpscl>NQ`6P2yURxT}@*B4kzH zrL}v_Gx320#BlpX+QpL>TJa}HHV+`J{28|Y7<+Vat#cc!SiTFFA|k?%+T0$bY_O_7 zD})9E34gEIK3um90Xs&wX=+U(S!*^ za?=O7QQTlOS234jT+#J1I!V4O8a~eswx)u9tzuI_fpn!z1wDTe-se=XY!Igc4jud- z$L}%Dc1gCKW`}|V&h{E{Q$r?OOb=X%)A0?8l=^|a`o!tyrJsLliXe0rI$=$NAZm5K zkwH@}CrGMh%ffVc(CB%Lt%Y(UAe$%j6|iIdN%HJDiXbZ*00M`jl_ z_4qDrUf>yTXay)_B%(u&dC@0Jb(a|SU++}4Joh*mSa=ynJGG)u z$-)=0kDBKYnJR{Q9kDS$OSO43vO_2-0>?2jegUY8Agi6U`-VZpC`Ir%aok_WRZ1Au zyc!B}q&E^ZS1Vyn8snKQnr1(Ngy?=MP*I=7T{k392EXL;pY&KKheDyjUe70qYN#ukpEUP@|vp1 zOUPsEiu{Fss)JJIQ*VQSunz=N?;8{|I2BO(%7JnNaZZ+tlSD9Yu%ZB*D;`q}-K))` z2ulHqGa`-$0R`-(j`5k_P>oaTmhkD>^t#OkFhmttiClP)^L2S!X=XlH2qCWLe+2oX z`1)Px1LG#2hi$O)=+ppx&XRtz|_3)Yr z9PZ$s<3&`Q;LudBVRS;{;P*lCKzsAb_TjHx+Pku|pB^~sntoMab%U{4{CACtu7EK% zW5bwBIu1qxh2Z_nXjBlzi|;2=#{I-FSmDC=iz^QEOT~-$qJ-OH%Snb7@M|51Uz$IU zD!4RT)j~y_#Tpf?Lez1VX-N4A+I8%_{2Tw024ime_q5pcfFgPahhnzDe1S{I)|(v- zMk#5{_d&{?o^p3|s98KkwhaD)%-Ay6eHOM1@`1Yf`&(tpfDU7^tE_V{0pV^yghv51 zce9ocB(Wigk)*zeu<6LlBMIL>zBx{=Evggof)YGnqTY=etBg#-pU%^U(FWLA4J!6}>NxkcM z*ZKaAdY_E<$$WnW@4kz{Z*b79RQ32m^PS=_p=Wg{+ zMO+#GO;Az&6Cv{WuMMnXsZd$C-2=3tk9mJ?gFeE5EYAB@h9Xs}LyxgF>K0@@B$)^u zSg)ID4}FXCRki3=gsJQSeCyhvtfWX3e;CE4FoEA31l2 zzf0sbWJf|oq@cMww93EYLqbvJ1Ap1ayx>iB+7cLHwR?Yw5FS@Lur+gzp3oYEpEPr% zHNGnWVwNtJjw^BF=K0VZ9iI@ULPsuj6vu6Z&7*(bhsMu2yQro_cL$Co-`&d{Dn-2L z%o!KvZ7nkDA{&94`z;JOh&=URo`i59c^e4=6*I|rH}G{mwC!z^w5e+*X)_LMc}EL0>e_R*(Pa+b4~U5E@0q72X8FN5BuxgwTIgjBsGB$uI2OC@D-=C;abs# z;>->1TpZZ^wy3(OqqC@H46J0JR;S`nF~D&PD%x9QY>9gD2UC3X_r=B!?xPk4A&uaI!Nvd^jt}YswZET3)GY~eDx@78{Kl``18*3fDQ)`L_wl!80 ze4}$qwHM%FcP%<`V{zuY?%a1vs>@nud#;BZj-#nZ&B#Pd9q4EtMvSp|=f5fz=lzV> zG~>7cIHkhFroIlN1J`W!G2FAa$<9$q7Cc`bT!Wv0yBx9YDiZRl(%GdElT~oz7 z9Y4RH45w9-pWVjVV1W6+UV4$SGbil5{)i{P$*UpqM~KFq@ML()Qm%Y@Er{&+m!Or? zYRsLs&v?!0035oFAc#iQebE4IX~!* z?*TTmUJngp9c-o>br@2GL>s024vzy%c(;S-vHi_+;?y^!^$;Cn1UQO;;Ke+sHX6|4 zy^BfK*;-iZE&fPj%$f6-GLsqYmlm6FLRw6QqEiy3}a@KwqPC-is39wE5%u! zP)a^z&&hgzgC^?E&`=7-Iqp@?4{L#i0Oeho@9KxPdL8e25$PKCK;Kz$qYL(l@tU*G zcx`g^8JNI$nQU#7e=lr?t+e|P>lo@n5z>p0p3;%eZSi~)7#jAABCQ=>ZpKS8U)tm4 z7QC>VQat$^{c9sTfb{yJHj~RK{pZwz>yz)s!k30amgq^EC{Y{c$wRBH#&LRUxpAA3 zVl^&X8ut!9w9vS7nj0tkcWhsFi|w0UK>sLWo*mb|ocQ*gAK$(UPHG>nWENHdFC8(z zd5|pU@94Q6$9?vji?7?o-9*JS%733MgKo=&MzAq54z%Tq76?g0BFpfh$k-Uz2^MtM z!>i5{HdCRC$Pu|pE+6C~9B9xFeSuiJuA;6^&E@Typ}ESsPS(b5=9gL8*s#{Aa~Bx< zy3k(RHJcz1uNQaaND-*Yu78$C0G4&hBxJ6#)~-wTKNp#cv|q=9I~ONnVOQ}Vu+WLc zxZjRN^mg2d#fj|f-_T5N{zk2Gy6710z#W@E$Pi}l zf5`k%WL@;cz|yTQj+t9W0z?lwC`M$$KgpZF!FOujJ{SAK2G~s3ov?egY85bHBiDH@82uujvX=z2>dJCvWieEE*W|8}1o{W8_YC zUf6uFTn>m=K1`77;gH4)-H)1&TA%M_VT5Kd4@M2|W$v#~IVK_r0WW(+&(7Ny8H*Pp zIs(54*QeAXl=JZ_Ott{3I8ID_-I$cdZ48)u`Nf^nfQjNmK*1Az=K5s(K%wG(Lv~pf z^s>!bj=E-0%ULXD7p9N2(kEEy^(ctx4g9rqZY_IpRyp5Q5i}SP#Kqrc#afH0pYyY5 z7orlIi{As9t1|0VDXxQiKZ0tF4dc^diOjE!-J*3_O-LFGcp~2-rGBr8zF_ZUGXWyl zn8o`Q5*b9AL5Qi1l+Io_ zcQhy5flunz7?|Ts^7k?GREiVh5VuO=n5}uOT?_PoR_8QJ zn`^Pp-6tyKN+0iHVH?O#^0fuR&PdihAP=sopl@k8Y4YQXszJfjExls~i&;8zOpuLY zEK9gJ&J#aEN2pFIzzH2EE;!6Ezr0U+>7pW-t$WE=4rlYtyX7-Xnt)R`40T3Uam`2L z=wb@P!YmpV-Uk|D=p<5bMNClMG4CzUCbA_qvTe;5?#IN$6J0uNBMG5rWtOz{Un@f> zz2|k%&ofbod-;Pb>hjCc^Kg&_b4}pF%P0-NsZ0p|{dB8acc}-b`8gk~Ze^EP{hguu zn_coey99R~Pq4IMkRI`cP|L~6IWV;F^S_{|kEt24d=*dKZdX3puKd~)Dj#I?!=*Mq zT&OA^5MR0I-;o)>AW;=p^s(-j`3a(v@H4s@F+yhx!@e>DO|cmEMw?;(qCN`4UWt$3 zRp{sHBYD+#h0e41B+e$8+!1*-I2L`6(XmQ>I2|9rFd)`T>I3#UG%>71dq~DRZnx-u zt9o#n6Y;=fSh^g;in+7U-XW5MDK0c-v^_2`sne8?IZf)CRA`1tu`(oe8;EF66Cpz;~)D3wP33s6Mv!N2{#A#FaHOuB_8P{juqtGf+%)7?A4xqAKVd zDF}zSnaeJ0NvyEaxWdklE9}EhP*^4kQ_Z(ptVqCahMr5Gy@HxYvofSlDzjYtH5O+r zMY8VL3<<5h1Z!9cN+SXHj8*Qy7S#rSw?gc~rqVS84h-%?2O%13Z=yKt?=tSlVe}}= zo~%1Q)Q#7(HZVK<|8bEgFZSTwgBxHRhI2%wuN^PTJ&0K&iaSVAl(F91Y9ekmgSi4x z$asfky#=A7KSy1#WC$cNtro;A3wkJ}7fk`-y_s|r8RN9C$mu>(O5M1YDTEUe`*n%U%XoMXlrmwqh0B z?S~C`S)4^THoU<6aIc+mul*IUxhh$~7R1A<6u|Op`Q0#N`^%*Gk?~n3#Ai_{$fCZY z_-QfNAFx#*FF-?my;prf*ngWHDLS|Sg!85a(C3GS`tAY^l8P%q=ex6XFL;20nGbHB zuV*a+AO=X@R%|6R(ECUl5z1kTP<@_0xVos{&+54YpO1RN-=z+zp4jgQuF36(&Ey|w zARH5+R|7Uh=sQ~c3o&8A5KJ#=E~>h+*La@_3OPB;+MOu zl@Fo7^0#C(Bq?bf41s8YIdDDL1L-5yT84~=w#l{?z!9eD-#Dk++Z%c+YmuDxG z5E>AkZhjU~)eTrr`HQAM#wtR^h>2@e;EJn&y+c6uPJBJFvA511dr)eRB}=ok`)EPq zo*MBT9)j~pVC|10WyI`ZHa%1W-~&sJ$p7BICpugWWfZp#p8i&m)BBei-vIdQVKWM+ectc$W9#7u7*1 zNco30BX^Uh=Q>aL*q|cAY~1YJj9aEZYs6@ulBE}G%RVSJ-gO_^Dje;F%?y?q-8hbW zyP}tzU@!6-KNMC!+IKT5riV3b`nan3^{jFtBGYRnwmo2p>eWxr|6)d`VWD2R(X*S& zSfpS6qAWc#y6DD1J?9I()r-P)+Om6+qA8OWWpe_5M$kaSz2(Sg=_Smwd= z&DU{n0OMnoAaP(?-vYDxV$rxT9Ml6jv=*tbhWl<1*!p-_YoK7t2e$=|W%}F#(-041 zjjF|2dH`PL1J(t3Sj07T9PZiHDqj;*jM=!_Nf8{=-h5%f=BA51 z@JDUV{1mGJ8}PC?g`AI)(V9=yi)|!A=)n!x1JEkRp=8@(67GhQy&!YHbH3ySIQ(c= z`*%ec4*(_(D)ner>drU*FE6nPqx}EQdTTGTGBsfCLruXyLpc>eg)|%lrqsAos|!)mrq#GRR2O!rad)gP>{#R0stYwy zy_Nmo(??Z(C?A!eDpZJSlYs#1L+PjwHK9J#iu!;7>O<+M4>h4a)Qb9m0qR5Ps1G%v zKGcf(fPq%yKDD~gRpUOTy0BA?yHj=HDK&0ab>XQACIE#`$^T1yYTR9`3p>}iyHppZ z*SOQG3%k^~J69KWt#O}TU6@hh?p9rRT8;a(>cVa{?u_cf(`(#4steDkai3XT*uBQx zy}Izs8uuC1g+1cvdk40!(4XJ|-gq8vQ&^p{TAo@LRu`>y$YUxWljSjmkFDgf6(3XN zF`194^621WYdofe`P7E{0C2@{`j;dS6HKM(+4vW!9|81~CzV$#UQPECpHo~ooc^K>rszlGqTW^~<^}6Y|P4os~ zo4eB~<*XvtYsC%5?Y)ZUZd?x806u%g;bPQ)p~7cWk!!g7*10(};L(C95kJfQH_VuJ z!>zZ?@aApP4MY+#4tw*U4k2>(^6RGGF!hETXHQW{8T#@WH~41%apDa=JrCl9I}=yU z_}hB|tGjsfprp@sNdr!{FE#J~!ydrO&CebY+LZFP4j9yLg5d(;41!m~qH%ks*WrMJ zb5Zd2>{9N!%|`|m4zxI@0d8=Jz8mgenqPlMR|q7$UF*W780n6mij5nyBgX(NpVymR z@Y#dwQ}w*{?%;@ASb+v;J<5>|mG+n(uIr%Z{h)`}W$KO}_3(O^o`?ISaIS29x}Ntv zZ-5Keb<*>`(!*;qaZ!^0y;5Api1fU5#l~)TV0Rn54-0kf#iJdQii1OPqv_4^a30RZ zuXB2EkYku9Sc(bsLv(}BM0iD6@BZidvHka6^toajz)JIkMnHUSF#A7?MjJEXqH&wk zV<397aczdjn3+j?UBdhfezf^#;-7`;MZv3k<<*wvuhHf*_93;2aD9KoO3w_=&JNV! z5Jl^mSL>lm?qTBf16JWG#HKykve}T3_T=hq599F)JZ7s$TqnLKQ$1de#|-s&86I8g zF&~dk_1GUjUYI+*!BOb#+cP|QwZYjq|KTf)mk5$dacx$3b%)ZtcRk@S*kQdVT5rXS3vR9PTW596+ZQ-~rnaz#M8abKe)~NT z9(;4~4B3F&?B{ooFE=X@p>fvrdWc&{fSu5Bk1Xj>;qf-TmB2ylVCZ>#a3dK-;aW;~ zmeVu@SY>q{htzL`2v}ML;5icSjQ-D+bmUs7-=3@$Fmk zkkvk%j9f?4AjIv|8C?_FcLw+Ro6x}bQ6)DwqV0&4efR5ja|hl5!%r|w5(H8<>=kC8 zGzCa2HZwkutlk4L-0^iK=T@_yBH9$?A27FR7H(*u=QeT;0QIDJ-*CQ`N+G^NAre2F zp{ScDM6&z~gi1XsHZ6&-343sxZB*UbL)hT6NOTl7MzxJ?(d7iwWd#-`wT*x76z9Nd2|s>b$8ZBzJrr@P`XM-ny#XYS<&VwQV_H2!9A zAOm2~yB5;nEvQL6F;o1~aK7i@XZJDN1r#Dy7zYgWIfnfAA@^-j|(;M*45RzXzRe#o7?^~^RcoPE|WCRcu zu20Dta8je$EGubtHH8)1zh+=NZ^|55h;%zeeU z81IzGVAP-*n71`@DP9!U#bT+nQbn>?I4nOuHP-E%c23W0lFoPuC2|;C)kcx+G3&h; z@4U)as|;FSPHmE?C->S!-M$h=nk4fr$>^J`Vjwoox;8fQR)gFu!iSgv+s%)UOZI}; zz=>$CAckh3n|K9%(554!UTD5A`s#U92RjsNL4_!y2RWxh8 zBmvPBc;9CzZF}gTW9Q6;i)y{Pqd80JN#*-^yi@f6@kVJg4eKse8zw&sSnT zTi7njPeY2VD8E{deQdI<=7c`~ghCfB_v-*7U+MBLWfv(b!gR<^EvCof2vF2HR0@a& zZfD_34cElqrB@asvnGjhoqHp(JILk&!LC_;vt;iPmgg`sOGgUJ9fE>+=XlOJRn`5N z*uWXG+@NYn1q*~tg$*p%^83W2n%n4cQ{c5G*V~2a@_y9E8^zk>8h*;3!At*Q9t7po zK;~787!xCVuq>r*Te2SePOwno3>ZnlP<-}bs8G8LUFHqgV4u6iWglE8%Lc>XCXv>z zzqwTT9C>)32{&7)lk|K+Td|EF{{Wi{)Qqke*^&8 zeSIF>VYq$=ZLme{xLw-fczvyx_h_8wdU&1{!6&9JGP;uvrHGP9K5^C%E^};F)K|^9 zLW&&Bl_GC^7jE1$FrraUpb*)+^f9g#fDSnjn!PLJ#3T3FVuVb;L^*ZhQk~ex#Po_$ zV`CCy^~Dy7N2rl2Zo_sjr@U2joz$h14rkI*COs!6>3SyJgr3a*L@{2YuEQu5_($(y z8{TZRvJ?#mQo6zOiA{B50l z1(VNb@_8}I2^z{may0=BBKw!zp|d>6zM-?kBJpq%h-%-^(=U{Tg&GL0=^32!LuK z;-Ubhz6%&B{bkd8iY@)D^+`wcQjl`DuLAqK(VEhM8Ox5&513&nC%fSYtVZMCF{Jxz znBLM;&^mvwbyTevQi2RXh$uPu;{RtcesjN=eyQ@%ts;=}em^Pg-Co=R}Qj-`l`Bhd6U$ zaaIC0#8Y!EN%rKRZQ9F@AY6TSxlq+rV|bAmN{QhxFh{AXAclkZ$@i^y`&UqU-2S(g z;?Yo>ZvP|85CFREr4_(n_=oatO?&B5`52DHlP6vU02gJ`v9RRBUPXz&74_(X*KQ!~ znZB%pptGFoVro(+PzJSX2-H)6GBKDr<@x6o6Q3Wbi4(OK89he!Aq`>r1YNRWK#KbP zh?iwNi}299z@&c4m119{Bj04?JSK`DQMA}JAt_QH2`~0?mC`}yVjSv=JkfK(uVd&` zkmnhKL(LJj&C@cn4WBb5TxS0<#es5~@SPFP;&|Fes>61KV!lQuL8cBl4pq(7YT&>g zxZyZi_NZ!>cO3SRcMTg%oWs6YOc^;DseHa^$}~O}Nu}yyjqD?3exw6;!39bYHrl+=|KJ%)5 zPke)~JYKiM)u43Kl&C|M;y6Y1v()E$cW&){6AN;Go_Q0`zBX7o2RmC-78bM3gKX-^ zp3!o6GP-V4nwVGBybApCjoNJNb_#}QGd~YX#by;!7UDogbOtxCDsGBL0_46Xpeayz`S1<9Xb|asJBieMfDac z$Ca%1KJ_cjih@nt&=OpUm2#U}^v6%MeC(y{Q(^s>f^09P3W&CFFFwph*|?E|KxyQj zNpzmYjV9S<19!7L-VUe?G=^a=xM7=oLPN1I(W1g+AaRVwden}hmIc0^8R4nfyTTZO zrQHKu*iGszA4y!Vq|qkIgNQ)|J3tSp(GW5hZ;zafdXpH10_@i(OBLAO5V4mMw%m*y+Jo`M1q-`p_ zugLSnERnnF?Mb$k-!eR>A9_&pKS5|HB0ga z7^W@TXaOo!k|zGjONB9ICqTR?!gFJYrz#9x5wnt{EdFm6o~OAgXmb{!4l#TxNf7YmK|5^*vSzX94U8iPc92n`lAEyeqE zhq_TGTc(p;&SV{!tVkyldTs|O#mosB8!V2{1i%?+U&|29RnS3no}eOi=dNKbPS1vi zq?C5B#{SI-VDCcgXT%Ui(9i@Bg&v>*`+w~GXa!_geVEKX?!bjrk38#YRN=^h}DnkpEt?ueQy_R_;T zj?K+p8o~$8N&Z2?XtfYny>t2TNQjefYH60Rc~=i1u;>pEm`ehegK$kCCv!>Q9FQD( zMev~L77Kw*^N*PgfHdiP0L4u#fe{~{))Fx7m}W>xZr+A;zIiqH4gJa`KthKsXCAN0c1R!juoFRBe(%r9y7p zbQ@q3Vjpo03WOq;tY`5!fuojRLPmD^t-I9oB}Zv0?9`fd19)sTbBmjQg;=Nukp^~! z_je6k1YBw1E$x2KcR1l*zY(BT5@z&yvl`HVcpAH-vW1qG~kEL zA&?FYxs&A}QI`LYx4z|R;jyojgvHT z1b;lLsSA#!N%bCW4-jn2ti;d-EJP89kQq~PR0`EL#C9$=7+tfvIZj;B1AwY|>}D8I zHLn71;FmTj9+sg@mCWie6ey|@m)ZueILUyE8J?>P(7WBimW(BiI(0wd5!zo5fME`1 z%owiHdPpUXxj-uM>>5;J9xL%;RIYAn>yV61?KrC&JJWv?t(bNJQZT3TYYBc+OV&A! zD_I*FfxNVFC40`P_0S%vAk^ox5hV*$?;JP9-8@i)M|%8cUFV(6>+7Ea8ketH#v`+_ z86fSPhqThmR8O9z^_|#=qVKqiFeB$web?U7cP-iS&4UISbY67K;)&ICqD0_+e3kmH zyRfZmH(}dFCFr?s1zEVKYrT$1(lM!wxt`_z=AwpHQH21q^i?t+jdmHnsX04=H&7ZS z>&Yo9&Y6O2;K~fi=Fmav!g{SZ7slYT6z3S=Q$vsj?eU3XFa^Lj1C<&N;0DDR_QD=D z_sitZ($#@B&`mhnLbXF%1y}QtX|#gTf#HbvS;-AsUo8EaU?Q+~Y;^dCy z`ho^McZ$xwAO=))BJ(#vsp^3B$H^M>ycke6Ly~%h30j~*SDRRm*ma>uz%moGy$O1r z1}!r|KZyaAo)K6Tx?>6(r_ew^8x!=o7|`V=*3~ActY!z&el=;G5d%uiKw2L&L7&rD zU&ZtdDqdY>g0}NNvEGC%PhKW!cPJ>+xu&?@Pu&+!oq!_&F>z_r^(o%oFuF z9<$_M=MFU2c0~`H3W}WNXP_$89SLX#;1-(2Ms*1qkRj*I;11Y8T6e%o+c<}iHz$hn z)99@w5)=QA9w{BC{jB!&4sk*J(oGzfP;jJubpj%5YZ=;?LH4|;5s1EXqhl{dAl9ae z4pVvRwJ9SoI@vX`zF^`e!~X~c2!>G6Id05uLObSV450CcfdkHfrqb!X_DCe$`Ojj% zSaSr27s0C(J+bsX*mYp81>Fxyv?)stQu?r7qN&45G6)P@jULTM0+0`BNDR&EaX1pX z`r^Y8bHCx2(}f*QYCx95l}gT}s^M+mTM*2yK-ayb!jh8cTnR_moGmdO$^NR5puh3X zPPRduvGDAj-O1*`*^LiOwZX*Wl!#}d;rEjm<$QH7t}K_3H?g87jiwahpEd?+F(9l9hzRD&RUb@{>I%)GVBkz(qlor$6sjXP-|oOu57+ zeapo&(NXna(%u=6=3O83CGLCiLz#&nEbLHl))A5XRso;|{M!2!`;`V8{*o=mrwJ}xf~PrL9afTRN?8Bet1BFuzsU1_TYeZ`#*M+QpR zRF$F2aHyjcyLbZ(o-CGO0N8;-jr~2@rhCeF5E{Nj{oYl|yg|bk68E2A=d5X%V}B|d|N~P@3Gk$#KrRGLIE)T)lf1o^l|oKXP{&PrfR)M<7Zrq z_i_yS@ji19s^S>Hjfn{**i#p&S=)+Vfs4n#pJWS3{}@Q&K>}9m3 z*0#_I>sBEA8Vk+&gvztN()rcT`VtJ8*4GZEJIng|f?utzub1%!B~D+J&ELsQA{TM+ z2a^_U5>trvs@F~lZf`>U2B=2966vN#*px*MBwB?3&*J|Z_;1Jm(fB_J|L?>97zKrcVyj9n*%D<{dY+%XCE#Absa2EK{qUwWlKe` zx0fEe@o~`x&JMlzsUs05`Ukvj#4CEsK73!{@D6T*IE3p z_Q%e5(A-bkA7h!09d$f1F6DXn!neqWy6MMGa@JHSvAOud}SLa(=b8 zz82yOlEVHtS1ahI{qgf#g-2F@JP}Z*m5~3*{y0`&&tfb12s0*V1@P)E@NLk}cXWKJ zAy52?i$fgX)X2m&j}-o9Fic_J#*KLy_h3FFOF{Un7@lzt!-MLEXZWnbl$YK?ZSdt? z;=-~?7F`I&uq91?o?w_g=f@cN5bL`@UMm?o*aYG9x9V$tU`DB4Wttxjew@t@G~K(1 z4oedCJnwp5KiSQzS7S>~M9ziIdx$2gdgBbSBaI}nJ9mExJMt=quZO>iWp2Yv!IBd6 zq?)6l&(hFu6FM7_m|vQtV{AI+Nyc13JP*d;;jC+@6Y=!Z(A%0zuD20-2s7EwS!Hy? zINEGJILbM~dvpUwK~$kxiUL3d5`IAf+i9RKGNI^u$Ca&)!UH*j-EKzRA=&WqAt=)< zdcuJ3bh_su>=@bKIz*1(E7cqOLh9kS_C$P@-pVAqYX^RV$K%}-!_YD9NIzHYAu=|# z^>^XCy_Bwb*n*GI4}Gw(wDigc9P8NU-vk8@YQ#-Osgl0BQVaocPY!e7l-*gapo|PbyW&a$KRm||; z;ZkAAGlxV>UC|M<|Bo^DWu0oUPPLQX0V5F}zELgIF_}7MEn~(oW{A2^$F$KgFEQp8 z;#v@sQ>rc57VfTwY0y3KLe%{Py^)~fV?dh<lFaQP_YINP7>^uu`0IY^YlN8@XJs zdj1XP$Kr|e(=bQN0~jztJ98qYK8V^;vsi#vMA}X3Q5|hAlJ&owI4QHu%?I zKOFi;=s%PK`j1{{Xcb%3xH~vaL1|XM4kQtH4XRP{Dt;$*>ayDzj;Vo=`lKIUMPc3i zq=aG}dJS8?bIlH*clo}Nb)75NMyLz>;zhRMvvgNH%_Q||^xyfE1;Tpe!UyL`7%#6N zpkDPR2#r0Nw)Iu2b!enezG@|&qHgQZR)_=(NdWsz+LH(BR%8(plmm8ipf}c>q1Yqv zH*oRt1{58wEGOLM6zCo%G@Y2l`HMa}=_na13vM@Tg${b#4!%;i!DmgtbRA#dQ;h91 zRL2%uu_EX=3Qg&jlkkp?=Sn{7+w+FB#j-oazB+)T_1wN6B`i@im+xm>R%b62zu%W$h3Y^m^guhsRgc#+FEdl$E2(iT4)$MhC0pHk zl}0HxV})}@lpxuz}R?WJ6N zQ!n8VRH?RzFqR@G$FlBFA3we~65?bMJf}uG*ZhDtNLcnRk+E=|JRpG1H3twA?xVk1 zQZwYdHEjM;cITSk36ttv^9P;{;p)j$X*a#~9Mp$}$?y+6sNQbNmMJq(zJqM4#qBUR z1NOZ7I`!~QwM?G%YCcZ4re<_xQHmAX<Ek1;z@z;kaV^YfaQhDO8tVPAbqMgX$l-Y)Ms= zSOZBL9DCWI?wdlmpn44qhDE~yMSAI`&#kW^z{>?LHB1Ok$sB5gfVA;C_0m&j8?94+ z!>d#snM!SePTUZ0HnEp*p0B#_WlEoPH0#urW-s$Rz*UMFwM55c>zEOY8Niq!>H!^dhK}jW7&Z&_xE<0F(esm&kr~u}p$7dK9xg!N zlAw76of-pbtwi^M$ib9QtuG<>>r@^N+9r#{z3oZ#7BbM#K4CXFZ*+3)QvI4D^!s%N z|HN2w<&DfBDJBDoRrv3$;O9`mno0E7k>i|E$#6=f6X)W5^2r0^lD=zZuxP3UPKoJf zag{92LE9yFZ+cVW45>^Sntxe=sc9ghe{ZNKBvxIeY5WOZG+;s^X`D(LCxST^zt~NN z4r7F>x@hPRH1t;(!>zmv(4p=k{O0&7i;{Bw4W`(C41quSZB*b1FUCZum>E3QT9|u@CdF`JR{0$g{u)TM z2p8rW>Q)UJuR*&Il%rBLS3tvE)KCrZ!*OB49|i!{5a1>NXpymOlAd78Diju!*DL!% zEeyk6(d|VRu@_+7*4z>x>s9Et*_c&3ghLPHaW^7%+iFel<2O;pps}zoMBC2YGfc+Cxa?EdQ18llE)YwKf$>j#{?>ApmsHV#*(9EgDfX(_GOi z{Vu9-6r8s?H2AHEz(e03!)j1f_^)baODSo4O=Kq$shH`bAl@nuMI*#nVAIetAJO@N ziDRX@FpXYPKNE9oMOTQ2t?Iu?hPj-G_~dy`l*r)OoLNzS$Cd3M50jUi^BE7=CSzc| zirA1R)-!TgIlrT?kX`~l)75A(pL`t^4A=l*pF5CJ{Y;V#Q%w+pOjsxn4bf|>>F`(W zWK3*f_;@wX8-QVxd3S($*LL_)HMNDXW7RKG@f)~gGn{!AgH5_}tr{E7+C?2?u?~VD zNl$&o5FIG*O||fVao|J=PyeX(h^|aF$BEPS zzCNN?&t^~1av27nL2nR)&rSFZ*2fzYcgm((L|*&?v%z{fp6uSb$Tx=qQ4g2K1UItx zkFuCwj~1c}s5eZ9Es;b{deHiWu>6DjuQ=&}lh>GJ=D8Sra2&8xeUr^9fwd4+PQ22( zKxn@SraACoMllkQ!ae2w z&E4kvEWB^WI}USONOW)>oJFeNM9;wI2HK;@Ym?+vuii7i%gt}GR}ufJ`Mt!9Uv7Sv z@tedyCB$pjA~_L~Q$Q08xWq{@0x(SjUGCja*8v>#2&sJo7}Sit%)SafIYJ%7-(>hr z68>PuR0x#4v?t~)C6n{<;`W7EWDIvoGuYv}{RsLVJH}tbV3B^tW>H{PLzlY!lQL!% zz-D#dpqFb)1O+z5LZ>@Ei|69-a6c~&CsMNBcF(=8zS{*gCskW3z^JeZy= zKP%SUfyXHWoG2&xb77{fV8nha6U2-qDzcTRmJwBbjz+bVtVE#j9CS>DTZMs*31lQr z2~gP>;~>lVZ9p83a&zIt6cYt(9s;DA0KX7XAnmv!*LlU5vwSl%vh6V|t&3nlhftbP zfe7-|p?}`gDKyd@vRx(;`s(YKi*fmu4pT{LAuqem+>fFZH|-M6$~753EDV` z-a_ucn-k|{79)a9V*;844fIRT(-TLd1w^K0rV;^bKs|3p=30@9rO+5ss)x;}b95A{ zBbw??Gb-LnWzO_WI$viMjwcD`>49i))k2a!W1S6j2eVFkaG%yhTBE2V4?vS6QX#T{ zEoHx8$K4tz-*sbw2_oI-rn_dNMU1~)PC=DB0Uk^#9%rBamXai9tRKi!E!6~YR!gaOy zepCTIYKZP(egyh1KsF*t=y7?9z4})T2hNyyKmdox>RAcE)F=X)Ga!Hf_95NQR5HZw z@-V+c>Wg$eE%qs%%%&FE(8jp25Kqj=81P;^im*oGaP-IO4Vy40vR6v{%X*Y}IZ0d( zPQ)^YC0;}>Sd29%iSFc-D|DcgnkQ9G4bd-Fjna5u(Rh~-FQ-@4oxrPYA`W_p^>Sj> zm4zyVa*+N2c*8E$Mq{~IV;Mm#?-R=fQ7o1S{7!mLhsC50gmK<@Oz0L4vsOgFra(#y zjrT|;@NOX9Eyxe5(ki%x{7E7|UBQX4EfG%E2+Q{f8(${EkBRUvjS-5Ny{RtK&0!*f(?4LV+f77%m~K{E)d%d6RG&7X!5Vfc>A43*S}6q_oy80kV2 zu`;UOKPkm;sgWLC2c)}*^fP2;4uCNs>bcZsiY{pu5ddSVd3>Ny(KC7E+qaG z9HQ0K7y+Y({;RxOId}zc(){orJ)j*Zw@Wwb#xBo<2lpbtrY-kJch2l`#_Z<-6 zB9wme5ftz1nxh8pj&xzdJsQ?SL#E)vPX`b@E&^#&#nh%0zXvX?K zq%>|Fb2($`S(>&@OJh~x4pw)xSv8Ot`UVx(*gTf~aMAfS=}fpDhLKYN7r@~G`YG(; zM?a0)!|Z=(H*X>ZpIv~Av@yK*Hx?fDu0DByI_vwaUo3x)<|FmQ8Z_70c*XP_hUIqx zg?Ru$IbW&2VX>mSdQZNfWuoB|qHklOZw3{NE&5rA(|zoZaHl-`O9M}sfYgl+yuK4r z&tm|JLEo-NlAwn68tA%MhV0FBq?p$i3L4|gdkz1r)V$owgcN%jmwZ^FMSRMr@J#lM z#BJoBW%qjU;#_+NVpqv***#HW{yX%_TC{WRBqqNUn6zZh(vJ!HF~$^EXvzu_RT%E+ zTsxI1SkV6ric+f8$3%8gcZ*cH*CyF~U#lI*_iCN2&w@~|Qov5-8Eogf$DpKvzFWUT z#|484vt{U*0?1kYzLRONeMy2IVbJ$x5N759#mlzT7Bk3D8E3KD>(vd#Bq;6L0IUck zpQWz9c%SraL)z)ylLE$`>~R-1!azvSk?lZGqmIn*^Q^?ujQNOg`)^I=MB<*m?B(t~ z&UMquxrcTWJG5TH<8y@a2JHBO){Y*wN zf@SoEnUi(S@)0cCwy!ig5IGHpj3Ztjuq&e&x~r+>0H*&sMH;WR&oCNr}1kYkoWM^SOK zQGwjCavU9NM*dI;f)Y{We?}@Cs1Eoc+%;!qB%lEqrXOyF1E(0p8 zAQ7qvPz2~nED%L$aiGk>rLr7uzg{Z9SP8a991UwSR{dawMd)HHRK1_qV-SU`ow!B0 zY_l7OF;f??6e2mOXXfL104|uxcJD64p&w{^J=__%GA+f{t8*J%XrF?Cw0S37hUy4;s3~6b&PvE@vG<9i5JxEB?G3L0Un8oj4MjbnDCc^Ra z2-oS3068(z<=w<6%geu-i8>;jtrLT3eImOxCRGEm=FgOUP*2XaQ-E>RLjVpvj%-#9 z=G#=fIoHx?UUe0|PphIr;{ZffV+W{l02BXmiWQM=m<-O7t@VOlk2up^U}2S(-r|VZt$6!hfC=Il|?8RKh+3HLmPmN z1SlFm_|zBHRWvJlJ$%NW*-Nhj7o2jx+jo$(=yh_C#6%lz zlx~j8r127Sms}GBWv?Jvs4cKq-3*(5F{H=vCDRfBXCXr{gUtO5{e~c_`+Ay7vD+W> z&?PvJg1fD}M=!#~Pq;fed;qsT;TTnGlS1r#YVBabg85XQpAc{+!^hP>=4jb2q(2^q z4X54;Ngs|kU?~K~O8f&yF<05j8)?b-O2!{%d~edi^kbQRkoS0|2U8M~KQ{sJT)SM( za$d;!&hy~%j?LJR2*k8O$H8-AY9n94K`;qlkuNbVDoYxc^KFAsM%@K}Ax-(8mj6ue z(Uw;JJVO@QG)JCBq{iZdg^~Oisi(xhcUtVgf(fj;4=Q7(#G}h3;bX^8!?IR{z9Fct z-*OZ)%9v?BpBf=kcfIlcI48E@1kx^nQR*zenKb{fb#LUzm%#()vYYR_8b^-+UM9Xb zA!F0$xAEa+AhO(G+M${>mv5`fx5-rSIS}oPbkLPWmSFT2SMe)NhxhF*Y%0IyVcDW% zG%c^`FL}rzW#u`5srHB3$5HcPz5~2kkXn2!$uqe4SaW;XaUkZp6db=Vw-rrNkJh16 z45YN)A=c8IWK8V8j#%}|K^Z5t!28OX_zBVP8!H*!%j1eJQDb$gFTavh22=f;sqT$Q zHH)cA@#FOMDxMYBtmqnlVY1Vg>Mv~V^tE;Rvr0~0J<{20uBYVWH6xwbp4KHN^G1%$ zwwIj)@<^S%EI^#U)Cz^S>%6{6LSCN`^;nL$=rT;#`&sL-VQ5>2 zkWT%fPJIbe%a~@8nd4ssvRBcUQVCK@nW;(A=}L9F!_ARy5AdpUV+!IW8NFESoxciC zMr-Izg#Mb)b-=t9 zbcPR;k8whB=mP{4o`Xwl5z{K18Mhfj?c6QX&t3BXK`tc7<$-bbU^pSr`RPFGt287wV1NLm zOj4U*(1sJ$%>bunffnq$=5D5?&!0N$fK=a;pNok7?XMW398-N!CzzF?$m)g41d0R{ ziX2Qib09Gt#i|d_^l8MgM(aIOkzDN`Aa#Ljwtd^NQswq%tM_*uqWLfOHm_;lpo)BY z9rQK~XPy3zRD1^mG)X;oSK) z3$vLRvC4cnR))Xe#EHR4`*uID!lWWG2or<=lG~<^(Tv7A2z=-0K08$)?WG$5JIgvMwbB;taBO{g2Av@8g$6(J;rk{C-lIWe<;VuO z`CvIZq%OQzj~(lPLXV3Xi(_Dqs)c4$2vKg|WLs!5aI*2DW`5y<1nN7a5ZNd!#Bw%( zNVcJM0;LlPqjfMXjwlN*)}%`eW~XppANY}_PeFg~sXY<(VX2hOa~mi3$KQp<4ka^Q z{NKy<;EJ5?sgDdTQwf)e6lyP^-R04qvB3d|{ZS^xid%7D z&tAqE6Y=Y*cls*SkoSM$dcD}VfwUGVjkElAjB-`WD)<2%S&NcK?0F}{iSib;ZGY|u zDPvERqdOwY|HOlEj$p(;am1g}WA5g;;It$yUlB{vj8CH^&D{8(NE#bUlC}T6uO`PI z$Ikp*@3HnXZ&u6pNzHWuVl3>vZ~`@V2qg}8@g6&4WdJ!KbIp@^9x4h^UuDa6XF3j^glKs6U&KL8pbef2kIA3XjAA4){C%HZK^fXdFF?8k(2>!Grvne^ zTCgL5!yy=0P#|n-5U)HrPoJ94vQ$1LUdgXe+bNCoCtY;Zl{#zjN5OohIpyA2vajKs z#Im=%lw_jcd))JnJvz%H4Vsw&Nsa%(jTVEc2 z`=>}=ZjHU{Z-}O2t(f>gAOC04?$YV?c$&SZq?!k|1shs+hxr~rPG@u zkn;DBM8H%cteB^ndk!krs=x;U&jKW%QbYH$diT>yA&_#bPW28`y~I?_Vp7ri32#zs zHRu2h`Y=IXBj{JixM>D2P@1H>qvt$(XijmaW6_$5W2)~UT-DXxyv~Z%%WhF-xS#p` zkQ~%7Yerx=4yH`2-pp3YX=QbDP?+eMN^Oie%`D==Oi(7DRO9rA$&`s&G7Efin@rBl8C)@gmBoD-pxRi%kR{jkpal{8S6vPsi$pN_)!;uUHw9|fKB(g6_Qe%%H_HrG>`5SNF-i)mRNwyK{!a5pBu}XthJozHVXbiS3z83O z?GDWBgg`X!jl=A=`QuZeTJhbM-|cPlM|7BfPul!p9p_(%Yqp?Vxk!X{p*Nrgc(Qvk z8Ye6=?X?LRmSvmGs@Fqu(ZnNf0gOj!+5lz*hRFiYRMBBrmm}9cV6iZODQu*pwIu>8 zQ8>dJV&p`wNA8zg-_1xRI#2_xbpy1P+0jgkQCX%N=|w0jnahcH@c&pp$c=9&M!~A~ zd{HPIe>gb)$g!86B`n_H^qz<-Y76~BbuZgAI8TLiu&gSOBP*;y`-p2hTq$|FuzH}a zZXVoB!%|Mu%{*flTP_@7ycJk|9|Z*tilx~IFm1d_)Vy1ImI>i$y^dT^ITQ z@oL7mR2nJ2Rf;Oz+X`1d9H%%v@?ETr8{)ndv6Vyfh?V^YN6fUMs%&EzPxidBv-I+(qUZl3P#m>PoKovI$1z6T9E-Tp`s zYEF45Pl1%Wpp^3AgD@o)Lw40dlwOx|j#xFZG5@pB>eAY!)C(z7;WfoN1H-K;i7qUDCr+nT%2j!EZ zyni2Dsy8?eZ%8V-f*}*sO9&ab45*4OX22M=7y%HG+)Z;k=W`w_hs_-^h3bC9tl?yI zuA0+Z-TAffAygrJ==dA>P(?nJfckBl9S^|fcNc!4F-IX!vo0K@*Uk)mx$N+N!H}f_mZJrf%lBb@oR)pv?t}?7@=> zByaI#^0~XEz zn72TZh#LX=j{Lx|=Dw}LP^#~%;IEz7RsNFds|I~AKub2e@?6ULtD?ElH!R)+``*Pm7Y47O_7{#Wzj zA?<*)Q@w{*t^u0O1{Et(u~XgfnXbVx@XIzgNYu|oQwWUiAZ;ff{gy(2qZEzebRs;EY_f~G{_6iN3Tbdqv!X#ErJrRfqzmqbG;j27#2$}Y{02`#P@r>J2cUh{ z9CGJ7SD@#)3`UXmJIgzO_v$9ZVEz7FEO<-xp#R*VTm+BfV50+SiO0qXORY2)A$k>4 zhf^7YDiCM*_Dn_IOtH_X_{}~yuM5>{msBcl!f|3Bd+gC{LYvtUPya#`U+pSUe8232 z;>)AZ`@gh00C7q&;#RpF`&MKoIT%p7UaxN1 z55oXH4(s(I_6@Q{Q`W0$fK5$5Ukc)u>neu7WOm4=!}`2$u*i%KFB6d3x#?_b8SG|% zW+|!#4Jec9b@#up_FSnpA*^iI>`rF2Z1%KK-Tn@Ty(j{Ju78#b&hTeK;0+op8ti7G zhsm5y?18kWZh*0aHi?D_C{ktBn)3T_~Zk(X|!i)-rbjM5&o=gCV|2py|Y8VCU z|IEHfxCLfm6J^EG@Glgfe_Bq{&O-;s!DtQAnG0d2|AURHu(m7E$<-mSi9UWIS_MY| zuHtveIzQMofFh4)4xb`F_R@n)U-&eb3@WJ7mm@%~OQr~%0h%QAZv+cH!Im^S8Pu9X zfLs3o(b%9;pM(WRE$5h#S-f zBu1D2$UZ`4>I*WGEOWS1VfJydxHT!95k~$T9 z3Y>(vQCui)ap@4Z9S}DPm)PmDkyN=%_`d>P_7jIUt9+gF*Bd0~@9#&>PXJB0l=GjW z?L-KNp9|EMgm!-}DxoZ?x_O64s40kHH}#Aps8yeBYkN;*}RfN6=ureCyuN~Fl4n0S4n)2AzxNZ}wPpcFC_&Y))H#Xp$Q z#}&8^R!p6WvuKb3!*(tZvsHN4A-10Ld4(jRQq{}SG43Dz8)s3E+spa^U5x16X^LL> zIxRM0bKa?*U!`RS$^mmAC=_MKLD4Iu=oK@hJG=rLTiyeI0suV>yI z--jQH_PJ5{%Op)(IwKd#pPqQfU<14(bKl`Zh%QOJXQ4uMDB(MKjWGz*1EY?r7r`&3G_y@$9h8+Grrc!_#?M}Y_i`u zpnx-#n3brBbL+EvjgsJEK#GdQlxaE+P%Q9OJ0sh}PIurbMj8HW6ZX|+I1U+eGw}Sl z!GcuVhSx^I=*F7__rJ1M-_i*Jab>WqN5K9t&-7h=wlDS5kM&J?6h32@Q zg?X^?X0aLP>5ip=YpUN3wGs3hdA^}~hb$$l7qYO3Fc)V~O=kY@on;D!nSbD!Q}TEF z&+!i(AXR3PV$#7%$oX?xUc2U0T3$wWr_w^HG3n{ngq{;m|8Jw`qH(9v)A3K~X|#Sd z23%JQ&J)g-Pprny3?3iksNdV3Bq{LTHtt=6C^ z3jP#N$Q%HQH0pFQq|uGw$I-%cTmd2iDW{%1*Qr4W8XtVppgt^lNCT`WE>k{#dFm8- zKs!Nm-Ha{wfqA81pF8j@c^)kq7)RBO#zI0_wpi`)`FLoL0Z7B7r)_G|V@gxQQYeP? z!Wpfl)V%9Ym8o@SG!(C)y=i@H-p4WsASjia2OvPle}%!1JMaOic9qoQ*J0-^&=@el zz2jVTnl9K^PVirP{!bAjYNjwsfkwNx(yyYf_j-t0*{^lkHhl9vjS-#I0ILb(jQRlsfasB=@?wjsMgfJTQDToqXlTFY{emB-3D41F|M?_kNSdr6 z7lD?cps0%i-+C-dpHdfWoDN*PT#x4MMsv-xpPMKBRUQaut;ALaNt71B{T#YlFmZ#f zl%;s}S}iS3wubH}U2%UHamS6^lZJgT=9~@}y)FZ4>g+in)+!ZU|4zBiFJ?cRS-x-w zt9+ZE_&-#>a`%6?e9wZ||3>+wzc#>sm7+_hm2Y&hZ1<9ANnt$3HlX_^I{iaYU6V2f z;0hmIUjrHhjn#zKJjtx07>PZo4vvyi43JuqM$phC*g()SsAmVwd_y!N^8vP|$fLu( zn6}Ad1JvWO=rOW$u)ZH@-@p&K`P!UiXi$g^_KWc2L_4iPFp$$`$6Q!van$<>*%jg0 z+|t0d?Z%t{jb7g5unf<^*yCfvB+_n|?+;ft#C;>=(8xaNZn4kE#x!4LT!HT!m^E(S zZsW*T?yf%>Eshw$$RK=lJ#KX00nZXy9^{THEEHm7AS#i!JESZ0kG4gywoY4shW3ba zjgq0$)a6zY-pDAHfk9&GVzW9+GdT3Z0uMHHV!FnW{R{jfxPbEdWPRWLbBy8q@lw1<2K1M} zXt+7Yggoa_cDxT*28$i@#2mZbO#tdLkbzeK>yh-49o`yD*<>WRO4b)6+!c6}e1Ibw zmeE|@e=UHXfHukAMi-4$7*e`&;k-d2vB;REuN3DcnYq70qx@QcGybqC2Mfs9$iK+# zi@3Alw|LYzQjN<-BSzOMR;(Lm{AKUBD$qH{jqzuTN_X~gBY1p}(W2hyx^swqMM7@+ zCTDU&AEWCLSBsOf)fs!wvK3}u!Dx5(ext=YBm1BcJT}Pa`lZWTg|jO*M2h~(x*<#R z>?G1=2jsrmBJ(gK+lJY~+fvJ(w7Q)a?KQnI*#f)?mF_7eZ@@? zeBtmCw|&?N`-)x0k?n?k$hYpUzqz3oycNxW>Ld@@&0Sx#;V?{LXd_*A5A3Cl#*r`G zaR;0W`eZq>P67*d6C$kIsM^S!4g1hqqw8|BIqBe9^4(`;#umS*{?fnZ<~#=bh_%Y3oFn55n1ZpuTJtMQ-y1Aqe$DC z=|-FTYEh;)_7QZuu)LObq9<&%|v0Io^YIU+{TId8S%8q`sHrYUQuIkY5J`QSZz zo?Lq*h1!vOnzz_fo7vla;OI20V|e;uXq2VS z(?n&d*+S(|a@cb=$;9Q1nKGlq;ZqCc>U@CHRJ+W@j>a2K^!_>nj8G*wxegQ+pk338 zlQ#*EUf5uW$#R8o8U~4d6NpVV?AQalj-5ow4|2wv{l!gjM%-p2dn+NZ!;SzL#*yA_ zpM!Upf&!HgJ)B2`X{cKDGwKr|xYh(m8M59ot$M3hwL0!R9{!b?T7(e<&Qk^!dunt0 zb!;DPo6DL)xFuuSA%+!bnqKWu8kxlsvn9I8ra@nr_O%}_D69v6;%#`f#t#_l%aT#) zd~im_5auM3w<6+mVq6IaN_eKT1PcIx65PUOggt-5Qj6grHy1y7bFsHS9h$9=TvQLO zlSzRS|9@G{7k`c2BHS4OOK0%`i}m1R+{FHzj%ragECzs4?~G%2GYyac5Q$?8f7%_l2y2~sUYSU_$b#n6z5`|VEg${MW<_xs%j18h$h`>n=>pOza zFzaD>Vpt_v{S`qQ^Ezi08~!ASHKc7*gTNUSHB;&!aEOA;;l@=896+;4L^h@fqC{XP zlbQ<=K~-JUNY1!SsLCV;&UaeY?2CixL3EOk(mM6QRyKOB$)d%7m?SM8JyM}*V^}kP zx}jVAA6Y(z?7tY`TDn^KqAbUR(f~5>9YBa|q2HnBkCrW*CzHyFa0f)lpd7j9;&xuj zkxa)p*iEKe!o+^W^bMgjo%}|mX}l#(S%{R$r0>+Mi5=tlBgWpo{^aQyq4UiYrPf@NFvaZB;Lgt&4Ty8~Q(;gADo5)yPfvk-}8D)y2W{$>ATJ2SY*M^DYYL>qucI$m)4?%9&=% zOl9mv$WL;sz!(g|fJG28*8Gd<&)mNE_kA#rc$xS!6_ zaSt*MO+(@|8`JJf(Qy+P2Tg*wqFEqs9PWu=UIxRVK^Z;);rmSZEQX^4VmLe&(oUN2 zofw|1!_lXtJqE8_;;}ItrUl_UA^b{{CbbvgFsBgiNi~As!?!ST)X*fGKOqiwC1o(C zeV3;LP+_|kE6ZCSR|K!6ULTBLpraR8znfc)sUguPQ=<2A8{VH&-M$VP90Dd6^xox0 zch$+=qZ&is?UUX9&fTb13i@xK-f@bv!@(z;Q+X-7?UPTq3V^gf=X`*#4|piC_N6XA z?G6{(+n&P*DiC5Dz1*RM3Eh^A{6Tb%l+S=s|_9ebfuK++g@cu8vR3(-n zq(y?QqAGDN-e?k5CHl;_a#o9RM+Fe3lrUvgf_M@(g!@y1#YF5*NVftpa7cO43Pi%G z&&Ju*14QIq&kolya`bg55?cJuNZNF|T>1Y^h21-n-@8H&$NXC&X*V+lWz99blQMEZ z0d%nFAGaU2JVu;uKm4d=u^dYiy3| zg4F{E=)Db_-|czZ4L{swkRBN#w@z+?y=7P{?#4C%^`lP{e35n1!-OE4z$L~REG>#|E-=*{wrI% z`v>o9=^vaNA9VMxxDNudE;GEl_cmzeqJ&-*;SS#WGi-n1Tg&Bf;pTWY>&2pVssvq` zY7iPU7~6HS;ZNws{s4!uCF87}{+m1}gfHt0>8MIITILb;&i)>?;zFzYIa$EW4g>%i%1}J` z$tONX;IT3BO#H@KB#0hV+|S82%l3|`-Bivab)u}NyZrSof35#(!yi%=fC2#>F;7?2 z7D9}A^W#{+sz}<<`7KcwaFiBU~RH3`fGBPbF{>S25c;h9<I(glr266|{Bvwx=OaX&gC8>xaz%bMuqw58Meoh_ z=kxr*{NB6r{M(!*wRd|a-{~wlc-!5RU3vbG{k6R}I|D=F@>^BgpLdqj4V!#-kuM=5 zztvXYN$}TGV`56ZbPH|6@dXu92M2Sw~gRpJnQ*otkoffbk!_hb&7(7c(=({E~5mURiXVKk-&Tdp9LlC62}m zJ9A&+D7-ljSEhS%yqnUi5(nawy|pi~Kic3H{c)A~$i&QV_qSqSD)K@@rr$R+u68xTN)s3M>M$&Esu;Kr^0=Y(D z$qqys{*Tc+MAEKe3?zAg)hh&QBvybLdIntK7*LErHNYpbpq4M`RQ~M*b=f9HqW35? zd}Yjrgh-&Xd?|C4zO!sTUKMYFH2gXRtm6q7>=t6-)PX0D9YhaulLO2@8pmE2_}IAy z@@wsj!4ddoGSl|}z}y^>w6F1sHq{!cGdW^6WIRf#Tq zkaV>iHC0;S8KK&PA8HE(NBxK&bWzQ?(F}XE`a(s{0|!8JclDG8Pe=SVL?C_@<3}UD zD)IMaEo{&vHb<`r6!aE&Zo_}PXprL8Ps0z_<{Yd+prggUDajNaqo&PrEU5P< zUW@c)6?0Q`Vh56BjjxPs@+A&HggS!!g)`~ifrCkO(Pip4a7%p$@>M>_^>(`m;p}IU zP>kN&91A{Ecj4{uj{) zS*dUqSOh2ce2j-4EM-=@{8%T0zU5`B7(rmrCZD|f#_(f0c1*h4=jh}sM132{qv7p| zTpa95aA-;$x_f(W+SFAoY;%)(SA-6LVS&L=VsSI`Veu6YN@7zfY6(Hj5@vRYr0v19 zs(%OMG7FjMDIfnv|5vC2MaSias3(C86{Ms7isU-V8;L9G2c*5E<6<(pXVQga3GQ(h zSx!vS8GGPWgw72xk+eIJEgXCjy^;7L45v-B9|7LV(Sy~O#80o#B)15RiGw!D3U{zz zBWc~El!u%5ri#t-V;}epIO7&(p7SO>hBEtJv=6h}J<2|}4i$l33`?0PQWcCh3S3mE zq+@*)q?mG)ey!pES+}diy+x4nRlT0yeeqL0P|W_7MIOUDxf3M6pcCxJ0f_bI!(${r zHIjC;e+!$x!pP?PUi}^@NY~$!0O`+1yD5OBUz2Y1#(VGlRHJv$_yBCjAF!W~kKPSg z!KB2Ld*aO`0n{2?%JL;%$xo1cqx9V@7)F!>A0G(aS=2m|c0Mw0kT@JhkTMoyC|IlBJE)KCRO(ZjyG?pQK%m69{`(SbDHwJQj(L z#7zY$kC^yw&4AH?8r%;@h2EP>xbcn?RsJBfD#Arw?q5(qosfQGDDx1iJtjC^bwtP_ z&ESRz4IDXa0mCjMem`0z`WJS_I12d>C`Y$Hf13YVj8h6GC>&)Vn6hH@d8FGhsRxVt zQ@;;3iQnJk`-djqUpM*Q*5v!6Cg1Cte6MWs{Z5nb9|lrxtiMRNubX@ihGbi@pL`PO zc6pQUUjLKd7d3%DugUjWO}^VT`EJ?d`}QW^w>0@~9N$oq|8Krg&C6eH=VHEF{~5-< zncq*~XW3OYiBEOwXQBCAWIk;LI=p(ge%6lA&zjZxnOdlyS=Z?2GV@tuKGR3)@I~gc z!hB8`rNc?j>Hd~qXX3k2KPTLxpUcf>we>yOgqy0L6}Rc<>2NY^+1)z6+I(7YwN1XQ zaO+v~or$-mG5)Nr`a6AxiFaG$cq{#cpW}qj)ph!rdf5E_Lq9Dz$26V3=3f0=?lHe- z>nH2&N&HMJ*56jW)tKK|C1&_T`kCd`&t>M*s<#UBdr_$w|6kObtxUsJn@@}0rq7Cb z8gBUl6W)AU^;YQD;S~$@)A5M;d{jRR7wKnHyvxn_8uMv0;i~_7dUz}S|6BED<(KuC zrl)wZeolKrKdtsw_%}1&d{&#!^rv-rjrsilOM5H+yDnF)`Ly^n;aMHN=pSah`7B&! z^5OrI_GZz``r{OD@gc2GIW&*`jnCtPhwc|27@c>1&(Bchb?ej&VAD?H|n=;ZHZ?w>+iOFFK*)Sswmn>W9reuW>y4 z2>wkwtjmkBP@_MKuE;B9dCgd>Jhz$jE{`;($BKVuvd*u1kFIYExfMThgT_0}OmFeu zcI5Q*v^Eixy<^g25om_r{+o%<;=dV>(XRYus?*0?^wjQY9BZSi%r>Cd; zr@H)qCJ!xBbh;@2kf9zOXnHNY%VXrhqLmQoM;r5Lsrju_+id@|D%cFL*tKQ4Ntem@ z*Zu@*8hTkR{GTB z#_i2QGofDd&&q$f`EAA1c8dM8ct0*x(`E66eRb@g1^4@ly4=f~@WqP%Grri4HLh0+ zwxIjWYS}`Ba5DN^W~;xCOsA+ zGyK2OhXZ3%|wYoB6DD>hGFCS=qgM_vzd3vYcG!;30YWLrplLf%#IO zGCzz;ZR|@pf6ThTWRtXWnF=_~59Z%(nEJO--(khc)&~FKDEuGOoCeMRa?qdUPu{Xk z{GYOKvMz7j&w!jpfB#R%6V-z?{;W~@xqPgC{$Fd~jQh|0Xv7?R#I}uC;lOMdW~6Mk z=MT(Yv&5a@efgf%u4iUHHFWIHcl5}8CqFZN^v>TFU;O=)*z`?t^E*AId5gq^gl37& z6O$6+`Inf`qD6D8xFp(|H%ClDVnSkke6yA!yjyW51%z1K87u=I_G% zmv%It5A!de{2t8P!u{2Kn12QJJ%D*$xW9S`^Q*zrUvfoHQZQct^K!8KW0)6#@Td+B$P>_C`kOWADWMLPN5XtZE1Hjjc^B|!Vcr}(^`7wWX*|>) z1M^#<{bFdp9hNVF`MXd~e}xl0alra0g?V)t-!qsu1OFW68=<}zFs}{e^l!1Er*}~P z3g(wWeXn8O4*VOK9|7fWVSX0Om%+R~_;Q%%hw=)Te-HaZCCsmb@^>(w2+Py&q|nn; zSYL@S9{~Nm2JtO!d3p8I3^S7R%`S&m%QH16{zNc{I$kLU|0# z?}74jFy9L0=VAUel*hvSWGJV6UbPiLFuOn{bn3rFpftJVD6>k2a%kgz4ktmmfJ)`OWHR{p0JZj_AO)f;PX<^Fy;8V ze?gLYc`;QqK78KUB*k2A3;o0AIeqD&%kgG9VF30D!wi(Rj^)Nns z9!;J(bUD7yIH_Nf;l# zPts9gE*}r;AKy1EQDrXgRzdq8zK?QKV=j+^{^R?q3+l|}ym0*D`>Y@Hnah_#|L}d+ zaK|^k4;$|MgYV0R+rRLA+HluTeBUQ7#A9H+-Kr-1!0D z_YHS`!1saH3z^$L2-g>UUnsbUxm=G+^7s7$zE3BfTFlG8g7M+|P@Tm?m*e|V#uDc88n}Mp`_$55%JF^c zxuwj@$HMsWee5~yq08}o?YU*l$^P#@+dC;g_R6Bn3$1#V+LG?b^+N<@QKd zCvLQPFu2{{x9(D==g0nO*H9qOYSc@nmJZuUABS-lip^&S?6({L$p*Ew@bf1 z{d>GTWB#-{I!|rM&eFMisn>?64+t_%gXeoc?%sQsZ*S*jBYh@~P zprL1Wd~x$#4XO0wPC+ZxzPNDq`K-UEyJQhnp*QvS4ntxGCx2QT-3NN2J{ODD164M7 zqFzr--1%Glmw&hz>Ut`2_3r0STwsabqd2$o+q;!sw;3mobkCU;T>05Kf4{%sM5TLe z^H+a?cIjp_e+pDzJyR%*03kQWWq#4^^PA(OaJlWxi{70 zp?=uQn0)$9uThTs;f;wuVtik|y!K$@&!vnNVQHI=ecLImZRZ%Y>E7X6{7-uKGsG`R z8_(Kw%5z=i<8kcb2J_q>^voXA$IgOosp~HacTY3@XCH4xMkGh1h}TGsxPUuqKz0YM z$H8lsdQAUtJ0rDL`9;sV=EGM_rIxeXuQdC{7MR6YQ<gsl3#V(< zVe}>SeXXDMHLS+v=a}wrxxV<9i{uCT$e1&w(f_hw$NBTlsE_m(qu*bl3DGh=OSS%G z9&P>y6I;dVcAh-brt6Z)o~%9@oysB?pWXOWv)MCJL19ajiFEuyP2185cl?z#_9q0E zSt*<~p80VLoE3Iscs8;Ni zKJR$Jq3Qkh-B)D;@fMqlgVrnE z#mbM*TWk&%wq0@A6LX_$U>~5qzw&qgb6k{{MZ9{{Te&nXChpeNV5d`ue}8QX9tWL+ z`bPiW)xi@r%s*3KD#u8_(d;Q#ziG}k(O;6B@h10emeeTK#p1^OsTn+@bDUFiy=U`> z+*-kQ_k~IR*lEhMeO`~X5E09nXB(K-CElYqt-7Ij@$PeGT+8g1yz-6kV|Xe5sr~l@ zf!LbNwuSufWR_Z3$S%O!LKMcrY?T53WV`8|DTDhxa-2IJ~7kYYm#&Tc&H8EA^lAn=K)Uwjx9=?~q|5x>( zUiD>NuWU%tIj5fc-0Dc_Tun`U4pD^h#LYS36ekqxF2V?Ma@cuczk6}w&Oyr$_3-!h zz@r=r5I-Y*Z^|r%__~FS@57IAIM@G}DI_QowYASza~#`v?qGfX0JDcCsz(pWKRYYy zSMd2oi)?E`K;g7IhdJgQyYXmBhv>CkTSe429FC6+U)QBsv)@Qo#hCT0Wv;QzW;G)# zl^uIVX3U*fwIWPXZkuIvc+%AjUiT_mkpC9}m*U-x_2wix~=Gf#jQ%{@`JD2XK@g=r=hj~QGgpSPApyS8d zN7R}KsT#C+o!lp|CND?*MlRQ!oz;O0xQLIw^PkoU|gly}V}Sf5@?C z5mc3Nayz4*$8K?GeX5+Na9Q}bCCg1Cmu#AtlrvFkp}pMcFFPWvIwdt8=-*;lbdAeq z&iw8Eu{-iL0`HvK6Z6bo|F7%er-6UKfn0?!XYV2wovslVOV!gJX9h(ryMB9H#+2hK zb|dii!smO%rPsF=XerozHVu`2;Vb&7RxIolu~`P6a|RdTpZD5{aR_~g@@eFaZ@RjJ z`i`N#%&L~V+P7{99=*(ytQhj$BUiL}@%cbTXaC4w`}_IEaS6=`?Jp>Iv*jE6*;%6a z)y{6$^HcWfj2&EgMAeVEm^|M_`u(X`jqw=A5ezmFPYYj@#8_-IwNSbjN2sDZxqH{vBKeKh@b-oE=spYJw#w2e|W36@NAR_%SVkG1_e(QdTWnHb9u zm!4;9Bm+avjv~kPF_lGteS{%fp}J2*_>7vECubT2h#LC)`~Um6Y`m{-nr*uIcHgTp z5p5Dti@vPArF)L;i2FyqBL@`ZRFX!N1xRf-5Pd9m+bUX8#%X@#*A7$DbDw?%WGXhA zvrK{Qvt!bXtG0Gpi+lDTexEhCS5$wV#(!M5*!AW3JQcC*wW7A< zxIB065(w^7c1a)Ux5xkdsogJfV}IYnQ-k?EdH??RzmIFWDaTJ)MSG(goln?a$Lq(9 zp3cJhGCW{nS=NNBTNXNOx_(UX_KBxY85XC`#6B$3F)VGczWu}K#$)~UO=bdI4MD-G zqdBuhjAvY%woQH8)AxtB`W}o(60HBcxdZj&cX#L>p0;e(Ou5A2F@Ld`_L_|bcwC|k zbkB7E>|AdDM)s3V_HMQOXC*UyW4uD>Wuce~G@<$i{-u7(baysT(%)bbS9^TcOgYvr_-{m5?kV0%I1t1FwQ@62YUbDif^ zm%a_p4_jTYRo6GkJMvg#^9m2%Xn`3eN~X@UtV*7wYiydAYe4h55a<(+%j)kI~Pg>CgDkZ@uv~ieLTn^u?qvH~7i&EAW;aRZ2~)EDXiK>7y?- zKI*fJ`nXoKi~4r|_b=)sxt0hXu4#HC&N#c%X4{<3tK+%e%-L|4mvK>AQEKnnF9)B^ zvU(xtkJm$4QOhCK58KdZ&6(QI>zS~J+q$a#ODBGAVoA2YDMGNK#mj8W>*~A&<426p zrYkLxGW%ZU&aGJ|xSHjnyVE|&LaT2I#hC)jPBT)jJzn*!&q0r~Av7yX1lniec8in< zOQL=yB_97+dT7+&KTm1H&0k#0Di=#tJ6(rAdqEGMnpnngofjSv?`$HrL^HUf|H$<1 znXD7@O!xwIZErn%BqQL~{X&!Bo7FwFI$Y^KZTPXkGv9$`a%zueqKwj7ouIdi1GOMzr;Uy3p4$gg|&}A&v0M%*u(^`Hz%uBZLzXA z_2naL5EpNt35w#^!iB?DW;eL9Rh24nZc~ZtiY- zE*UWvTUBVaCfy7_YtYwQUoaxDbLx)+Z1~8b{-0z*dQg5;Jf9Txb$;}ILr?$n^}{;S zT8Jz2v0>qO-)24o1-D%*W^?>vYimBg!9Vtu*SUzXn_fm}&-Yvm#%#p0aYUo6+ zcGj}M@hqLb+3R_Ktk`vJbM>~W&Rm}}k2+Sk1}{-7I>D2*e2Bhhq~e|4U|zxbZKdVY#tz$e7TXRpgL(NS3om{4 zcMuvi>i)uF$J4H?+ano|Z$}47y?m`V&r9~EpOd*jeQU;dk;MYn8D;67Lh@`51#BJB zlf3p9j=y9uXXJGouV9vvQ%WXW_t+C<8moHB3R%8|bxTdtt9rV5aNk+Xu9^rljQERQ zeu;QBupiL(D)goqs-J&0+2DF$<^ShkVc9V|%TLW~=cIO-uL_;xLv1ezoprmmI_19mXAUiUA)F-4)>)l7e}@qYK}CixyxtrvmuV*!_*GvS@T;HvR`iPne?iC z=hbR_4y4CG_kY)`m^N=`%>_K?b(M8RV=LGr_3_*3Ox66yaj`J$?YycsE6I5u86kdq zt(dx!x@c30mT%wK^`5~Y*BCz^S3gNSz<*mzwf*+)^{pGTRi_@F`{PIRPM2TChgld- z+!ut{=5V@w@RPoiduUYa4~O`|xh~thSbp%WAyz1V)2>E^LyptfL?qds*1W;TDBdymeFZ{|H9yX95-%-+Ca&f5p6Us(9J=|7E~g>~Pikp-FS zwp_fBbV%r&;-bRSzxFIujETMIt7Gcti&3G45dvf)W?a9?G@}H+? z8ha&1`P9{h|0upw?0-Ylkj-I}QJ=*4$+v)w-xLIlj&Wj$K2rEuYGtV|7$-o`_nLxvo=&!3o*23zk5yXMi8&dK{1|RBLeEV3iPBBpJsM{_sG9*Ef}4akg(y)H2E7T#u`tX zSm$Z8)H`yF@>r#nm?e^!%Pte8Z0)9S{MZGlhS>%in?CW?m#^Qf)%0M^goLPA_qDby zI&W*Fof+N(*InjEed`b0eECUKgXg}nr`3!4#zJ#livyW8Q`SGl`w+bz>2;_Pk#@nf z`*qA-u>~Xabc6<(DD{Ow{Zk(`M|N5+b{6_0pEOo3)1K*`AcOGqRw|j&2lF3yo+FeCfCQt1% zSN?FCt#Z1V|Dmxy1}1FRuUhY2WpsOQSUkh##?!0LA3A2_#vhRQ%;&Uc?yU#$IYLKS z)+Ver3M>s)F<3pdT6eX^b3x&`zW401Sa+iDXdil8v(zQLzBH~wiR;e#3m)^MITQ}~ z+7-_$iygZY@1r`@g8%J2G;|BhKV_fJJpcdu91H8#jLYjzEXv+57&hw%-=g=4QL(2P z<6gbAmhjPGIXL;k;_aaaPu}EcSXQpMGPNZ6$n9rYs>_${=?Oo)Y~sqoz(;kb)*LuD zIcNvl@=71?)w%OlulQ7@UKl!S<*OqtF^cX+{ST+~GPWF&JG*>uUgDDL@vHCLzSXtk zdS$w3@}zz7Ny4+)6${o09sJHxtT%pP`=hx3vcK{R>esfPv3;L%{Bp%{Z%vhS#kaa2 zUaP81IbM}&(dQa$>%qr@j}3bJ(RIq6Tid0^qjXirnb(G$?}3^(zc}CD&#(Vw`}|+q zrYx+TJYLhpzJ^xGxJ=CGc&%XJbh`h^k=FKN=gTE4&In#R*nc(W*w1xJGa9csm|WT_ zv*MSanOpz3gf%=X8&$IkM|=16d&?N_aA#vJlUBcA=2Z4?|V4kCzT=EnJ+He8_ae@uXmF;hUFJ3`G>! z{q9Bz)U_LI6@NE>l8#i8@S(fr|GXwJpW*1a2AdMWe&g_++#HdDo5cO5JbD*(UO=R2 zfx)iVTMz7Sx~=eW)iS(I@pF*p|9tLDe{B{yv2bxs*G^l!M)|_D8n>|BowEy0&8b?W z#dvJs$@xlR%=~_pN4lcv#Vfa+eObnpRfNjD2$;`3TR?!;4Roc`O(VjNp}bC?QtIsef2oTEWx+;Ye!$Gi}^KV zA6d9lqFwLb;J#Dl`#8)q zEhEELp1AMNBk58Rx%mNC%%+vq_6HvnPd}^@!YVB8U;QlR!N7|D^?haSgh>?{!F=)m z?4$mZ_A-krf1W+HJI#&z{gs1wfOwo-To)xDE?77{*5n+QV~o+VdfE3TX*_IQoD;*F zBkELsZ4>S`*Xe84F`7Puwfo0D57U*Ss-7O2*05&T?(QR1IZrrl%~>~kg4M)NI?anZ zTn+~<_;!Y)bfAxYlfI1S9Xql=jGy{XTloAtk!pM^^@`I!F8IG!A{XO*LxoHzr-g>+ zS=G$auzg|MbuTwP9>uUMUpDfN-T10wca^S7H{E{I+cL7I|10CDK)%02csirI?QrVl zkJcSBM`~7>hBLNZFpQdc?cM9FGyzYW)VZ}~Gj2GU$kC0SFx+{%<-+&zrV+1aj1(&2 z?327Uu>R=R^67KSD*i$vsXyD;SV8K{k%K9>v8VkZ(SgPo`uayQV>U`l)HwV8z6Rqy)BnGw#?WBq!-Kfb z`VQCKwoLUIwCun`R+h~yT+2*G9G^Cg&*b=I*Ee3S@ve=HjowdO`X+TtK0UZMsI~QI z>bmc?r^Xg5yuXVD+guvO)XP`}F@%9Cj+OiGFFe zp+QW3thI9VjGqUWsQ&DgUG&U0IGPdktLOdI+LP6x>ss2}^Ge)|S4kagJ~HLmMRx|T zaoQx##$#vh9b#MmP4sQ@MBh6%F5Tv~@vNsh!jjc(aa#PrzRhCg^8GWdUIpg7a`6&V zI6JUE>Gn<9ml*_|t);KloOzsc|lW&W)79N!f z*zGpX)Gcw(UUu%UU&=d9B#-;tnxd+p*g4x{bX>>Odk5}MOE@B6dSZL{&y(^VBg0<& zGT*+n>%+IZCbiC99{y~8YnnuC-hR<{y|<|4d;583$)oQb?>*YJ>dF-tyO>8;Y-~A1 znR~^%Qmemqx$N?j?S-%H-~YK@(Bq=_AK6Rc3e^|z{$nZWASLDL!(79_-0yK|DYCel z#yk9cFCK0ZxyyF?OGEx7E_+G4y%{D(4eRyiecGrQ_Oz{d|INEA6rRd2`r*39FKy`# z1-4Jc8M@NlrK>_1js=y=&iN_#?RkESk@2Q=*0=33qNDXh{>Q@unX-};i+OJe| zg{xLYosc)fvc!VtP2ajW6YdU`c?AM*NA2yj8O{9VKZ(o7Juj;cS=b$T z_z;g?KR$NEsUC9U2Gtzmy3XG^hVPlnyQ2)gM~GqjA+(R{pjBjDnNtPt z`uW~+{WL-S$MzpmVEbJ+RSeCu91@C8MQHYGPaI6?_%KfR6|%_IpgaUXi!#VX8VEtFQ0tTy)fuw;D79+wF`+<> zcl*B|6TjC9{g3FMM_i1W=U=9eSi||P?n3wHX)#l$Z{2&YF4g$-n4@F-&C8a#J=pVF zVPu9uRI%)&5U2VvjNWk*U(OyGvQ%!*$hDt~6${>RM$T7WsxmKb)vM8I%O}?6kDYn& znO*0SA6?7**_{}Ces#MIeJhLV?;5-RZ+5agb-f}r(zqUP1SrW>%xkI`N~qM_i{WBPguO}YLBqDkzcZ2v;Y2i?;cl` zhs!#gF*_c)VaqE0%kOpy^SMWCP??(cmMd3Ma2#)*<$Q-LXKQVqcaD&p)>pV)`29ED zWounl9SXSp_0rnbBVvsYTAMb`ow`{jA}@bga>bj`4H=@PNkME^wjEs`n{T|!a{f|b zMY)sOWf|}N8nfR|bzV1hVX)J?C&}4*3a8vXYcEG`uyk5?c%C(bO+$IuKjQcJf2MH+ zZCAH?cyvxc-@`EilkZY5daXE)_di~071H;*^xr=d_1-AcM9#9<@7;T0)P>U@Ugd9h zwts_tL0sRp7gqmY1etf8=uF7;p zF7M^CAM|}Xy%q-UZy5{?)afDD11cKyFPG9# zt9#!d0S{RUSQGH{sN+$faH)Ur<;}Ps_&FKVe#&5o;)VZN-@pB23bm&UInm7wHI2bL zHNDL~cY7?Uh>;M@>W#O;+m~KX^a^RRTCslT_H~uk)sL=)ro813ov?vdnP*>JtWWc< zx;r*m<-06Tf9$%)n6mbaU#OPyIR4cMGjm!(3*I_dt-bRqN#N5Wr_~!y9?_oFY}S>N zU2#vq;mI=hNyqM~bT8Aik2-BDP;M(W(8jvsFZE?KqOE&>e-4fA_`mgIV7Whi8|`lF zyyd!~`96csaw|O(+(%g$clg|hxS<}Y+L52Q{<%V+~W8+ZN-z%j%EaxEZb6hdBbU^;}bRZ+^DII zQq7yy=kIs7utwv7uyG9U|0 zyoo-Pmi=2ei_zv*Iw+?A5elV!r$F5NX8Ww1(6_H8n|?7G6|F^i}B ze4qdHy{1Y47ypeb;*RwT#pXP>uSnUUvRPx)m=Kw3a$Xjl%G}~NFMB>b60KK1QE$rD zUth-Md9F<`>DrOEW1Gao%;;6ci4t>kqazCAzQ5Qrp0${%FQq-<&jhk{w^U`vU5q~& zgV*bS={Hk7%wy`+Rq{Nk$zHs#RZ95#=+9H0k2$Z$opRf*{g8FwQKuXAOG>8qtAuH- z^6neINB+t3A14mwJ+hWbedWWptLfYLg(gRyFIu$HV~p+;&4Qvalbv$bPtvZ?V`^`h zev88XdZov-;{2B@*$eGQhMYZcbjMcp@8Yi{MWsdgbws9aHFWhf8?n1rPSNzs_uYz` z?){BDKE`(?D=juRt&$m_?Pd*%0ko&KRcX}pcd<9yep{4+lvGqMmd zdZfVac$(;+%Mn%gxHOwfT_IC=_Mg@n4=(CUCxnR_u zjNFsW_mdQsPj~v@%hK00gFWtJ^ZdI{8#8uJ80qnUcXC?nQP}KZ#%Ods6*!Vg8 z-Kg6?r_TP$)_QSG$X{dAp>4H`G?hb*QqL64sw@s% zs_MJVDm5|qM*F43IbLUa4Buw13Cg~I>_cwDSC)CtMsjzt;{W_ZH9sfQ)$Vb98ip36g6czu<*ukvkPJ>rR1joYRo{%!qA zrr#xEg+H&c%~Erh(pi{m?^4m~ep|8m3-_wE=aOBoX-g3E}{MkcA_Zj=joXN(n3d=W1bA~MM(U>Spd z7FUbmzI*#0E;uq_^!y#%BTG1v&gI{UJ+}I$Q~>|+jjdV7yLk^?^j)E^_OhA%@}qat zN~9%m9#%~=5@=iEp^)KO%T@XOl6)Y~#f8lqUU{UdvW8YXKBbo7ANEsUZ>-2^#c_U3D&8@tOV&5i=jyoT z*M<`%_Vg8N%U2$a$ z#?3lxtsYc2+hu8@xZZmW6XK%_#mU z&VG8M#`mQRzpv%BOY^52rf%FbH%O2t)>P0zYoQ%aiN!d#fQh&6W^q2rztorIcTi&O zgDq8OZ|puw-TE%rpJlMqqGI8Nhy>U8*QeC%y5hdBuiM6%CY&G{_A_Koxw)b4tI%&r zys-r*=50QhSbKYwqELo_a^Bck6QZuOl{A%|6gzmQRm}N~$k*~}|4UMeTc37sxqUdX zHavX_<44Nuzl{GsZx2y8KB%WZgP)huPltX#hyDEk{qt|-z0yrTqtC`zDX%n()_uBh zM105Tpl=hd2A_DBouj$(6wk@o_pi%M74j8YAHlBer26<&aQ&#1I+wOo8CoVw8 z>`;r~ZF3jaKkfDDoBd%X?Dt(5sGn127)<>bd(T1ZbNJp2e0TP}q=E7No5y9P|Iy=~ zRt@t1dj6r$Th!#8KF}ZfGQ(%Y{Cyfo4gC8nhS@9r#kYYc`-1B!h40-a&wY3~TpkAf zw9_4Ef1p^eyIX_{??vS$9O!#@Mp1^| zCi#S`h3^-{Nnd;DZE}xrd9Z(537~q%P=?+n4+s~XhsymYprpSChIR#nI|Ju8YjE^? zDYSb+xDGfk9EW!4a~17M2)7XW=LnAee`B1!*!jVlOecpBA%lYT#k-fs1TdwvEvobT!li-3 zNuNJy*GIVOaMUhN`u{7@j!U2U_%(w1aMIUS+VK(YO+Bg)Cw)&quNNW0ajr(k4^H~p zOV=kxxUPIu->Oq6>F@iXk6~HDy}pae1y7=+ug$bmAe<5$XBw8sO$Db+IB~e{r7IvO z3Qm=9dhb#B2uLx?4KD>!l~F$vzK7a94C_T=7-yUd z{kK9nopSW{SWdJX1NBV`MNV><`n+I!G(!KRhH)2Q91SN>ebU3YC2+hQjYdv}a`bjH zAo`~Q?g5;qWx<&cP7Btv6|CnO!?arljvv+w^=ZoA>&2X?uNTg16JZ?mXG7@z*%D3^ z#%lylewcP^3(+`nDnPrNi27PKpyLClBIW4gV>{ux;CzO27P!5Hn*qNc1LtgT`w6GB z6t#;}3EW}A@s35~z^M%G6yc&-ki$6#+*!iCnT;Gy`ZE&rcrOu7$QJbv=R9ypgqyMl zIh-osG6^TggW5gx3#BUM=xh32!W~(J>f_EusYV(4I4U4q1+3?8Na`apzg;QeKEVDm z7mlO(lpAVtf#a-yGQg77pW1gDV5KXc+h7J6az#ohUUZNALGLAc3K?gUAH-i_XF%7oK`^UI2fXnn5)w}5a-tI&9FL9PO)O*nf+RL^e6 z)!jQ*Nlq0?r#a4Jbt4$F3#n+ZKxY*B*v)4Q1&2L^HzeoQ&Fi4rvI^ zl5j_qkc)*h0%uLQk8q#s3+vAqoGszHz?}zY0?wXr%GRj=u8^kS90@n(EOHkh*Mi$j zIDRkWq#)OU+eWyxu>Nks`ZEK!gK+9_T{VU6W==VJzuHZ>emJjfg!>l@$_zDC!TCx7 z&VQCeA?j!B;N;=FW;KkP4d0tSAI=Nw>1C?%B;afjp1miigJpZXK+YJlq-huXIT7eu(XJJ9yUxe?qE!mZLo z^(=zzYfm})Ic6~79<4|9bwD~meG!BkRgUVr2kUPW<>>226yYYq{)y8O>N`)kayTw< zI)S@DxGAZqe>k1N#S<DY;{cDEn??Ffx%Fy>O*@Wwa^5<|p-$9w7CZATc-O6FR z?W7RB-R=?fO@Zrp#!a-ocTt9Jw}5bVa9pf_+zs_TB^>qR|1?=cJq^%5S86HxwVIbi zeRp8{{($Z4Mj85kt(MYvJmBEanj$FYX_{_5E$RF7UW$^+oW5H2WQ2a|6|PGyv6igfjI0Vof+RIKCa>b&#Xr zYzZgPkLuBaJO<95a9+1jyK31egDFR!UmOXiU5d(6D^Z3}hTa}q3AZ242SwnHgL5Ms zAH05g3)asGaQg{21zb6}li-dJZaws`0nRh0z=aVm0{X`a*Z)v(=Ljd$hSnd>Fv^Xh z{w5Gk5gg9b;L-@U1RTzAaCwBY2Zu8P+!Mm-fx~$Q+-t&_fWsLHu9k2maKDQ)igNUR z+Cn%h*k5p-1^10`<6*zU8BIBQf9WIKn?AIjamIk-He@~zRlxHioaexeBiu!}kHmSN za`ZSR6K>}>v>)S)1vi~=u5g{k8Amz#d@4`4bB<^nwwF=87r@OW+@4veJRb5QxP^o( zf&G}<3i(Tv8)|Zc_Od)t`|%W_$E!`$_pS=nGaL5%%ivZMt^}M2xGUh+5^f0`m(!u2 z3E*r97Z8p5Ri}mWD&^??z=?1^;99^XLVddkHwX4poYyEvpC>&Dr*ay#hx0l(AHvOn z*PC!AQI5W@9w6LvL$v;ICWAXfxC&d;E{h$?8m*a#By@u+;c@ta&;fkQ1*N`dT z&J*t4C)D1$W|XPmt`N=|ejl<8T#wQyH`L@g6V($1nNA@V>hBGrK7&hW96{hRC_`_L zn}nP5961HZOv-Rjf71x(T7=padW8Hf%Fy>iS%iDuj>>UnQHEa6d4wy0_akxM23J5h zvrJTIs(+jq1ah3+^-FPJc%A;miZq zMYywTP<=S>f@3jaKA%d#@rE-W+$h3*DMRhzya#Rq;dD16hx0z==E z{A5%g&S&7R6YgF(S}!=CgS$nzc`#m_FTmX=+)=o0;Cu=0DdG5Dp#I@}1+JWMT4KoI zd=2gc;p+8}!}$hW8{rg|B8T%WxE{jIgV(WfmVx6mWI4i(S zCfw~E$lCEY^~ zXDv7@!hM44InFw8j)XIU?SZo%+%Ceo!}DmI@44ZB34(BIucL{e99L{ENMTAp}Mg7Cs0`3jr zew;@R=Vx$rgcE}LaDD;znQ$4feQ~ye`$4!(rs#Zyvke@Z3G;FJ9bOl}*$$3}aQW~& z1?N|ALWJ{*L*vES0ZxK&iQsT{f>R(|d%9P1;Owr4O}td+>Ow9ak7JZNjOV5E^%^zt03G3xZlOe39gQCjcd{VjFSso z6X90Det?r3TpQuS;C>wE2yk75ivx#qB)C4pZSzL!1?MPm9Hz|YjfwDl0q1CNqX~Bh z?mKagp&Y$^`3d&|98Ml^!i0->kJcYfUU1@syABQ~AGjHW%W6mC#W@z765%RgyWt!M zPMvUEPf`DH@`GDUxO>n)oa4di5-#IB>L1Pt;0y_O5!N400dVGo^MQ78P6TI5xCm|3 zE>1yk&V)-YMGmJBxSfQ%cMdt6lfdmGoH^`&IEBF-AY6JcTF*EqgF8&PPPo69h35kz z;7$@Q)f)8==M->}gu65qIh<3$#St#15jmWq;I0yGE8NH86a$w+xP@;}eK@CqyG^)> z50Jwt4(=Y|YC@62DFN;Y;f}*~3+HrjF9`Rt5Y@-_1f?Xn3c}?)N9B(prNGq_F7qUE z`H<4!nh9qFuj@I%ej@{}gK%kZUBD>|u7_~hZm3S6 zIA_8&l%Vy3Qx)7!!X?A@#i<5vAK?n`q55#DQ;vRJ?*QS}2Ox)YKDZ-<^MQ78E&vxw zxY16iKAa1|MH9{u-cP}~2wXhj#N<$YI5of}6E4FJIh>l{ZV@g7_A8uP;PMH#wFhlC zoQo+(-)}r2948xcIG2EXK{z+~yaUdq;3^2G4A*I#+TiL5X9U{==Q423gnI?|&p36! zbr5b2+`r&l4z7oAk#POPxdI&9I_C45s1aH(ICa5|BAgDqu7Xn!96#Y^gTuKJoG{_y zozZ@Pa}_vo!d&OKjSn6 zr%SkEIPc;#0%u6L+pxdjGzMo*xEgRcO~Ba_?s*FuFHTc%&V>5_=Tn?(!R;iRAFMx| z>%i?J+#A>raGHTTK)BBlX#3(c2X~loh81WWI4!`PB%D1soR;7s2}hkG{?jA`&y#Vk zhx+1(`ZmJzMx55*t`aT>)(cJ>a4CedhwY1V1Gw9SbA$U+oVMWZ5$+f`oOa-z5N=Nl zTF*E)f_p)@-ic_t;j{-=K{$;zEJ_4r` zxE{i_HlcP$e?sXDj?IkuJZS*;oeti}Z3Z`raMUM&e5fMEbjU5#41GQ4C!96x2j-Al z!A&NdKU{y!vr)awtQhI9qDg>d2UdSxxnj$ zUXb)bK-U*cxUoM`JwlMa;7$@Q4bE40AoqhiO*qL$G+rmj1K=VFmjKT{aUKL0LpVn` zes)9pQI6ieafCCoK;vM*>tgNQa_2E2BIeNPl5H7J2Iq?+K&m)wh$6G`=u|`ym z^C;!GDffzSt#BVP1)dKa1NV+_@lcL47~BWKu?eF7;S8Z1J>JiR8)uFBw-oX?xHiJM zW+9gd<2eDYgK%46zbb+DPlEeSIJ~`Zo}%1PQ_Lqcj#H4Kl%w~*Zlb==D#+msqa6ME zcpu?jg2QayX;F%^;i$w2SjBI3>cFL%TSm!Ko8&Jd77-47kOF3;B-vhw~ie=@aNdNo%thtWRw(1a1rjb1`u7F;cNyGq!udo0TA_bez?~u7D(K&1 zSbquN;s|#L*57?te^4jYaHEf-^OXu@GPq}i>o-Sk0$jgtfO|{0JK?BZoHr>){b5uSZp#K# zAI=mC(c`EgoGjcI;7o;f>j_s2_h~rOC`X@PJ`wKp7}P(U>6D|7qpyTJJP-8`X9l<) z!tICa49-k&>=w-DNl!;qAI@9g#t_a1+QpegIr_RUKsXn0IB$a!CEN?xUO2PC$r5hd zP1HY}IpCBDC%FPSoOi%2B-{qLZ^4-hPKR)h9-#Vg=7C#7xK2;xaNY%HLAd&5$l=Tf zXHU2iC1J_Hq4;rXGoX^2= zS~8yxgd&i``2rjt;fmq?H=HlQO(I+k+!x?{1x|u+S94IiIA4R4C!Aj}ayZ|BQz4uq zy#9>yEjTU0Er;!avkaUb;kw08yEx0inGo(7>`yo=z*!Sccr4nFaaMwJBAkW-Y8U4_ zaJvZCHyhQ5vkIIS;m$lq4revEK*Dh-j{;~MI6r{9PdF|(Z{TbIS3OrFY%1FSaDE0io^S_YJ>&cW zZVKU=L{WV>Tfs>a?lm}^ZQy1TE)N{ec5w3vmjDjuS8&>dlVwBe8D|GLeZu9yeJ9RN zaAt&Sg!4GgZ{X|*=W-gY7o6Y0Z6%z37;-qfC`aF)x)F|b7i_mHXgohCM?df0N4RU? zaCTFUlX5#O8KSH=(VdVZKxRZpFfc1j&7v<>jo*`W8 zebhgkec;X#j#~6TP1W%EIx~1*nSQa8`olO!)OV2!jpI7JF2zD2x__~xTZHlJ zg1bn#F$a;u$p-E+;c{mphm#%LRl@l|y9&@Q2e|8m>zIqifs+&54Z?N7{Tfa#%F(Z{ zr4p{{9_k-XZg81|`y`GW&Jp0U2{$eh^$+JraCw9qnSvZn`WI}|{ku;%4yX_3XmACD z`=yKO!#M_A5#iW1&^U1NfGZ_j4;&vjdBMFT9G?hk_fLX*OSpQd59e5Lm4w?Gjrxak z9Jm_7X+)rQaq@$EPdJuD;u{!I46VSCfp3D52pyY z(S&1%{^6Vgj*oDAVEy5o3T`~%+@M{YqTmEc2kY4lQViT=!WlyUa83g!N;pAi7pFKl z3Bs+0_HasolO|jnyzYZ@IygDPML@eaCBZ2YE(qGiDFsfMa2_yToYLS_2)78@#VG@B zKH+SkU7WJuGzcdI?c$sPZVBPeg2O2XPKR)ZV1L0m6PzC5@}WMQ^5FCdX9o_a0yrbW z-Gp{=DuP=}xF~QqXMwXI+J5z6XE2cKAdyFZ6=%@ z)Q58}xb1`+0S@OpaJvZCmW9rLI90&85v~#(PE~Log!2T4Qw^LC;a=WC?c!7icaU(G zVSmOsA6y{eZa}*@7l1oLxEOFa7lI2RoM0U4AI?SKP7&@t)Q3|8TsYxIfWxT?E{brw zV7=hf0(XvZ$@!>%I2VJvKsYUE7v~aimkIX`)-%qf;1UUU4f=;u8(cEszJtTL3|uPV zvY=g@I^Z%1X9M-&Tn;XqaK~W0I9GtnBOE7eU!1z&?i20&_A5U;93ZG56){iO~ADgt^m&CI8DKI5-tJi!?_mR55jT4`!gX$=zeS+ zxL(4|hR@-3lp|*bj$zGw|Ch#zj$fSS;MfV5B97|8X#tL#a3UX3eK;+_jV4^?d{m!J zHcIMuUjKgH!$-I)FkYPN!Hp-}J#aX!!3h$M7v8tRX#;LD;U=6xK92SSoZBhKK|POOOSl8_sDC(Jz*!P*!39(w&K=-35Uz+1 zIh;GeIS{Tp2|1j*z-=bnNjQFS?gr;VxHDhSIB@O(w})`150LYK^OY;*=-2D^5w2t{ z>fhN`ly2aB2{&Q^s_!tkz2E`}rvv>nW<~Y6gF8yNKyZcN_JKP|xY+xs9-JPOqhF_q zAl%qZsDC&;!JQ?X%Le3ddVxDnxTbv6E>3T77YTPh3OSrU;I0sE8N6SC(-+(|!u49B z{^8tDIeLG-K{)kv)b2w#A040^H|5d^ci1h#6&wR+E!?<-N$Xx?>Y#28IoFlm4VcbRN-!m9* z$S|%N>QRC`K8za+>tz9q_rx$x7y1_i^_?8X37De&xe$)KQn5Cp zHEMS~Wxjob^!NT`pUbg$5QDTmLuL{W}Den*q;9dau0In}e^CJ-*H z1-a|sqQOleTwXPD`ru-~NfGWo^zZdc9L`j5d4zie_fPaHWLvfbrtY0QZ(~_Ap+Ync!*& zCkx}nc?;Y}!tICg;>-fqM!0t{4xG2a{UF?Pa5%HUF*Y!tPd|dgnFEfSaM|E+-T}u) zxb&}RyWz|QCrG$ha5(cQN8hiB60Qff2hO|TrW5WGT~mqIB-4yr%E_pc%FjuA-IKvTMg~vd<1SW;iSOfEC8oNI63Ga&d15o_ z!&wM!4dKQ?eK?*nGS`FeW3-73$;qw@9hd#X_1#gFCN zsfK5l(3UX<^o=X>gq^||<&d>?qe0|%_n#Y^&i zEo)=_T8~>=$kP2H*95DyqLMP=EL~4c|z8KV@qtTi$p0 zGEm^3UcJNLu=(}wv+;ZR61)9-eXvsHol|)~$d}#o)p^U7^U_=!f5d0j<0+nRmwdO( zv%a6?E9v=q$@huM`&qsUp0D7Www#CN*my<0YMyVIeCbu*FY?v#e2wMXsPeAL*U0m| zqxT0_G=BVw?|MJA^j@In&$fNPrQ!QU|7LmD ztK)jxvg`gBq5hnndd>M8>eYLP?%%$;f0N`J?fLTPdeqbPNS1G+=lemv`ntXu#g6vxtJZF8F%-3zD_%A`&`T+-*L~kd!?<{#hm!e z@t*a374-Wa7jwz?ndkdS<++$!zHdF>sn)iAF6O~!-VK*9$ zs_FdQtyo&VVV>_(t(Qv5S4O^vJ>NC?vddRizHy%KwAzj01 z`To?pIP*b!ycOh|lkW}B zw@f}4tIN07^Ocd$#TxP*_IxkjW83FqP5DlGzF*o~pNluk_o3&zNj?{A$@hup+pYGw zc#C{rdcL;z+In5AE#LQ^Z>)SS){*b3=PN6pi?_;m&GU6r`&_(DzJwnC9`8TW`E#+Z zd>K7o0r_04CtnWF*Iwt(#rpE)_k5SsJ{KFvSJd`&?`;UmMRiL*=>HLcV)E z-yxOfVoUkDc)lG=?YQFN?eg{VeBbEzE-v07-vH0Iah@&D#a8kS^L(dOo{O#J8}0dy zcC_WK*ZIB^pIN_5@O)3pS7EQc@7l;W)$=_o-%$D5$~VXJeIj2Y`RASKhGYEzo;`yYXG`Cq@1A2YrvLJwCJE*y@!xaZE z<*aOdF5V~KG0(Rrw{4$`o#i{>`AR%(%X6`deD8a{6HBbm#rx&^!1I;V-^01sRlc*H zuaoL^v73D7Jztk@wq6&z%lC=ro2K$y>>=N0p09$+bFrsw&-YnX zd!AkFE#J4E?{rsNo{N3t`@!=Szt#F&>?_~To^OYa*TsJFUG;p~=i2gI>@VN%p6?H> z>s)+5zP~)*Jz8hGI6%I*p8uX#4^*+|#l?a0g+1TCYqotZ4w5gO=S%mu^|?4$z9i3g zxt{g8_@I0lJzu20^|?4izATj~aj1ORJ>LqA2QChiFPG-o~_ z@1$IONWKD|?=|%U7e~lf$n*8ked*#z`HFhJ8~WS!xj0I`5}vP6ed}}aVfk+Gd>1qh zxj0(BvYzkk7TaH3d<36)Z&AVXUD3SotLB9<_^$U;Y?CeT;e)oFkK!})LQSu{XXX1% zzOnMv^L&@u*z#N)Ctq{VH%jFlRe9s(YwP*0$#=W{zH@?n9X(&hwzgguC(75u^WCBH zR;%7g@(u8O@5)zP^-h-WAkkM|kHC-Ft7_XW>a z`-=6w^^=V=@R|O%!Smg`#^yiW-JZXh^1bN!+UxH&D|NQMS@_JlWS8gbE?;f=X3MwV z^X1li(J%GhX%4>Y{Vdme(V2QLIyVj9A^CR7H!ls}4*8yyZ+;rS^Xu(-NvG?)0G~O3 z$Gqdnq;<(vJ?}5Xmx%8J&v*VuTb_$gsk{rGZ}0ck=i(yyzVv+KRi2BB<@?d|J*)Cu zTq55ep0B#fbMa~UBE9}Sug+I_E-sZXljn<9c`h!KFSq9#uJT-5E?;5K_npdf@frC_ zd%pKoo{KBwtL*s-syr80%2&(tO;&j>u9B~T=bNqaTwE<*OV77M<+=E*d{NK0ai{GE z-SvKXjePfczFFC=&&B8D>*e`=%VvEpK9A45#~kGOI?Lzc3-S&3eCyRqdPUy*O0=WDjvo{tA| z+4w3xGyfj;0UQufBY5 zq~SX*Uv>F*rQxfy$<{Yl>#N=P%>4VK*S_8IW!L)ZP5FNJd^NSsDWi4HTk^&C{`YwQ zw${7*wBCJNzGTlgPxXGG_U(}`yXVWN>(*24+bdsw&o@`?o22&bldqWP`$FyeTJ75} zUs=!BN9}uE?R!VQDxU8f`D&|u2jr{e`MRooZPmVm^40fz@2hTS2%`doY$pIHy}^n6z}{w%%E`cL39{rLgU*Z#6Sjy>|7ly8{l%iPeGKt;; z)APM|hxNI5UcPrc-wo=|E?!W*$2{L8wa>+m9THbH#b@3B?rk`H$d_U>?H7eJl{i&Y`xv1HhzZB+`m^mUqgK#@wEP4=8}BZJl`Ju-tt%deaq+a zCHDFE_`F-k;o=wg%>9Uv1CVQ@^*b-_c%=@8qlJ`6kL2)!#vV zFJEKNH$%T)aPbFx*ZY~PdECVx)9@u|-f;1!G<@w;o{K-H;k!K19;b^}((o0YZLf!m zzog-7s^fL>Y8t*Gb8LAo{+fobxW+3Ne@nx6>xZ_y@`}Hw;oG9Wk9}V8k2HJ_gl&2A z75~I%`g05K`u@D#`ttr@&*NWdly~eU>uXrR#%n6CtykV`&HFcZx8?mUUkA_EU+)>( z>%FXbnPb}5&GQY?`|;_DA^G}wzVUiryhSlyz9F7(k^1|KiedR4_I#_A|CVAzz6qW$ zuh#W9DJIA_)$aaeT4o%dLMeqpo6ld}jQ6#`De8`u^rP zd)<=cd(QJcufIQWF&Urf3L8D&%|~rHE@qH#i|5-}ukyfAW!^=C@M zcV7LkYCh}BoQCfM`Rd4*B@N%PEw-HQ@7wd8H4Wch`3B3EEe+pJ`9{l^Jq_Om`RuCk@{=9Y+TpN8U7i zU+X<LPH z4;M?w_p#?|qWP+tVo7}0`|0qpZQrR2HkL}m_lee54;Qz-8`AK7A>VlUN~htgq;=$4 zttZOhGwajK-f{GQ-Hr#(YW^=P-xbf7Mei;8YW^=L-!;!yO@AldXSOZ3ynKm$|Gkba zx6bAloMex;f_zy$-&P&Rjq|MkM)~r2zCN0NtLnMCqI|_X-)tSnA3Bao@|E*^FX=db z*Kt&qud3&pqT?7b$sR`)`RaJSdvzS|>3ZBGUt`ZVU)ST9u18h*T6w;kbsQ^n9M$A& z@A*3GIG)jQRG065&llBkG@fLSqlSEaJ>RQW>~;A3CmUaGT1U8e z8$Pp+7~}bF)jGn(y7G{4tZ-%`&v zM&&e9Y%1Rx&$mJ4j8tqU-zLv@??<+MB^8^?_p;|J`;GNo(Y)0{zBfJJ@4CK66kE!7 z(DP+@&z5)ZNgHpM@1*BzqvLF-c!zxFJl}X7=TOB~@?G+LH|jWxD7KdGJI{Ab$N9bD zo$~$a`9`0!?O&?cM!rzLe~e3w^G-nVy=?@`Z}QNK61N51>zd(87)Pnh(Y&sRylDf)dtH~AKMzQ^SI zP`>W+t@L~y<=ZS@5Bb)4zJ?kHTUFVi8oqIQKB`v4&L92onfDgkz2hw=-&cjLufKe+dA=vK9!k)zsk|z32JT|6sgnkIHj#3_f!` zns~m>XKedid=#H~FV)=h-SV!j??T3d;YueA_)=Ij!$EerL;jLcTXW zU!3~=5BhftrpkBJ^G!Z$%c=8;jnm{i>-jF|@8ELDKV7~rJzo)x_rv6WQodh2-zUdy zeY;QCI77bh1OFZm%4(dNF8@sVvUaC4%zZ%$(Q2!8l~8L7iZ&(=em{ke9J$x z{chGW8|UCN<5hXjw^7$4pW0_b8()0H+#OMeYQRq=i@WuP(9B#{yuvg zE-sL-x#!!Y_kJ!eRK0CI-_Lt(y)Hhb@;Z3F-m2HdMe=p`e7SYsy0{ph8DAdoeEoD@ zTwJ2|4fT9~>b`aHY1KR0^L?P>b#bZ6o8eQ+d-p-*Sz&$2I>imv6r3i_`er zM*e5yTju%Fy=t%92Rm(CA>VVJZ`@p)@8U{)*ZcWnqP=b|u1dq#T;sEgtJCle>16A5 z@!2$dEi{h1xF!u>6+P#>_*@#k#ajQn_&h#yA8q!|%R#OGU3@{lmp$JbZ`k8lqW-!T zpSgc`d%pa~tnZZ8z3cFq`)#l1E2nwCo5r{G@*VYjtybCca@V%kWdpwJ{q&OWM)@|T z;p-q@G5I#7;rm_3`aC%9rkLV3 zeCE1c_R3qY@@`Ri+tVm-zkE&Q+mVLvoP4e2doc~)N0sb#h_7tpOKRWGUi-e&_e8EK zzAWFLp6`y8_PpfJW8*9GMF#wP-a4o8^i#!G<;&>##^~=$T-=H8dOvrlf4cZu8or%+ zK63H(G<-LyAG`QQ8oqSr?Qy!eD-GWY)$8K!G< zoZk7Htl#^dQhZy!f}Sr)@BOMP?vbyA=WD8Uc!HiA_sUn^^S#{Jw$H_V@>TVGCH}NN z7x&9o+w*<&yY;#Fj(iP0-|6-CdboH1pBdL$dcNWMz0e}XgYwABMybB3ibv(^?fGh`Uo}-cCf@+h*YX#89Gw)8 z%Qw{X{Zz=FXBXeaXP#3=dA@dytk1<0@=fx5A9uGt7f;GJ)AL=cZ+$MlC*NYv*Fn!c zF20Y?tec+oe2w()3A%U+pSeHQd%g?Vj)a65Bo(KalS=&zDo>xp+ptJ)W;) zBU@e{#Si5>?D>A`XnjeYY&pp+yzl+2X{kzm<}7*?@8h` zh{qoW^Y4!LLf-@C-}bwgI1AqxvyFW7@A%C|&w&u>cnCuT5+EIzf3tcF`H#X_7zg8F z0!)MvFcor8W*6uT=HDB9lJXOY=^+V{Ap>NDOpqC}Kvu{G*&zqygj|pt@<3k52l=4@ z6oeEg1cjjp6oq0?97;e*CG!_yctCEF5x)sco$B?VK@T2U^nc6x8VSsfDhmd9ED?W65fON;S{_HZ^2&J z2m9e2I0%Q}LpTfP;3F6WgW*9K0z+XK42SV>9xlMg@Ckeh7vVFw1fRnf@FkSsJeha2 z3B((qG?am|P!7sN1*iy>pfXee^Y{7YZ=cP2!>lFDI>D>~%=~WVZcb_3%aBO=QQ~iG z_#GZ1{XF}<0Bd0#Y=Dih2{yw+v}**6gi-J?Y-8W5OwuJ4-KFpG=j#^1e!uKXbvr)C7ACRn7{Woe|!HSn7>Ike{XKyiF`^l ze-r&BJc_2NaT{stCfE!^(Su<==>@P5mO~!=d0`=>XTKz~FWWcb4HL%_Bg6zq2Z@j# zk{}r}Kt?d@i}_T#3I8AHKjCK5KeFu_T!G!x@g}?lZ^KV)`x}0NJ?Oo#5B9^)Y>Oja zCB8#E00-d^Y^Ur8sBZuaguyTf9)uxa-kq8E_~schcu&thNss}O;Vy_m8)yr6LOZw& zC*UN!52xS*FyBo$3+B55=6#!a_jQ5v$6($wnRi1E!$tI0@Cr>Y0Ixz$s0eJ13(;=06KsYpuobq!cGv+g!b|Wn zyaKPnPIwJohd1CU+Or52!xDHJmclZ~M|<)^0VoKW*p?ZxKvu{G*&zqygj|pt@<3k5 z2l=4@6okzjM+&hJ6ow*D6pBG{C;`*qNmxMJ{-jQ`7B%b4RqVSON|HB@I36azL@33! z8=y2yLT`gIq_466-(XI72;w0O5lDb^&=2~9c_uW^d*)frJg1dq|8h_sD!`3U5h_7t zr~)@ZRj3Blp$621tkh|q;mq@ydG@-QZMC2QG=xUb7@9y+Xa>!p1+;|Q;SOj8t>I2+ z18w0hh(bHK8`{G?a4&Q)k-E+M2Jea}PnZF{pf~h^ zzF^kwW?jC}^c7ePTR5d#p&$AEAscm?HMv=Do3(UCbS9Vuv!NE=TVMy?7vUv%8Oq=- z3+13ZRDc_yB2`xm9L1xlfz&yK}=cQ@1)jSW?gNYpf0`g|S8kh-7*!DCmg=H|3@dg<&up9)hPR zZxJkpCGa#Xg=MfDo`Dsx5>~-#cov?6^{|36R>CS+4LRBND2#=1@GRTbz;p0C5O=?wdk0Xbvr)CEN~oKr5IE2RMdWAG-t1#iP1*bDn$KfD8D zIad>561+wmUWYee7wm>N;VoDS%V0Uogjp~f=D=K-2Ww$6Oo7MXad^U{=?9O)X6o1i zFLGU9f|ubH_>R2q;Rl$Co(9ul9NzIT0Vc!Sl(h%;!amp!@4x{#2#4S>9D$>7435LQ za00SZW--bs4)4MV$icP}#FOwIn0F56jmG~Tybq_~G<*PO;6peI=inna4;SEL_yj(M zi|`p-g3sX#_!7Q?ui-L$1K+}T@IBm0y+06tgrDGNxB|bxRd~cedw(VV2EW4}@F)BQ z*Who63e#XZJP9*kCd>l&W9FlI zV8*N`Da*`Ro5?iux*5|_z|8Mk*=FV}Gv~Yc-&}Xo2TWfu{lKgN4#IIb3di6O9EKw> z!Bk2#>kHFX(;rOVHT}c%6VpdbAO4>ngPCu}&^~jIOr+@B*tf1J13ebZxNFAx3i!-g zwIY}~%dB0^8nzr%27jLW=NxC|Jh!&}fbh1UG^2}J;9GXBaFypBiS8s+}panDoGu}>xEnwy} zGsl_v&CGXZe%r?OZ>aA8 zQ3#RWhPaUU67gjNb(*=RC=`b2Pz208&>!w&e>2z5WuI+e=J?g?v!r`=^q)J;Gp2i*m(x2o2cmM{1nTuBANq`}E{(#@1I@=fEp9&+%8^g9tYzvXD zLaYY$ImZnkGifuQ4aHLmN`hGvnKh1CFKvOz@G#p8<1^3kg@`kWvmk|Z5hw?>;Z`UL z<)IGD#xnCvzO z-H_uj>ol{DJPBqkY1VVQ!L2bzlb!-^!%+5V0QF%oG=^f7W!A@cQPw}##cj}LyA;0|;vXbpEl3fopvMj>KhC;~;{dGcO>OVnf5 z-0#9jwwv|uBT$p{%}@(&ft#QX)P`H3D%60c&db~c*a34Fwai8VG5)p{}EyXVq<6o4Ph$VronW` zi0%V@p*P$~dFFjoBI!HeUT6-jp%t`+HqaEB!JW_^%)5%a;dY2ZJGcv4LKA2K?{jW* zP^Ve@nl-Rl8=nNTJ~nIN60i{8pYR7nU?1CVB$kDDNY@}%gz``ws=`fB6DmM8*pIIq zu`*PFtkhYFIF@6}OUw?rpde&}0+0i8LVm~%c_1I8hYpYgUEqG`2%Vu5Y^2OY>PsLU z51AkfWQMFTpL3ak*bBmNGt`FL;8v&ub>SAM1@)mGyg|9A;0&Atvu^(YK7zC0*7@I( z{szo?|5NxJE`xa<_!>R|^L%g-K7{w-G@JzU-0>Zp2lGsDf%q}(0`pw(3tR>BY~Y?3 zekQ#eeudZJT{r>9;3yo1;*^!0b6$*i9DM{1!CUY*#BqFo5&nQb;TpUKZ^Irq00&_| znCGs2uovEh-{ChnLiy+60(=a|;UoAG7E*=#>dn|q*p;Qs)92Ck9*hWu|~J9z_%li)Jm$;8*le-(DZeDV{B z>)6k{ceqOWSGWRCp%=l=q?ZwYBmM$6kj?_xATwlzT<|93fJ~4ba>6<4JPS`#PZqd9 z`dPNGfmJXc%=@#I@H9LJtKl1bCxGhW5+Ou-8juzDAoPax&>6ZwKaBTsZ1HdtdItJ; z{J+5$u#+8MgV*5=*af@cWAfJHH}9|>CS8wh+0b8;&Q2VL{*ZKk%3A|plFm#VPJDoU zlJGnSgCH56hq*8l=D=*22eaS>Xb3;C@8@s~I#bqm;)}2Y&cl9q8Fs=;un%5?SJ<}z zyb3uX7vzKdkO%TYZdd>np(Jg;kMcXgJlbmB&-EtV2j)}WQ?LjY!xDHJmclX^j^`S2 zD-?ov;7#aCxzC!i;RYxLr6B?ChdJywf;b9B!o$#$ZHL%*80m*#Cg}&*wu^Waa#J*=tIi$_+FOR}8I1cZ^2{;Mw!TWFuPQwRq20ny;{$BE7^Z?qD1Q{SbBtswS z8BAUn(!m&b1V+Q7Fb>AUScpJp7zF)cC=7>TFa#ci0niV6L2u{_eV`lM4_%-u^njkw z9STBSs0Fzp3)}*=ArIt*tWXb9AR82d`p^IhLmkKm?cqjf4He-|xEtC*Ludp=pePiB z#?S$tqpe@`5RiPS0p*ggGQg8#5hL&(U+yP~vER=&* zPy;eUWk`lZNP_fG9x6Zv$O#!C6V!y8AqV7wHc$mRLI>ysWw@?&VF=WNm26uE%V7yL zrJpu~=FkG3VcXNN0ve-7LK7GTOW9VIGFpl;YI4* z0gsS2KmEyD33KsngVm&;VBhKR7b@G2KJjpdJXA$#O?4r>8W_1gvZg(qTe7s zN8E^iEpa}22dpPO4c`oS0{sGd7jYeN6W-aR|G#`jFHN2Q?YX=I=cp5Og!{lek8gn@ zV4lk}v+X;M=S|vvh4?G{0dJ$g#GA!@Du4h#J`C@zyZ=% ziNC{Na0q=6ekT16F%JA^-!W{G(L?Zu%4n03}=+Gy5L zL)m8xyhwTyZJP}LTsQr1)c&ztZf*lJcK)Yvb0_Uu1Kr_U&ci>)(HU$rwMde{IPVLQAE zJK=TM1e;+C?0^^HC3p?qfcl(^!Ib+c13;IESn83CwPz|1fUeF$fQf?_I39rCQ@FKhnKU3CQ z@EvS{$uNz59wx7!DTBBbwm?Djdg2Dy2%BItJO|Ii3$PZ}!9o~ASq~9M!v^;KpS`7U@pf|YpS@)2)hrF*~3I4x`71?$S%0XK=0VkmX`~Ww?QTPddg!0e^CQ@z+ zaV6yxA{K@sP!!yItWA`^0XD*BxC9@-yD$>pE5t0sns77Jf?MDwr~|d(R;UU!;7(`? z&7e87fR@k>nm}c!3-zErG=PTC2pYp}a68-qt)Mkjf$C5VZh$D1hP$9FWQJtg*$uiv zcbEY^peJnN_{ZXT6mr5im;mD;7fgi7FbQ(Q6iCN@j}RLW8$%;#2vgZM4W>gzbRXyo zz2Q#kd5-%kk@OvKFEoeN&|Hk{YJ(Tx0i(IL3((V{a%7s;9kyo2e!RTI*Hf??uU-h89Kp6u0bOC38dp86J&wR zkQL^0eHOxEcnUJGtrvu075lD+n@QJ(+u&BH19jmRs0H<*9(1IhPH-P|hQqWkjxygM zoPslO4!(yE;3GH-U%;2}EqnuC!Kd&!T!u^VHGBe}!A1BG-iOn065fOF;5^()-4}=- z!!Gy?sdpZHLi#=U0=|N;!MvLr z4a?yfSOFK=ehEH@N6?=U&%!zQ5Z;AP;WT^zXW*aj@>X%ZX0zWMILxtKM)#nNQ%uDB zBUf2iw;T<>t2jLJb zV&5}VaG3NFI10z$IJ^rd;3T{U@53oL4IjW6_z=#*Irs=d=A6TL${awOo**WeZfFM$p%D~;qEHMPLlbBU#i0b0gl3Q*vcs)#8x(*_P#tc9ws03z zg=!Fm=FkF4!3|IvTEgvc2b6)bP!3u_4af|YAsG@O3DQG(r~nxtCuD?7P!n#39FPmz zKoz(T?ty!uBXodHaF%{il|HeTKJh&aM^7NmB`?bM5Pj)MbQ{v&k^YhN4={>!Uzkn0 z3$ZKQ4+qgDVG{nn@F&||psa`3X5OPc29LuNFcqf3ba)bGz)YA0+wk{f{}Jq8gIE~` zkv`7$fy80NXDN3LoQKch3-}T)z{l_jdE|K0PGP*=2CE^pJDbbY7(UdIFl&sN| zY|)hL(Ucs~l$_C&BGHuM!D(9%4aG;(hkHQxXryd3!euO%G`rN}bABw}JybrLQrNVN z3!4y)WQ<0FKJ&{XiS?6B%T2-@%aNLqu(74sbf{d3aJ=~lyDk%sovEzh^wE@(RGpBJ zXg|XWA;ow_w>=`Ao)`&@uMr>06-(t-Dw&R9bCbhnKjTXdyK@x@MN+8N3iavdOJte0mDxh{|~P~u6R##L~ zR(H~qvbxqMWsOVSIWBeYxYXU_QumKbEgQS%MwmKeX&UjZ|dm_r=GrW>gkK!_jIk)auZU^O_#bg z-9NUb)|M{yVx>#1EuCtM*FY7g+tCJXOt31Q4Z3s9h{iCOWP`aiQ5kH=XhWRtyg1!? zaT*olTxBXaP9?{w=s1-fr^4e@dYp=nbLD4JJ((4=C}y=Wq7%*%!1a2RBuT2 zhE#7z^~SqVT>h~9%*Vn0VYMrws+~z{XOh~Pq;@9Dmn>hhe97`9%a=jEj4sNbQT|FUYQIYEdQ@`PqmsKGmE85H z(wghWR zlL*#HCXvf598Ds(OXP8hye?73CCa))IhSCyXh#1dHEru_22&m?oWx1ed2*-A#gI#c zT_WNV$u1F&Z3-v2ZIM)eEH{#xn=a-{jHOH~f@N(ab1ao5mdX}OWsjwD#8Nq9sa&yC zZkGzh4k8pE+ZJnIDAvAEtbL(a`$Dnyh0@3NjkPosI|rc*vD}QYRHj%ec0{4r5rtxH z4#nCWN^NtV*uHsVsr<22fmo_wER_;V6^f+_$5JI@DR-&6hi-^%D;-OfiKWWMQmGFd z6=K_NjHN2ZQk7z<%CS_HSn8%&s%k7%EtaYtOVx;_YDOa!%)M=HdY&GGPjmB=)~5Jq zqu0Ciw82|B0z3jI=~WVJV}rY86YEM zg3OQ=vO#vp0XZQT3vPkhPzP>>+n_FRUgGLQ184}1pfNOorqB$ULknmL zx5FLK3R=UR(1z2>ISg_BLY%)4=P$&$3vuoeIKPPo@)J47iFgw6B;rZr94F#S#Fxl9 zPUIXXa*o6J!}!Da!}xgtVm^W`!AG#&WP~Xx9Hyu+g7pMfDENGFg^PbxhpJ$*4K z4(yhmz0z-Zn6Zv^YS=BnL+N_acg&+_&R`i}Q#?8+MI)MqHS3173dyClVlEy~3$ZT~ zaW1#=47tU8$SvhVZXqAatPwZf4eIf3P>*+mI;+ug<08Gv^(tppwPs$iGY2uY!A>=7 z(WDYQT``FSk5(qS?V4-A%>#&u=IO&k^XOrsdG0VVCx^lcmybmAQP6y(n2#j$k=}e1G9QJ_M-lT;)O;kH zk7DMdxcP{fj|B6P+kE6PA0hJ*Z$7e^kG$q1pZUmdJ_?wRu=&VpKC+pQ66T|%`6y*R zZr}t*LgpiEJ~-o{NP_vGGlU|E<|DoNNHQPE<|Bjo$jB?0xM)hzXiD|66Vgvx)Pnh+ zsXvwsrzRue6_pdhp`?&vF2yp6EsFel%!}%!DvcK*$MflEox{#Sx^Lu z1%-|26zzhI7kELxOm82H%?qEP7n+26Y(z`VN~w=Tq14CIQ1DXAtSJ)QG9jN!R0u9_ z#CF4o?Q9Ws&q(mV$4W+>GvXfg+_SNJ8je?8?m0L0@+AYaJI!ExCGx*FEeC7^5^8}w=gU@ckXXcO%aX~1gU^f=Fs<2d0Z+u}} zXJK1u;e=qn!s%>C3_=mRUlF@Q5xYYXyF(GXLs461QEMq`EyeAA#cf`3n^)ZCm9Tjw zY+ghv1D9td8$n!c6Jb*bR3 zsM!*{6*YH=zi{Ih3n|{HSW&UEVwIq( zGfg5H=&$_F!F=e`1sBbI5d7-E1}9C2XR{019NY(NHn)KZDugjC(B=kZv$>;9P%dwi zjMk^!7$1}I=I$hy|6D>&1zTu5FRcO}uP8`_$C+otN#X9nhiPT^f#zcn2hK7gr04js z79L?e|A+PbAJ$4Ftmpr*p8vyczkIfwaDEf*Tb%fC@IV|NPAJ!7K)D_RgHt@cR>t^{ znE@gplXRmpFYiLBZw*7KF9kydgZX6u_kv4sr!gO~#ZE;e>|8`5!DZQ5i7kn4OL~_` za*1S@$lwwgT_Tf9WOfPnj-0oU70dM)RIbP1XypEAq-!+NEgI=wu0;B9Y$fF;u6Q@f z#Jf=@-iu-q`;fa}p{#UsbGztvFda$v4=acg{EsY_xIy?= zic#5ZG)`mVhgFM`v3&FT!fp%Rzr>QM#j>5-Hr4d6Jqc;Lt-F8i@8>kg#J1x2_x-T-cy3X1T!_<;F&m+TikI z!%=Lc(qPDmGY-$*Vwy*8K7j2CH$?po<`d|DDz4CW|^q7%Yx-Rkm$N#|@aOd-XVEz9s z*Es{bhS~eP%Wxa(#`*vC=5HVEe}2yObm^qA;eY&>VXXNtJT^-ZGmOnLH`D+5U*IsF zuu09%l=|QJO#ie0{!wFiR;JYd#%21S{pa?t8|TKhW0fPp_&2DEjrq3O@#&XUHuhR= zMioT&3EI<-gQ>%&!^(b@31;S z1#WxEYV$Xh=BJ3tOSCd>>K8UYz4V5SRwqeklRsHHVTH}lAYEBHqx3!)?0rT z>2lIprEgIA*`!@?$M25Y9j`l1cYN-+-0`^MaP4>PcI|cTboFFX!nN16)3xt!?`OaK z3%1(h>RrrUN7v4Glz&;*HDB1?H|}^2D8KtoyMNwq{=NS}<-ei)^M6^N+y9XAk14*X`iW+l%U# zX1h6so8)&sKmN4Fw$tCf@PKXKroZEZ`^cRKcb$99vH8c8?;l6O{x*M5hs4_uKNEPAjf_&DKBjFWY|SbM3FI>+jm<^4)nHU&`+P{hxOK zsWRkM{B@Yk?@~TFc6_c36Dw7&a#Pi6)oawexnBJS4I4E!+srx4rvqO)+wPzB_kSPH zSWka1-TDvf+of3PPpi8qn)3hovq{@8D4M=t@^}4Z_xoP4dbMgdRr|+|W6Pxvj%T^z zKeB@Flon=#856!gX^(sCF&mA~-18>i9OCuz8?yabsGCN9Bet7%nxDEKrIyR);AbYE zX8frO7cSWRLI3d0V*CC7A%8Y`<^Mze9P-Njhy1zZmHZF+^T_M^AM&3e&mP_1e~&$5 z+rR5n>Ty0tzBxZ>o!{q`pVs+(fqZlR>w+KPEpT6`Oo@(SKP?ekSGD z{a20^CO_rB>|aRvE93vq0?ht}mA^cV{36PKCXM`}%3qO2zWLRkIX?3}75~T7e^&YC z`z-#C$v>xj^IaGJ$K;#uG@9}oU*`mx{1=qp=9 zv;83$2F5>)gA9R5FDKR|E+j6AM}yh^G|^l?lU_#&39ML zcRWqneBX5@m>(0LgSw=*5T6Ir9+Q3nOnXdvEtvM0^g1x@G3oVS_T5U{0A^p4-Uw!2 zlimboUz6SpW?z#w-yJjk@&ud#(+5w&ZR$%V)>Ya>^W8x6y?Apz&36QwDQ&(h*j(w( z#1=}s@5P(%$eW)^kJx#-w&Fy^rixt@?^0Z%*k5s`;x5I7V{CaR6-Q|MbBfy(4=J{m zuZZF%#mkDPC-1|dmfa2?vmBRrNYzM$(nN_w9Bd|xPNPrJ{opS4z?cjZr{`DC#2KsxnsI?vW{<_bUpc5s|LqA z_)WY2M(IKIt>*i^L4L7wR&SF2>_e-kN*|tU^^?*C8d*I?`md!{ua=&v^FKoRHT8$d z(k+$0Q2JF}|E`mUqtb7x{E^a&HU7+%zDMK5gVLvNx7TyC^q`2<%cQ4h|5eg!bUwC7zo7eTtn@;) zf1LDW>1{5*m2K~M=~o)t`W};hUG147-KwR{A0qwUX}dq`nBaUAP=6dIJtBkkKP!EU z?w1A9&83G+pOSxtbVuox(yv#v>)U%rT zGtm6G##l2ikCo2X&T8hLAiv(@Rx_Uk`sS$Bk4Wz>Wi|6}knirdG13EcJj{1N{{G3< z&wLr^HY$(xK%n2&`C`5f^Z@mL=G#Djt?R*j80bILzLC-!RQ^A&tw}RphfUnB{=7;0 zfb@RpwK`wjrGIN`+xwREK#kw@x1hZFI=+vkm#RGGiy;4k^d9MQ>R)@Mb4kA>{Y#3y zKBJ_|>iFo-L3yp4+WdEI!^uPed+I{`8Og0e?MIx=IcPe*2A{vko1!}zJt(g|g4dE=$sdf}Y3TOXW}{%F7T&y|k6 zV)bt6GJUOnMcU2R=cVse`_4)~uJQ3B=|aER{SQme)BLne`b(X^_oQELV*4BOV{p7< z^d6e|EYKevv*pc{zD51Hm-Kp_@7Ja0RkD8OyI}u&)V>MQFKYZ)DqW_b^-q;9to$R= zuWYmV`=npaYuo#_^u4#+eCC&+zDsvmJx_X%$~z|gg4)OY9r#D;c$n`3JzxES^-`eS z{C_|?sirOeRq0Eb4^~J&)!XK8m+m>kYSueJdDoiT_Ro?YrtyEabVKRB(vL5({!gWI z-(mG}=@ntCUzGlBjIHma^zN^%J|+FhPgb9nUiYEZC!~LxHAzaY?p4&r6@(YW*XnFTG>+66stAtnMfMhRT0f`WLltwDhOy5B;SN>v%3mXH)%C zq-#BD%YQ_==nq!2o(Zl;F5Qn~q?aYz_P-`wV!HLeApN!a#{<$Yp0oLHNFP*x*d_gO zC!5by_8)(G+E-_BqdHqZ<9EdD@S)~^?zcc!*ZkoZ#(W&)*VOup^-!SQe8~DC(C0GS z<7560bjLonKGr{hetwD7tWN^HsIB$0z6o?4<+HvC^w9*H&w4D-SB}~JSziUZipJjw z(idi0KkK0&|Ee_WjX-CgW%DOWmzN$Zeg7p}KI@0Tf9GPWS+51UkFFQ%wLsTwZ1cxS zKT+Fi)+<5&%r#cCJ`D6ajR&lE0zE6EZ4c|IKp)llV|^LuO;v0@>%l-bSADFv0-Z~m z^-!R*m9_P;UJGXxpXy>Y>&HOfrT*{tj|c6q zLSpM>`dfnWonENW1mq5NY?kG+5d_FAb1Bwba({A3x_OC_m*hn?Fgq)MTrrPu5Jep32< z%~xZk(_ggpJt6)5FstbwL3tyKT0KMha&xP>zk>WMxorL14}or5*XDCS1bV`!R&&1w z`pt`0GadxGm+~1e0{yw>GscHNPq^Ru>HmRt>x0SCIX<`f+|NON%1o=LNKdbB+xM7s zcWLg=z+bhb^*=5>R{7j7L4IqEN8E3Lep1(q`yYP>rn{ggEQIq;`{%=#Js1KmU8%_ZsMzuWxxrF(s3HRnI@ z57GF``3rO{<#YZ5?VhJNe}T?8&+h+$bPug}IDbL@Z@T}u9|P^ilMT`@X#KfY`l!~Y zoUg#&Ti0{FbRIoVuakDqGu*F%KktLKJ@oHD|E%=|{X5X(bw0UY0zLDh^>h6KJ*Jtx zeq8TBU-{hTbG-tcUGHwiIdllsd! z>0ebo<6YoiF8@o?@2UKcr2lAX%X?XRl#ZA2F4#ZulFdIWz2Yvb8Lxu;z3PA5|AAiH z*5)6UE~@$Z73qcxZ2qg#i!@%GmhPeUe=2>dhxIdF2jv%?V)Ys6t2!R$k0AfN+QWDk z=tf^!|A*4(8UoZ|QzMCOuJ_@i3@wvEGL=KLmQPj_)<; zWoqwK=~*Xi`=&{cQGePay+G}MNBV~q)<0c(b}y@6mtLsr$NUx4e`tZdUye&B>Ufx+ zg8VkRKJQBB*YoKK=?%qf`6s1s)BZEv{yN`pNOx8Jv!wg3hGm`_GqtTkE~|q*K)X1JciEd|DuVTGw}>^bL(|d5fgo``6vlbJYIx(j7IP zESApux!wPQ^k=$%mq;&G``?nz`iu21lU}3#aY}le&fjO!?)h!GbVIf08R_BsZFwuC zx9fVWlrE+9-#+P)?^yqv(hsVCt&$!j|3zuHzFi~j)~|0%Kcw^hv2;4kkNc%>Re8@z zPgZ^Y{5gPs63}cLw%WFDlyuFdw!gBz4Dz$-{PO%6=o{X!{_fKI+F9L0`oY~+v;GbI zhxL5=fOPjKY<^GatCekieWaiL!RE7`4)*^>>qpkZfo`_L=CghebQ9(Gmu{~6d!%$o z{b#iF5bZxux|`ltJ|f-mf-QfTbfv;p^L!lCH@uo{??cjsls`hci}HI(msfqP&x8HH z((~$2>A{z5`MssJrU?y z(yadi-6NenpR8vB?Zyk%^MO90^*!sCK)d%jtXBfvM)N)ElR)34^UeAu(D!Nl_s?gy zxUo6Swrr{E!$4n~W{-#Gmq7Q_^RK@@_eXlOZPX0wUn1>(-#k;gn127x{Sx@y`f#=M z1C8zev!&0dzb==4@MD|L`<7t;SM~mDnRKnMZ2ptdZv9Aq4g62*{-(bMy0p&sv(l%^ z+WlupH&Oj_q&sLlr+)|gf1~+{`zz4zN^}1Qx_wt$-gDB&HJ&gY1o?NWKRqwKThD`Q zr5jGR`!hZT{+2DQX8Z_rX{~>zORv6S^A||F_tQ^EyXSGnn_&M(E?a*u=|gpG|LZMX zb&t(oFMVF^VSEhse?t9jsI+_E&3F;yyZ750q`%&2%X>z8=2@%PNq@D<>PgaTN?Scq zdifBm8GnQF$CR;pqx6>6w*8MwKPLZNY4`iDY0~cZUyn(*(Di#tI+yy#3hCNw(hl?@y*myZ0Z9q}}h=7E1rB z-~UdQEFsV|C~89bLP}Mv3q-q_LB+kruGO;I!90O`nzIHplA>OQL+XOZ3V{o92cW} z41ebM5#>kdn`2e#{ZfGyL4qIgg41o)0%QF+cBB4b&m0$=Dd@0wjyF=gfc`k1O!u2$ zPaMm50sG;2H}wa9;JD>?(I5O<&>spMNH~_RzYrfe{n|4@$9i#GhvY@P;rKJHPY7Y- z=EA>12^S{}{qpu(8w{?IE7x9488&N)=O}G@bM||h>W)($y*dNDg zqQieV?z36Y5g$2rB>UAdJ^<`rIQ`sIF&|vNa4heSAfIzQk>-c}4aZ}fiuSmE;Mi}v zz}Qc497g&=zUDZ9;u)@II9@^V3)e3kZ!#0}#eRX~YRyG|*gtVRB}`ykpKm82e^E+JMaJ+)<&tX5pvEx_K-&n#6>G}}Yi=1xfE$G-!aeR!f zFK~Z^;|DVYeIemCWM8=67kmKBhu^;6473o9Y-r!Hx?A%g{FYq6ZqdSZ75Dz&XM)uT;un(;#@&~7X zr}YjeY~Duj`xDMb_PUPnIoj{RUpRjaikG2;8yyt<8p%&Mh_Dx3AIu`$x0`5>{U-0P zJjGAMJC5i6xZdD+67AQ95&c-O;9o#^-vNQc2s^(L82O&_e_1T>O2WRhA4ELk^ubRA zeHr17q<+vODPkx%&e@_J+d;SnE2 z`(VQIeirf@Z=biXpd-F>TxOoY(+EF{7C3v>{S<{^l zWBI%t`Ih5Pr0=za<^2@wPdL35?H^d|&_3||fYXPkuD`%tDSjjWaJszy!u2=D^8P%o zmpPsmDCUp+!|`^CUx@b{OZ!H==6LdK(H`-T<9^g0@t5O16u%IEIqpCh@t5O>rh*^w zm*bn$1V;Sj_+XI0h`$^^r1>GrHrkBjF$Y2-{Kr@HgIm z9`y%*;dnN+M||Pfl;WWe;b$*}Jcu`(ez%Umh$kFBwG$Zj$#H&)Z}4}Hy)O$o@(af~ zNIuv%$E`^oU&8YJF8D8}AHOd62NPaF>pg^U)lY&B|KaV6Qh!4U+md`5!pCTQ_zQ0z z)J&|`6vBR2MSsX2oSsPQhkVCzZn__W{K>H?J)c7S;P@TIJH%6t3s)8W!T&hkN9#F) zuyKf>BR_L`H?p^>gx|Y~`62#s`VGP%gbR?qkY73d67`Sx&v6ye`Jcme<6IE?mRh!-4>r1>IV zaGZ;-FR@?bxF)S9;s?j|$==}491o!O@L!Hc*^2oho^b3;`bYfWxH8Qb@rC1@4TZdj zCmbK7_=$MJu^o+vc*1ed`afR}KZPbYU!QD(?oT+7;#CmgfNx^FX@qB&6d3Et`Q`Ni z_OBfOLHj4H2gjy#{{YXIIL=S{LpnzBa{rF_$gx$Bpksf{@uwzY z{jlB~+tT>R7aYs|Jg!$bUPt_}2ae_b9`?xbc*_5{Ug0=|;s>4=a_mm)hv$174km9%<=C>E;K%b*j=vle82-WWf&&6iC0yoD zfddI=qvuudAKqT>|FwOQeMUU8W%VWUKg27J_bw6rAs%r&o#G?n3&*=Co(B>>LAVd$ zGL&CN6856_=}mYp`7`1N@2@)TXRv?bI2+|#~#R{T2{@ z@j>)IpK#z4ff3(%`^l;CM&Jje55!YW&$2_bN4(}Z(pg~S503K_9r=mlIh0S4k2to> zF8pIQ;d5bve-7cyZUQ6U^7gqZiuSVz&sGUKvlZ?7Io?e2A-;1QyHoHZK5;ydu3zEr95UA?VT{M|LBg0H$Gd8Z^@2V) zK1&$-=J+mM|3e=fyL}e@Lw=6WeHR$(%WMCQSxWlC&#T5c2m9q%z8{AC#&HGO59sR$@iYfJx4MtuN4(@XjN(7yF~?2M3VL6{E(-sk*Q$9p|-z|j8G!^u4iRtzq<)V8cd`B)$7K@p*+*DjUt_&FUEcr2^^xeml<02=;mNe07)tmH`Oh%I zN7jh;y9gJj^+Z17{k5d{8$~$F9MK-vFPtvF-*S+!{66dtVAJ}U`lqV}r@&!_$0YQn=BivGtCZb|yU^(Y@-aaquJ z5PsB5U|c_QdX$^MTL~Lq6L>t~WV&BGfp9;{&l3sD{qZKk@_i@`VR`?25@GrM7wiZ4 z_`6Aen+b2JFUFrj*uS2@euRrQ7kCZf?Z(2M))H<}SJ1Z+4x#rS{3U-$K@T7-@0Udq z?&2!wQwhua_xlOU=Z}GeWxQTTc+E_~A4E8s^m~A?e7-o1u#C^s2|p(P4JIt_&(9!y zk@}lScmU};gm7=l521uB6CL{{zTQmec4v z$2=1Jh<6+xBKk23vteNH4?hOmZkOtNSnPPjYG2l0*b52f|kKzJd= z^HqdD9uoYBf4qHUae-lv97}&ee&N{i$NO0v7aAtoZzj5YK7{3wNGhJ3_vLz)lb zE5}<>pAQH;pVngy(Lb~i{ZAn*-WC#p|=G;;w7iAq3c2T7sp>p3OekS<80dn_9bjZbi_+e&rSTp2**W; z_TGfM5gqY_x4%pNj{L}R38Ew3a$K710r`^SvTa0vh^HLSIUq3dBgc z?4M(Ke^PIG6qrwbc5aS~=nw5V?hu$de~yh)zrQ8u8y5;X=Evy`whD~#IhOaQvECeK z4-s_Cm*Y}|p&yPT{}gon`Qd&CpMR+&*}uSBYYB||51cOV590m>$Bo|oyg$gBi;eM& z>>caLvHU(P^vSVjH(}4C33sh0`h&cjeuVB%!5%nHr1wLie~#~vJi`fxQar$VaQZTe zCs+@Tzn>QUWBoZUpHqyF_2hUx*&Fg3#|2A^_OLIGU-uGtEa5z)FW3{OXQlj!_2$@z z=7)UB@#*n`AO65`X}bRc`{ei{$qWDBSnh8}5YDw%@Q)(AmiDXgAKqS>#{C4`pXBv6w| zZiS`*J13w2ZXzsy?`scX`8*i%^7iuk+cOByd@lN* zNq8y6rwGCx2Lv78=ivPE`@2{ljw{jq^HYQ;kp4CkmcP%nm+;c7qQCuw=eXA(`gGs$;>us7)s{>J%3Uy1&3KbPZgw**ckyy~XF>j}%> zGm9ZC-zPvk;r#OVQ8o~kzh`!waBXf}c>ciU zb)@HCcs{{#ecBJ=c?QSw`$T65Pp0)aN?5)xvYhZ4g|Lq#!o8^d9KuI02>LO?^8J#N zgq>)8XA#~<{;`m-{QZ_7!prG-Nf=@I`z^N#%l*ML!sltff_%;8lfTC@op4@?AG>6G zdcHlEu>8H2d4$vYy_Ukr-DX&yR4qvrh0ORPSDk(p~%fHqNT3!QDLMfchp3~lslB< zl(OsFxmshGa?kpO+%c03Q|{7a=Sr$&JWrV-pBz#Cy6t|_R1%+A@7>fa={q5Ai1+P6sm*0*zy zX@(is=KMnLnCXTo_aCrr8PA!W+bFDgoJ%1)SI+o*ax>n4)8T>1O1W7L^>ew^vxL0i zR7;sc>4W}#2TA|8)R{0*xld(tX0mfUN676-HKVb^%v99>_P<^bfXT_^XGcbnyRzQV zPw%j!wUE~>(hs5*3VZNfr0f4z`06PX|8_qp3OS@7Fk3JoEai4wY?yMF)03OIH(LiO zw`z%D%54uBnVwUx*YxC8FEvcL&0*L~W?7_vtCtCReW^wherTxvw^x>sTlzPXUHZ3_ z+i|(Bz70ig3q84|e@nSl;f5)86 z9crL*7lV%FevQ?QNaxB(p=VF3ahe~d%ph<$t`qcRs?{5G?LxHregBrg)MQ*^ayOQk zj8lj&KOjN;$j zNoE$und})w?oK2xvvVf51q!qCq|L&4f zmD_doZ76c%dqp~OOaGQ~D|Q&B-1vUhPyJiUt=egra<4?PW-(2c)cZ^aguI~#b>%i= z$=v{swB$}9dBbB2Q|=X7`etfm>SV>2^z9j&vE-KePBKurS3$WP+et^?>NvyH zcUp2weH*IWtM&9PpN~oZPBu_|rzN-4x1q{C4GCM@zxDMU8gH2XotE5E--aspj9=(m zeb_MdotE5E--ar8px$%;)aOLBUrRAieWxY2)VHC^y%xco?bXyotOmijhSxkG-TZ$rH< zNlR|2Z$p)P8eNY`|IR4aV`<4P^=+uvV-rc=>{^Vyn>r$^Bj2N^91+jGm5HKGIVI}H zhzCDAJWuOBC;wTS-|1Xhaz~vvY`G_szS;ejjHK_h$1yEd~DUvUUxaM(5H%{kCBcA(S)@}DY#qOWYB`vv?R}5S3*`#kZ79}I;J1w~l zUEdngx7@Fdr2U%as<3n4Yr1~ho;m+dj;LF@?S9haw>p)U+~Ic&Tka{OZ@FK~DEdxI zZbR31fL8uy`?ZXs@3iDLbbaT5zUB9zS?x@}D;s)O#5Iq5dT~09c>Y@j>%TFXoFq4k zYZ*!ITqL(DgULNf%g)&|!Hi<(ep+^(k>sALW#<`5?t_qd}vlDe*LX@AmX^@ zp{SFeh&m;W>u=>#y||A)f9E4Dxs}fiTkb=oZ@E9uDEdxIZbR314Cz}1XJq^HjH2(f z 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 K|Ih#E3;YMX-lXIJ literal 0 HcmV?d00001 diff --git a/PspTest.sln b/PspTest.sln new file mode 100644 index 0000000..1f27768 --- /dev/null +++ b/PspTest.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33516.290 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PspCrypto", "PspCrypto\PspCrypto.csproj", "{A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PbpResign", "PbpResign\PbpResign.csproj", "{834B9F97-1B63-48EB-922E-0FC155BB2481}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PsvImage", "PsvImage\PsvImage.csproj", "{0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PopsBuilder", "PopsBuilder\PopsBuilder.csproj", "{D1DF66DB-52BE-489C-A292-525BC71F2BB2}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A60BE4C2-C0D2-42FD-BCFF-631B9AFC62FE}.Release|Any CPU.Build.0 = Release|Any CPU + {834B9F97-1B63-48EB-922E-0FC155BB2481}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {834B9F97-1B63-48EB-922E-0FC155BB2481}.Debug|Any CPU.Build.0 = Debug|Any CPU + {834B9F97-1B63-48EB-922E-0FC155BB2481}.Release|Any CPU.ActiveCfg = Release|Any CPU + {834B9F97-1B63-48EB-922E-0FC155BB2481}.Release|Any CPU.Build.0 = Release|Any CPU + {0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0F4A59D9-FF92-4B4F-BDC4-4BC473F9AA5C}.Release|Any CPU.Build.0 = Release|Any CPU + {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1DF66DB-52BE-489C-A292-525BC71F2BB2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {CEC62870-B8D5-4E33-9144-7C2B88EE8F73} + EndGlobalSection +EndGlobal diff --git a/PsvImage/AesHelper.cs b/PsvImage/AesHelper.cs new file mode 100644 index 0000000..7ba1b77 --- /dev/null +++ b/PsvImage/AesHelper.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace PsvImage +{ + static class AesHelper + { + public static byte[] CbcEncrypt(byte[] plainText, byte[] iv, byte[] key, int size = -1) + { + if (size < 0) + { + size = plainText.Length; + } + + using var aes = Aes.Create(); + aes.Padding = PaddingMode.None; + aes.Key = key; + aes.IV = iv; + using var encrypt = aes.CreateEncryptor(); + var cipherText = encrypt.TransformFinalBlock(plainText, 0, size); + return cipherText; + } + + public static byte[] AesEcbDecrypt(byte[] cipherText, byte[] key, int size = -1) + { + if (size < 0) + { + size = cipherText.Length; + } + + using var aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + aes.Key = key; + using var decrypt = aes.CreateDecryptor(); + var plainText = decrypt.TransformFinalBlock(cipherText, 0, size); + return plainText; + } + } +} diff --git a/PsvImage/CmaKeys.cs b/PsvImage/CmaKeys.cs new file mode 100644 index 0000000..c7bc8b1 --- /dev/null +++ b/PsvImage/CmaKeys.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace PsvImage +{ + public class CmaKeys + { + private static readonly byte[] Passphrase = Encoding.ASCII.GetBytes("Sri Jayewardenepura Kotte"); + private static readonly byte[] Key = { 0xA9, 0xFA, 0x5A, 0x62, 0x79, 0x9F, 0xCC, 0x4C, 0x72, 0x6B, 0x4E, 0x2C, 0xE3, 0x50, 0x6D, 0x38 }; + + public static byte[] GenerateKey(string aid) + { + var longlong = Convert.ToUInt64(aid, 16); + var aidBytes = BitConverter.GetBytes(longlong); + Array.Reverse(aidBytes); + return GenerateKey(aidBytes); + } + + public static byte[] GenerateKey(byte[] aid) + { + var derviedKey = aid.Concat(Passphrase).ToArray(); + + using var sha = SHA256.Create(); + derviedKey = sha.ComputeHash(derviedKey); + using var aes = Aes.Create(); + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + aes.Key = Key; + using var decryptor = aes.CreateDecryptor(); + derviedKey = decryptor.TransformFinalBlock(derviedKey, 0, derviedKey.Length); + return derviedKey; + } + } +} diff --git a/PsvImage/PSVIMGBuilder.cs b/PsvImage/PSVIMGBuilder.cs new file mode 100644 index 0000000..a751bc8 --- /dev/null +++ b/PsvImage/PSVIMGBuilder.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace PsvImage +{ + class PSVIMGBuilder + { + private byte[] iv; + private byte[] key; + private Stream mainStream; + private SHA256 sha256; + private Memory blockData; + private int blockPosition; + private long contentSize = 0; + + private int blocksWritten = 0; + private bool finished = false; + + public long ContentSize => contentSize; + + public int BlocksWritten => blocksWritten; + + public bool HasFinished => finished; + + //Footer + private long totalBytes = 0; + + public PSVIMGBuilder(Stream dst, byte[] Key) + { + + totalBytes = 0; + contentSize = 0; + sha256 = SHA256.Create(); + mainStream = dst; + key = Key; + + RandomNumberGenerator.Fill(iv); + iv = AesHelper.AesEcbDecrypt(iv, key); + + mainStream.Write(iv.AsSpan()); + totalBytes += iv.Length; + + startNewBlock(); + } + + public void AddFile(string FilePath, string ParentPath, string PathRel) + { + var sz = Utils.ToSceIoStat(FilePath).Size; + } + + private void startNewBlock() + { + blockData = new byte[PSVIMGConstants.FULL_PSVIMG_SIZE]; + blockPosition = 0; + } + + private byte[] shaBlock(int length = PSVIMGConstants.PSVIMG_BLOCK_SIZE) + { + return sha256.ComputeHash(blockData.ToArray(), 0, length); + } + + private void finishBlock(bool final = false) + { + int len = blockPosition; + var shaBytes = shaBlock(len); + shaBytes.CopyTo(blockData.Slice(blockPosition)); + len += PSVIMGConstants.SHA256_BLOCK_SIZE; + + //Get next IV + var encryptedBlock = AesHelper.CbcEncrypt(blockData.ToArray(), iv, key, len); + for (int i = 0; i < iv.Length; i++) + { + int encBlockOffset = (encryptedBlock.Length - iv.Length) + i; + iv[i] = encryptedBlock[encBlockOffset]; + } + + mainStream.Write(encryptedBlock.AsSpan()); + totalBytes += encryptedBlock.Length; + } + + private int remainingBlockSize() + { + return (int)(PSVIMGConstants.PSVIMG_BLOCK_SIZE - blockPosition); + } + + private void writeBlock(Span date, bool update = false) + { + var dLen = date.Length; + var writeTotal = 0; + while (dLen > 0) + { + int remaining = remainingBlockSize(); + + if (dLen > remaining) + { + date.Slice(writeTotal, remaining).CopyTo(blockData.Span.Slice(blockPosition)); + blockPosition += remaining; + writeTotal += remaining; + dLen -= remaining; + + finishBlock(); + startNewBlock(); + if (update) + { + blocksWritten += 1; + } + } + else + { + + } + } + + } + } +} diff --git a/PsvImage/PsvImage.csproj b/PsvImage/PsvImage.csproj new file mode 100644 index 0000000..5e9d468 --- /dev/null +++ b/PsvImage/PsvImage.csproj @@ -0,0 +1,8 @@ + + + + net6.0 + true + + + diff --git a/PsvImage/PsvImgStream.cs b/PsvImage/PsvImgStream.cs new file mode 100644 index 0000000..9da6020 --- /dev/null +++ b/PsvImage/PsvImgStream.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace PsvImage +{ + class PsvImgStream : Stream + { + + public override void Flush() + { + throw new NotImplementedException(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotImplementedException(); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override bool CanRead { get; } + public override bool CanSeek { get; } + public override bool CanWrite { get; } + public override long Length { get; } + public override long Position { get; set; } + } +} diff --git a/PsvImage/PsvImgStructs.cs b/PsvImage/PsvImgStructs.cs new file mode 100644 index 0000000..ec2712a --- /dev/null +++ b/PsvImage/PsvImgStructs.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; + +namespace PsvImage +{ + internal class PSVIMGConstants + { + public const int AES_BLOCK_SIZE = 0x10; + public const int SHA256_BLOCK_SIZE = 0x20; + public const int PSVIMG_BLOCK_SIZE = 0x8000; + public const int PSVIMG_ENTRY_ALIGN = 0x400; + + public const string PSVIMG_HEADER_END = "EndOfHeader\n"; + public const string PSVIMG_TAILOR_END = "EndOfTailer\n"; + public const string PSVIMG_PADDING_END = "\n"; + + public const int FULL_PSVIMG_SIZE = PSVIMG_BLOCK_SIZE + SHA256_BLOCK_SIZE; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SceDateTime + { + public ushort Year; + public ushort Month; + public ushort Day; + public ushort Hour; + public ushort Minute; + public ushort Second; + public uint Microsecond; + } + + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct SceIoStat + { + [Flags] + public enum Modes + { + /** Format bits mask */ + FormatBits = 0xF000, + /** Symbolic link */ + SymbLink = 0x4000, + /** Directory */ + Directory = 0x1000, + /** Regular file */ + File = 0x2000, + + /** Set UID */ + SetUid = 0x0800, + /** Set GID */ + SetGid = 0x0400, + /** Sticky */ + Sticky = 0x0200, + + /** Others access rights mask */ + OthersAcesssMask = 0x01C0, + /** Others read permission */ + OthersRead = 0x0100, + /** Others write permission */ + OthersWrite = 0x0080, + /** Others execute permission */ + OthersExecute = 0x0040, + + /** Group access rights mask */ + GroupAcessMask = 0x0038, + /** Group read permission */ + GroupRead = 0x0020, + /** Group write permission */ + GroupWrite = 0x0010, + /** Group execute permission */ + GroupExecute = 0x0008, + + /** User access rights mask */ + UserAcessMask = 0x0007, + /** User read permission */ + UserRead = 0x0004, + /** User write permission */ + UserWrite = 0x0002, + /** User execute permission */ + UserExecute = 0x0001, + }; + public enum AttributesEnum + { + /** Format mask */ + FormatMask = 0x0038, // Format mask + /** Symlink */ + SymbLink = 0x0008, // Symbolic link + /** Directory */ + Directory = 0x0010, // Directory + /** Regular file */ + File = 0x0020, // Regular file + + /** Hidden read permission */ + Read = 0x0004, // read + /** Hidden write permission */ + Write = 0x0002, // write + /** Hidden execute permission */ + Execute = 0x0001, // execute + }; + + public Modes Mode; + public AttributesEnum Attributes; + /** Size of the file in bytes. */ + public UInt64 Size; + /** Creation time. */ + public SceDateTime CreationTime; + /** Access time. */ + public SceDateTime AccessTime; + /** Modification time. */ + public SceDateTime ModificaionTime; + /** Device-specific data. */ + public fixed uint Private[6]; + } + + [StructLayout(LayoutKind.Sequential)] + internal unsafe struct PsvImgTailer + { + + public ulong Flags; + public fixed byte Padding[1004]; + public fixed byte bEnd[12]; + } + + internal class PSVIMGPadding + { + public static long GetPadding(long size) + { + long padding; + if ((size & (PSVIMGConstants.PSVIMG_ENTRY_ALIGN - 1)) >= 1) + { + padding = (PSVIMGConstants.PSVIMG_ENTRY_ALIGN - (size & (PSVIMGConstants.PSVIMG_ENTRY_ALIGN - 1))); + } + else + { + padding = 0; + } + return padding; + } + } + + internal unsafe struct PsvImgHeader + { + public ulong SysTime; + public ulong Flags; + public SceIoStat Statistics; + public fixed byte bParentPath[256]; + public uint unk_16C; + public fixed byte bPath[256]; + public fixed byte Padding[904]; + public fixed byte bEnd[12]; + } +} diff --git a/PsvImage/PsvmdBuilder.cs b/PsvImage/PsvmdBuilder.cs new file mode 100644 index 0000000..38a3f09 --- /dev/null +++ b/PsvImage/PsvmdBuilder.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace PsvImage +{ + class PsvmdBuilder + { + + public static void CreatePsvmd(Stream OutputStream, Stream EncryptedPsvimg, long ContentSize, string BackupType, + byte[] Key) + { + Span iv = new byte[PSVIMGConstants.AES_BLOCK_SIZE]; + EncryptedPsvimg.Seek(0, SeekOrigin.Begin); + EncryptedPsvimg.Read(iv); + iv = AesHelper.AesEcbDecrypt(iv.ToArray(), Key); + + } + + + } +} diff --git a/PsvImage/Utils.cs b/PsvImage/Utils.cs new file mode 100644 index 0000000..ab7be2c --- /dev/null +++ b/PsvImage/Utils.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace PsvImage +{ + static class Utils + { + public static SceDateTime ToSceDateTime(this DateTime dateTime) + { + var sceDateTime = new SceDateTime(); + sceDateTime.Year = (ushort)dateTime.Year; + sceDateTime.Month = (ushort)dateTime.Month; + sceDateTime.Day = (ushort)dateTime.Day; + sceDateTime.Hour = (ushort)dateTime.Hour; + sceDateTime.Minute = (ushort)dateTime.Minute; + sceDateTime.Second = (ushort)dateTime.Second; + sceDateTime.Microsecond = (uint)dateTime.Millisecond * 1000; + return sceDateTime; + } + + public static SceIoStat ToSceIoStat(string path) + { + var stats = new SceIoStat(); + var attributes = File.GetAttributes(path); + + if (attributes.HasFlag(FileAttributes.Directory)) + { + stats.Mode |= SceIoStat.Modes.Directory; + stats.Size = 0; + } + else + { + stats.Mode |= SceIoStat.Modes.File; + stats.Size = (ulong)(new FileInfo(path).Length); + } + + if (attributes.HasFlag(FileAttributes.ReadOnly)) + { + stats.Mode |= SceIoStat.Modes.GroupRead; + stats.Mode |= SceIoStat.Modes.OthersRead; + stats.Mode |= SceIoStat.Modes.UserRead; + } + else + { + stats.Mode |= SceIoStat.Modes.GroupRead; + stats.Mode |= SceIoStat.Modes.GroupWrite; + stats.Mode |= SceIoStat.Modes.OthersRead; + stats.Mode |= SceIoStat.Modes.OthersWrite; + stats.Mode |= SceIoStat.Modes.UserRead; + stats.Mode |= SceIoStat.Modes.UserWrite; + } + + stats.CreationTime = File.GetCreationTimeUtc(path).ToSceDateTime(); + stats.AccessTime = File.GetLastAccessTimeUtc(path).ToSceDateTime(); + stats.ModificaionTime = File.GetLastWriteTimeUtc(path).ToSceDateTime(); + + return stats; + } + } +} From 562016688a77d1740b333f55a49aa59616a45614 Mon Sep 17 00:00:00 2001 From: Li Date: Sun, 16 Apr 2023 05:22:43 +1200 Subject: [PATCH 03/31] Add NpUmdImg --- .gitignore | 1 + PbpResign/Program.cs | 21 +- PopsBuilder/Pops/DiscCompressor.cs | 7 +- PopsBuilder/Pops/PopsImg.cs | 34 +- PopsBuilder/Pops/PsIsoImg.cs | 12 +- PopsBuilder/Pops/PsTitleImg.cs | 18 +- PopsBuilder/PopsBuilder.csproj | 2 +- PopsBuilder/Psp/NpDrmInfo.cs | 22 ++ PopsBuilder/Psp/NpDrmPsar.cs | 33 +- PopsBuilder/Psp/NpUmdImg.cs | 291 ++++++++++++++++++ PopsBuilder/Psp/PbpBuilder.cs | 9 +- PopsBuilder/Psp/Rng.cs | 29 ++ PopsBuilder/Resources.Designer.cs | 36 ++- PopsBuilder/Resources.resx | 10 +- PopsBuilder/Resources/SIMPLE.PNG | Bin 22964 -> 28983 bytes PopsBuilder/Resources/STARTDATMINIS.PNG | Bin 0 -> 7374 bytes .../{STARTDAT.PNG => STARTDATPOPS.PNG} | Bin PopsBuilder/Resources/STARTDATPSP.PNG | Bin 0 -> 4935 bytes PopsBuilder/Resources/Thumbs.db | Bin 14848 -> 0 bytes PopsBuilder/StreamUtil.cs | 22 +- PspCrypto/Lz.cs | 17 +- PspCrypto/Lzrc.cs | 205 +++++++----- 22 files changed, 607 insertions(+), 162 deletions(-) create mode 100644 PopsBuilder/Psp/NpDrmInfo.cs create mode 100644 PopsBuilder/Psp/NpUmdImg.cs create mode 100644 PopsBuilder/Psp/Rng.cs create mode 100644 PopsBuilder/Resources/STARTDATMINIS.PNG rename PopsBuilder/Resources/{STARTDAT.PNG => STARTDATPOPS.PNG} (100%) create mode 100644 PopsBuilder/Resources/STARTDATPSP.PNG delete mode 100644 PopsBuilder/Resources/Thumbs.db diff --git a/.gitignore b/.gitignore index 8bbb701..a6c5e43 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .vs/* *.7z +*thumbs.db PbpResign/bin/* PbpResign/obj/* diff --git a/PbpResign/Program.cs b/PbpResign/Program.cs index e054307..8ddad81 100644 --- a/PbpResign/Program.cs +++ b/PbpResign/Program.cs @@ -533,11 +533,11 @@ namespace PbpResign Span table = new byte[tablesize]; var tp = MemoryMarshal.Cast(table); input.Read(table); + // Decrypt Table for (int i = 0; i < totalBlocks; i++) - { XorTable(tp[(i * 8)..]); - } + var blocks = MemoryMarshal.Cast(table); for (int i = 0; i < blocks.Length; i++) @@ -578,9 +578,7 @@ namespace PbpResign // Encrypt Table for (int i = 0; i < totalBlocks; i++) - { XorTable(tp[(i * 8)..]); - } output.Seek(entityOff, SeekOrigin.Begin); output.Write(table); @@ -1073,6 +1071,17 @@ namespace PbpResign return false; } type = 0; + + Console.WriteLine("VersionKey: " + BitConverter.ToString(NewVersionKey.ToArray())); + + NpUmdImg npumd = new NpUmdImg(new NpDrmInfo(NewVersionKey.ToArray(), CId, npHdr.NpFlags), + "fft.iso", "ULUS10297", File.ReadAllBytes("TEST\\PARAM.SFO"), false); + + npumd.CreatePsar(); + byte[] paramFile = File.ReadAllBytes("TEST\\PARAM.SFO"); + + PbpBuilder.CreatePbp(paramFile, File.ReadAllBytes("TEST\\ICON0.PNG"), null, File.ReadAllBytes("TEST\\PIC0.PNG"), File.ReadAllBytes("TEST\\PIC1.PNG"), null, npumd, "FFT.PBP"); + return CopyNpUmdImg(input, output, pbpHdr, psarBuff, npHdr); } @@ -1086,7 +1095,7 @@ namespace PbpResign return false; } type = 1; - DiscInfo[] discs = new DiscInfo[2]; + /*DiscInfo[] discs = new DiscInfo[2]; discs[0] = new DiscInfo("ABEE\\D1.CUE", "Oddworld: Abe's Exoddus", "SLES01480"); discs[1] = new DiscInfo("ABEE\\D2.CUE", "Oddworld: Abe's Exoddus", "SLES11480"); PsTitleImg title = new PsTitleImg(NewVersionKey.ToArray(), CId, discs); @@ -1098,7 +1107,7 @@ namespace PbpResign // File.ReadAllBytes("TEST\\PARAM.SFO"), File.ReadAllBytes("TEST\\ICON0.PNG"), null, // File.ReadAllBytes("TEST\\PIC0.PNG"), File.ReadAllBytes("TEST\\PIC1.PNG"), null); //File.WriteAllBytes("TEST.BIN", i.GetIsoHeader()); - //File.WriteAllBytes("TEST.ISOc", i.GetIso()); + //File.WriteAllBytes("TEST.ISOc", i.GetIso());*/ return CopyPsIsoImg(input, output, pbpHdr); diff --git a/PopsBuilder/Pops/DiscCompressor.cs b/PopsBuilder/Pops/DiscCompressor.cs index 57fdfc9..486124b 100644 --- a/PopsBuilder/Pops/DiscCompressor.cs +++ b/PopsBuilder/Pops/DiscCompressor.cs @@ -75,7 +75,7 @@ namespace PopsBuilder.Pops int headerSize = DNASHelper.CalculateSize(isoHdr.Length, 0x400); byte[] headerEnc = new byte[headerSize]; - int sz = DNASHelper.Encrypt(headerEnc, isoHdr, srcImg.VersionKey, isoHdr.Length, 1, 1, blockSize: 0x400); + int sz = DNASHelper.Encrypt(headerEnc, isoHdr, srcImg.DrmInfo.VersionKey, isoHdr.Length, srcImg.DrmInfo.KeyType, 1, blockSize: 0x400); byte[] isoHdrPgd = headerEnc.ToArray(); Array.Resize(ref isoHdrPgd, sz); @@ -155,7 +155,6 @@ namespace PopsBuilder.Pops private void writeCompressedCDATracks() { - Random rng = new Random(); IsoHeader.Seek(0x800, SeekOrigin.Begin); // CDA Entries @@ -168,7 +167,7 @@ namespace PopsBuilder.Pops using (CueStream audioStream = cue.OpenTrack(i)) { - uint key = Convert.ToUInt32(rng.NextInt64(0, uint.MaxValue)); + uint key = Rng.RandomUInt(); Atrac3ToolEncoder enc = new Atrac3ToolEncoder(); @@ -202,7 +201,7 @@ namespace PopsBuilder.Pops AMCTRL.sceDrmBBMacInit(mkey, 3); AMCTRL.sceDrmBBMacUpdate(mkey, data, data.Length); Span checksum = new byte[20 + 0x10]; - AMCTRL.sceDrmBBMacFinal(mkey, checksum[20..], srcImg.VersionKey); + AMCTRL.sceDrmBBMacFinal(mkey, checksum[20..], srcImg.DrmInfo.VersionKey); ref var aesHdr = ref MemoryMarshal.AsRef(checksum); aesHdr.mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC; diff --git a/PopsBuilder/Pops/PopsImg.cs b/PopsBuilder/Pops/PopsImg.cs index 1a46480..77a914b 100644 --- a/PopsBuilder/Pops/PopsImg.cs +++ b/PopsBuilder/Pops/PopsImg.cs @@ -12,27 +12,22 @@ namespace PopsBuilder.Pops { public class PopsImg : NpDrmPsar { - public PopsImg(byte[] versionKey, string contentId) : base(versionKey, contentId) + public PopsImg(NpDrmInfo versionKey) : base(versionKey) { - startDat = new MemoryStream(); - startDatUtil = new StreamUtil(startDat); - simple = new MemoryStream(); simpleUtil = new StreamUtil(simple); - createStartDat(); + StartDat = NpDrmPsar.CreateStartDat(Resources.STARTDATPOPS); createSimpleDat(); - SimplePgd = generateSimplePgd(); } - internal MemoryStream startDat; - internal StreamUtil startDatUtil; + private MemoryStream simple; private StreamUtil simpleUtil; + public byte[] StartDat; public byte[] SimplePgd; - internal Random rng = new Random(); private void createSimpleDat() { simpleUtil.WriteStr("SIMPLE "); @@ -45,20 +40,7 @@ namespace PopsBuilder.Pops simpleUtil.WriteBytes(Resources.SIMPLE); } - private void createStartDat() - { - startDatUtil.WriteStr("STARTDAT"); - startDatUtil.WriteInt32(0x1); - startDatUtil.WriteInt32(0x1); - startDatUtil.WriteInt32(0x50); - startDatUtil.WriteInt32(Resources.STARTDAT.Length); - startDatUtil.WriteInt32(0x0); - startDatUtil.WriteInt32(0x0); - - startDatUtil.WritePadding(0, 0x30); - - startDatUtil.WriteBytes(Resources.STARTDAT); - } + private byte[] generateSimplePgd() { @@ -69,7 +51,7 @@ namespace PopsBuilder.Pops byte[] simpleEnc = new byte[simpleSz]; // get pgd - int sz = DNASHelper.Encrypt(simpleEnc, simpleData, VersionKey, simpleData.Length, 1, 1, blockSize: 0x400); + int sz = DNASHelper.Encrypt(simpleEnc, simpleData, DrmInfo.VersionKey, simpleData.Length, DrmInfo.KeyType, 1, blockSize: 0x400); byte[] pgd = simpleEnc.ToArray(); Array.Resize(ref pgd, sz); @@ -77,7 +59,7 @@ namespace PopsBuilder.Pops } - public byte[] GenerateDataPsp() + public override byte[] GenerateDataPsp() { Span loaderEnc = new byte[0x9B13]; @@ -95,7 +77,7 @@ namespace PopsBuilder.Pops Array.ConstrainedCopy(lowBits, 0, dataPspElf, 0x68C, 0x2); Array.ConstrainedCopy(highBits, 0, dataPspElf, 0x694, 0x2); - SceMesgLed.Encrypt(loaderEnc, dataPspElf, 0x0DAA06F0, SceExecFileDecryptMode.DECRYPT_MODE_POPS_EXEC, VersionKey, ContentId, Resources.DATAPSPSDCFG); + SceMesgLed.Encrypt(loaderEnc, dataPspElf, 0x0DAA06F0, SceExecFileDecryptMode.DECRYPT_MODE_POPS_EXEC, DrmInfo.VersionKey, DrmInfo.ContentId, Resources.DATAPSPSDCFG); return loaderEnc.ToArray(); } diff --git a/PopsBuilder/Pops/PsIsoImg.cs b/PopsBuilder/Pops/PsIsoImg.cs index 0a96f2e..16d1a7f 100644 --- a/PopsBuilder/Pops/PsIsoImg.cs +++ b/PopsBuilder/Pops/PsIsoImg.cs @@ -1,6 +1,7 @@ using Org.BouncyCastle.Crypto.Paddings; using PopsBuilder.Atrac3; using PopsBuilder.Cue; +using PopsBuilder.Psp; using PspCrypto; using System; using System.Net; @@ -11,24 +12,24 @@ namespace PopsBuilder.Pops { public class PsIsoImg : PopsImg { - internal PsIsoImg(byte[] versionkey, string contentId, DiscCompressor discCompressor) : base(versionkey, contentId) + internal PsIsoImg(NpDrmInfo versionKey, DiscCompressor discCompressor) : base(versionKey) { this.compressor = discCompressor; } - public PsIsoImg(byte[] versionkey, string contentId, DiscInfo disc, IAtracEncoderBase encoder) : base(versionkey, contentId) + public PsIsoImg(NpDrmInfo versionKey, DiscInfo disc, IAtracEncoderBase encoder) : base(versionKey) { this.compressor = new DiscCompressor(this, disc, encoder); } - public PsIsoImg(byte[] versionkey, string contentId, DiscInfo disc) : base(versionkey, contentId) + public PsIsoImg(NpDrmInfo versionKey, DiscInfo disc) : base(versionKey) { this.compressor = new DiscCompressor(this, disc, new Atrac3ToolEncoder()); } public void CreatePsar(bool isPartOfMultiDisc=false) { compressor.GenerateIsoHeaderAndCompress(); - if (!isPartOfMultiDisc) compressor.WriteSimpleDatLocation((compressor.IsoOffset + compressor.CompressedIso.Length) + startDat.Length); + if (!isPartOfMultiDisc) compressor.WriteSimpleDatLocation((compressor.IsoOffset + compressor.CompressedIso.Length) + StartDat.Length); psarUtil.WriteStr("PSISOIMG0000"); psarUtil.WriteInt64(0x00); // location of STARTDAT @@ -47,8 +48,7 @@ namespace PopsBuilder.Pops // write STARTDAT Int64 startDatLocation = Psar.Position; - startDat.Seek(0x00, SeekOrigin.Begin); - startDat.CopyTo(Psar); + psarUtil.WriteBytes(StartDat); // write pgd psarUtil.WriteBytes(this.SimplePgd); diff --git a/PopsBuilder/Pops/PsTitleImg.cs b/PopsBuilder/Pops/PsTitleImg.cs index 0ed7b6e..89c2a36 100644 --- a/PopsBuilder/Pops/PsTitleImg.cs +++ b/PopsBuilder/Pops/PsTitleImg.cs @@ -1,4 +1,5 @@ using PopsBuilder.Atrac3; +using PopsBuilder.Psp; using PspCrypto; using System; using System.Collections.Generic; @@ -13,7 +14,7 @@ namespace PopsBuilder.Pops { const int MAX_DISCS = 5; const int PSISO_ALIGN = 0x8000; - public PsTitleImg(byte[] versionKey, string contentId, DiscInfo[] discs) : base(versionKey, contentId) + public PsTitleImg(NpDrmInfo drmInfo, DiscInfo[] discs) : base(drmInfo) { if (discs.Length > MAX_DISCS) throw new Exception("Sorry, multi disc games only support up to 5 discs... (i dont make the rules)"); this.compressors = new DiscCompressor[MAX_DISCS]; @@ -42,7 +43,7 @@ namespace PopsBuilder.Pops psarUtil.WriteStr("PSTITLEIMG000000"); psarUtil.WriteInt64(PSISO_ALIGN+isoPart.Length); // location of STARTDAT - psarUtil.WriteRandom(0x10); // dunno what this is + psarUtil.WriteBytes(Rng.RandomBytes(0x10)); // dunno what this is psarUtil.WritePadding(0x00, 0x1D8); byte[] isoMap = generateIsoMapPgd(); @@ -52,8 +53,7 @@ namespace PopsBuilder.Pops isoPart.Seek(0x00, SeekOrigin.Begin); isoPart.CopyTo(Psar); - startDat.Seek(0x00, SeekOrigin.Begin); - startDat.CopyTo(Psar); + psarUtil.WriteBytes(StartDat); psarUtil.WriteBytes(SimplePgd); } @@ -66,7 +66,7 @@ namespace PopsBuilder.Pops PspCrypto.AMCTRL.sceDrmBBMacInit(mkey, 3); PspCrypto.AMCTRL.sceDrmBBMacUpdate(mkey, header, header.Length /*0xb3c80*/); Span newKey = new byte[20 + 0x10]; - PspCrypto.AMCTRL.sceDrmBBMacFinal(mkey, newKey[20..], VersionKey); + PspCrypto.AMCTRL.sceDrmBBMacFinal(mkey, newKey[20..], DrmInfo.VersionKey); ref var aesHdr = ref MemoryMarshal.AsRef(newKey); aesHdr.mode = KIRKEngine.KIRK_MODE_ENCRYPT_CBC; aesHdr.keyseed = 0x63; @@ -86,7 +86,7 @@ namespace PopsBuilder.Pops int encryptedSz = DNASHelper.CalculateSize(isoMapBuf.Length, 1024); var isoMapEnc = new byte[encryptedSz]; - DNASHelper.Encrypt(isoMapEnc, isoMapBuf, VersionKey, isoMapBuf.Length, 1, 1); + DNASHelper.Encrypt(isoMapEnc, isoMapBuf, DrmInfo.VersionKey, isoMapBuf.Length, DrmInfo.KeyType, 1); return isoMapEnc; } @@ -101,7 +101,7 @@ namespace PopsBuilder.Pops int padLen = Convert.ToInt32(PSISO_ALIGN - (isoPart.Position % PSISO_ALIGN)); isoPartUtil.WritePadding(0x00, padLen); - using (PsIsoImg psIsoImg = new PsIsoImg(this.VersionKey, this.ContentId, compressors[i])) + using (PsIsoImg psIsoImg = new PsIsoImg(this.DrmInfo, compressors[i])) { isoMapUtil.WriteUInt32(Convert.ToUInt32(PSISO_ALIGN + isoPart.Position)); @@ -127,8 +127,8 @@ namespace PopsBuilder.Pops isoMapUtil.WriteBytes(checksums); isoMapUtil.WriteStrWithPadding(discs.First().DiscIdHdr, 0x00, 0x20); - isoMapUtil.WriteInt64(Convert.ToInt64(PSISO_ALIGN + isoPart.Length + startDat.Length)); - isoMapUtil.WriteRandom(0x80); + isoMapUtil.WriteInt64(Convert.ToInt64(PSISO_ALIGN + isoPart.Length + StartDat.Length)); + psarUtil.WriteBytes(Rng.RandomBytes(0x80)); isoMapUtil.WriteStrWithPadding(discs.First().DiscName, 0x00, 0x80); isoMapUtil.WriteInt32(MAX_DISCS); isoMapUtil.WritePadding(0x00, 0x70); diff --git a/PopsBuilder/PopsBuilder.csproj b/PopsBuilder/PopsBuilder.csproj index 2788edf..6e883ae 100644 --- a/PopsBuilder/PopsBuilder.csproj +++ b/PopsBuilder/PopsBuilder.csproj @@ -20,7 +20,7 @@ - ResXFileCodeGenerator + PublicResXFileCodeGenerator Resources.Designer.cs diff --git a/PopsBuilder/Psp/NpDrmInfo.cs b/PopsBuilder/Psp/NpDrmInfo.cs new file mode 100644 index 0000000..d740f0f --- /dev/null +++ b/PopsBuilder/Psp/NpDrmInfo.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Psp +{ + public class NpDrmInfo + { + public string ContentId; + public byte[] VersionKey; + public int KeyType; + + public NpDrmInfo(byte[] versionKey, string contentId, int keyType) + { + this.VersionKey = versionKey; + this.KeyType = keyType; + this.ContentId = contentId; + } + } +} diff --git a/PopsBuilder/Psp/NpDrmPsar.cs b/PopsBuilder/Psp/NpDrmPsar.cs index 4960cac..7cef126 100644 --- a/PopsBuilder/Psp/NpDrmPsar.cs +++ b/PopsBuilder/Psp/NpDrmPsar.cs @@ -9,22 +9,43 @@ using System.Threading.Tasks; namespace PopsBuilder.Psp { - public class NpDrmPsar : IDisposable + public abstract class NpDrmPsar : IDisposable { - public NpDrmPsar(byte[] versionKey, string contentId) + public NpDrmPsar(NpDrmInfo npDrmInfo) { - VersionKey = versionKey; - ContentId = contentId; + DrmInfo = npDrmInfo; Psar = new MemoryStream(); psarUtil = new StreamUtil(Psar); } - public byte[] VersionKey; - public string ContentId; + public NpDrmInfo DrmInfo; public MemoryStream Psar; internal StreamUtil psarUtil; + public abstract byte[] GenerateDataPsp(); + public static byte[] CreateStartDat(byte[] image) + { + using(MemoryStream startDatStream = new MemoryStream()) + { + StreamUtil startDatUtil = new StreamUtil(startDatStream); + + startDatUtil.WriteStr("STARTDAT"); + startDatUtil.WriteInt32(0x1); + startDatUtil.WriteInt32(0x1); + startDatUtil.WriteInt32(0x50); + startDatUtil.WriteInt32(image.Length); + startDatUtil.WriteInt32(0x0); + startDatUtil.WriteInt32(0x0); + + startDatUtil.WritePadding(0, 0x30); + + startDatUtil.WriteBytes(image); + + startDatStream.Seek(0x00, SeekOrigin.Begin); + return startDatStream.ToArray(); + } + } public virtual void Dispose() { diff --git a/PopsBuilder/Psp/NpUmdImg.cs b/PopsBuilder/Psp/NpUmdImg.cs new file mode 100644 index 0000000..8f7c618 --- /dev/null +++ b/PopsBuilder/Psp/NpUmdImg.cs @@ -0,0 +1,291 @@ +using Org.BouncyCastle.Crypto.Paddings; +using Org.BouncyCastle.Ocsp; +using PopsBuilder.Pops; +using PspCrypto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Security; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Psp +{ + public class NpUmdImg : NpDrmPsar + { + const int RATIO_LIMIT = 90; + const int BLOCK_BASIS = 0x10; + const int SECTOR_SZ = 2048; + const int BLOCK_SZ = BLOCK_BASIS * SECTOR_SZ; + public NpUmdImg(NpDrmInfo drmInfo, string iso, string discId, byte[] paramSfo, bool compress) : base(drmInfo) + { + this.isoStream = File.OpenRead(iso); + this.compress = compress; + + this.npHdr = new MemoryStream(); + this.npHdrUtil = new StreamUtil(npHdr); + + this.npHdrBody = new MemoryStream(); + this.npHdrBodyUtil = new StreamUtil(npHdrBody); + + this.npTbl = new MemoryStream(); + this.npTblUtil = new StreamUtil(npTbl); + + this.isoData = new MemoryStream(); + this.isoDataUtil = new StreamUtil(isoData); + + this.headerKey = Rng.RandomBytes(0x10); + + this.discId = discId.ToUpperInvariant().Replace("-", "").Replace("_", ""); + this.paramSfo = paramSfo; + + isoBlocks = Convert.ToInt64((isoStream.Length + BLOCK_SZ - 1) / BLOCK_SZ); + + } + + private void createNpHdr() + { + npHdrUtil.WriteStr("NPUMDIMG"); + npHdrUtil.WriteInt32(DrmInfo.KeyType); + npHdrUtil.WriteInt32(BLOCK_BASIS); + npHdrUtil.WriteStrWithPadding(DrmInfo.ContentId, 0x00, 0x30); + + createNpUmdBody(); + byte[] npumdDec = npHdrBody.ToArray(); + byte[] npumdEnc = encryptHeader(npumdDec); + + npHdrUtil.WriteBytes(npumdEnc); + npHdrUtil.WriteBytes(this.headerKey); + npHdrUtil.WriteBytes(this.dataKey); + + byte[] npumdhdr = npHdr.ToArray(); + byte[] npumdheaderHash = hashBlock(npumdhdr); + + npHdrUtil.WriteBytes(npumdheaderHash); + npHdrUtil.WriteBytes(Rng.RandomBytes(0x8)); // padding + npHdrUtil.PadUntil(0x00, 0x100); + } + + public void CreatePsar() + { + createNpUmdTbl(); + byte[] tbl = encryptTable(); + this.dataKey = hashBlock(tbl); + createNpHdr(); + + byte[] npHdrBuf = npHdr.ToArray(); + ECDsaHelper.SignNpImageHeader(npHdrBuf); + psarUtil.WriteBytes(npHdrBuf); + psarUtil.WriteBytes(tbl); + + isoData.Seek(0x00, SeekOrigin.Begin); + isoData.CopyTo(Psar); + } + + public byte[] signParamSfo(byte[] paramSfo) + { + int paramSfoLen = paramSfo.Length; + byte[] contentIdBytes = Encoding.UTF8.GetBytes(DrmInfo.ContentId); + Array.Resize(ref paramSfo, paramSfoLen + 0x30); + Array.ConstrainedCopy(contentIdBytes, 0, paramSfo, paramSfoLen, contentIdBytes.Length); + byte[] signature = new byte[0x30]; + + ECDsaHelper.SignParamSfo(paramSfo, signature); + return signature; + } + + public override byte[] GenerateDataPsp() + { + bool minis = false; // TODO: read minis flag from param.sfo + byte[] startDat = CreateStartDat(minis ? Resources.STARTDATMINIS : Resources.STARTDATPSP); + using (MemoryStream dataPsp = new MemoryStream()) + { + StreamUtil dataPspUtil = new StreamUtil(dataPsp); + byte[] signature = signParamSfo(paramSfo); + dataPspUtil.WriteBytes(signature); + dataPspUtil.WritePadding(0x00, 0x530); + dataPspUtil.WriteStrWithPadding(DrmInfo.ContentId, 0x00, 0x30); + dataPspUtil.WriteInt32BE(DrmInfo.KeyType); + dataPspUtil.WriteInt32(0); + dataPspUtil.WriteInt32(0); + dataPspUtil.WriteInt32(0); + dataPspUtil.WriteBytes(startDat); + return dataPsp.ToArray(); + } + } + + private void createNpUmdTbl() + { + Int64 tableSz = isoBlocks * 0x20; + Int64 isoSz = this.isoStream.Length; + int wsize = 0; + Int64 isoOffset = 0x100 + tableSz; + + for (int i = 0; i < isoBlocks; i++) + { + byte[] isoBuf = new byte[BLOCK_SZ]; + wsize = isoStream.Read(isoBuf, 0x00, BLOCK_SZ); + + byte[] wbuf = isoBuf; + + if (this.compress) // Compress data. + { + byte[] lzRcBuf = Lz.compress(isoBuf, true); + //memset(lzrc_buf + lzrc_size, 0, 16); + // + + int ratio = (lzRcBuf.Length * 100) / BLOCK_SZ; + + if (ratio < RATIO_LIMIT) + { + wbuf = lzRcBuf; + + wsize = (lzRcBuf.Length + 15) & ~15; + Array.Resize(ref lzRcBuf, wsize); + } + } + + int unpaddedSz = wsize; + wsize = (wsize + 15) & ~15; + Array.Resize(ref wbuf, wsize); + encryptBlock(wbuf, Convert.ToInt32(isoOffset)); + byte[] hash = hashBlock(wbuf); + + npTblUtil.WriteBytes(hash); + npTblUtil.WriteUInt32(Convert.ToUInt32(isoOffset)); + npTblUtil.WriteUInt32(Convert.ToUInt32(unpaddedSz)); + npTblUtil.WriteInt32(0); + npTblUtil.WriteInt32(0); + + + isoData.Write(wbuf, 0, wsize); + + isoOffset += wsize; + + Console.Write(Convert.ToInt32(Math.Floor((Convert.ToDouble(isoStream.Position) / Convert.ToDouble(isoStream.Length)) * 100.0)) + "%\r"); + } + + } + + private byte[] encryptTable() + { + byte[] table = npTbl.ToArray(); + + // Encrypt Table + var tp = MemoryMarshal.Cast(table); + + for (int i = 0; i < (table.Length / 0x20); i++) + XorTable(tp[(i * 8)..]); + + var tpbyt = MemoryMarshal.Cast(tp); + + return tpbyt.ToArray(); + } + private static void XorTable(Span tp) + { + tp[4] ^= tp[3] ^ tp[2]; + tp[5] ^= tp[2] ^ tp[1]; + tp[6] ^= tp[0] ^ tp[3]; + tp[7] ^= tp[1] ^ tp[0]; + } + + + private byte[] encryptBlock(byte[] blockData, int offset) + { + AMCTRL.CIPHER_KEY ckey = new AMCTRL.CIPHER_KEY(); + AMCTRL.sceDrmBBCipherInit(out ckey, 1, DrmInfo.KeyType, headerKey, DrmInfo.VersionKey, offset >> 4); + AMCTRL.sceDrmBBCipherUpdate(ref ckey, blockData, blockData.Length); + AMCTRL.sceDrmBBCipherFinal(ref ckey); + return blockData; + } + + private byte[] hashBlock(byte[] blockData) + { + byte[] hash = new byte[0x10]; + Span mkey = stackalloc byte[Marshal.SizeOf()]; + AMCTRL.sceDrmBBMacInit(mkey, 3); + AMCTRL.sceDrmBBMacUpdate(mkey, blockData, blockData.Length); + AMCTRL.sceDrmBBMacFinal(mkey, hash, DrmInfo.VersionKey); + Utils.BuildDrmBBMacFinal2(hash); + return hash; + } + + + private void createNpUmdBody() + { + npHdrBodyUtil.WriteUInt16(SECTOR_SZ); // sector_sz + + if (isoStream.Length > 0x40000000) + npHdrBodyUtil.WriteUInt16(0xE001); // unk_2 + else + npHdrBodyUtil.WriteUInt16(0xE000); //unk_2 + + npHdrBodyUtil.WriteUInt32(0); // unk_4 + npHdrBodyUtil.WriteUInt32(0x1010); // unk_8 + npHdrBodyUtil.WriteUInt32(0); // unk_12 + npHdrBodyUtil.WriteUInt32(0); // unk_16 + + npHdrBodyUtil.WriteUInt32(0x00); // LBA START + npHdrBodyUtil.WriteUInt32(0); // unk_24 + + + npHdrBodyUtil.WriteUInt32(Math.Min(Convert.ToUInt32((isoBlocks * BLOCK_BASIS) - 1), 0x6C0BF)); // nsectors + npHdrBodyUtil.WriteUInt32(0); // unk_32 + + npHdrBodyUtil.WriteUInt32(Convert.ToUInt32((isoBlocks * BLOCK_BASIS) - 1)); + npHdrBodyUtil.WriteUInt32(0x01003FFE); // unk_40 + npHdrBodyUtil.WriteUInt32(0x100); // block_entry_offset + + npHdrBodyUtil.WriteStrWithPadding(this.discId.Substring(0, 4) + "-" + this.discId.Substring(4, 5), 0x00, 0x10); + + npHdrBodyUtil.WriteInt32(0); // header_start_offset + npHdrBodyUtil.WriteInt32(0); // unk_68 + + npHdrBodyUtil.WriteByte(0x00); // unk_72 + npHdrBodyUtil.WriteByte(0x00); // bbmac param + npHdrBodyUtil.WriteByte(0x00); // unk_74 + npHdrBodyUtil.WriteByte(0x00); // unk_75 + + npHdrBodyUtil.WriteInt32(0); // unk_76 + npHdrBodyUtil.WriteInt32(0); // unk_80 + npHdrBodyUtil.WriteInt32(0); // unk_84 + npHdrBodyUtil.WriteInt32(0); // unk_88 + npHdrBodyUtil.WriteInt32(0); // unk_92 + + } + + private byte[] encryptHeader(byte[] headerBytes) + { + AMCTRL.CIPHER_KEY ckey = new AMCTRL.CIPHER_KEY(); + AMCTRL.sceDrmBBCipherInit(out ckey, 1, DrmInfo.KeyType, headerKey, DrmInfo.VersionKey, 0); + AMCTRL.sceDrmBBCipherUpdate(ref ckey, headerBytes, headerBytes.Length); + AMCTRL.sceDrmBBCipherFinal(ref ckey); + return headerBytes; + } + + private Int64 isoBlocks; + private bool compress; + + private string discId; + + private byte[] paramSfo; + + private byte[] headerKey; + private byte[] dataKey; + + private FileStream isoStream; + + private MemoryStream npHdr; + private StreamUtil npHdrUtil; + + private MemoryStream npHdrBody; + private StreamUtil npHdrBodyUtil; + + private MemoryStream isoData; + private StreamUtil isoDataUtil; + + private MemoryStream npTbl; + private StreamUtil npTblUtil; + } +} diff --git a/PopsBuilder/Psp/PbpBuilder.cs b/PopsBuilder/Psp/PbpBuilder.cs index c356018..2460f67 100644 --- a/PopsBuilder/Psp/PbpBuilder.cs +++ b/PopsBuilder/Psp/PbpBuilder.cs @@ -10,14 +10,19 @@ namespace PopsBuilder.Psp { public static void CreatePbp(byte[]? paramSfo, byte[]? icon0Png, byte[]? icon1Png, byte[]? pic0Png, byte[]? pic1Png, byte[]? snd0At3, - byte[] dataPsp, NpDrmPsar dataPsar, string outputFile) + NpDrmPsar dataPsar, string outputFile, short version = 1) { using (FileStream pbpStream = File.Open(outputFile, FileMode.Create)) { + byte[] dataPsp = dataPsar.GenerateDataPsp(); + int padLen = Convert.ToInt32(0x100 - (dataPsp.Length % 0x100)); + Array.Resize(ref dataPsp, dataPsp.Length + padLen); + StreamUtil pbpUtil = new StreamUtil(pbpStream); pbpUtil.WriteByte(0x00); - pbpUtil.WriteStrWithPadding("PBP", 0x00, 0x5); + pbpUtil.WriteStr("PBP"); + pbpUtil.WriteInt16(version); pbpUtil.WriteInt16(1); // param location diff --git a/PopsBuilder/Psp/Rng.cs b/PopsBuilder/Psp/Rng.cs new file mode 100644 index 0000000..c7cbace --- /dev/null +++ b/PopsBuilder/Psp/Rng.cs @@ -0,0 +1,29 @@ +using PspCrypto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace PopsBuilder.Psp +{ + public static class Rng + { + public static byte[] RandomBytes(int length) + { + byte[] randomBytes = new byte[length]; + KIRKEngine.sceUtilsBufferCopyWithRange(randomBytes, randomBytes.Length, null, 0, KIRKEngine.KIRK_CMD_PRNG); + return randomBytes; + } + public static uint RandomUInt() + { + byte[] uintBytes = RandomBytes(0x4); + return BitConverter.ToUInt32(uintBytes); + } + public static int RandomInt() + { + byte[] intBytes = RandomBytes(0x4); + return BitConverter.ToInt32(intBytes); + } + } +} diff --git a/PopsBuilder/Resources.Designer.cs b/PopsBuilder/Resources.Designer.cs index 2a9c5a0..1b359db 100644 --- a/PopsBuilder/Resources.Designer.cs +++ b/PopsBuilder/Resources.Designer.cs @@ -22,7 +22,7 @@ namespace PopsBuilder { [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { + public class Resources { private static global::System.Resources.ResourceManager resourceMan; @@ -36,7 +36,7 @@ namespace PopsBuilder { /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PopsBuilder.Resources", typeof(Resources).Assembly); @@ -51,7 +51,7 @@ namespace PopsBuilder { /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + public static global::System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -63,7 +63,7 @@ namespace PopsBuilder { ///