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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