mirror of
https://github.com/itsmemac/Packwright.git
synced 2026-10-06 17:00:23 +02:00
A cross-platform (Windows, Linux, macOS) manager for PS5 dumps, images and packages, grown out of PS5 PKG Tool by pearlxcore (GPL-3.0). - Library of dumps, packages and images (cards or list), search language, grouping, saved views, health report and export (CSV, JSON, web page) - Edit and autofill details (including region, IDs, versions, features and icon), in the library or written into the file; optional Title ID lookup - Convert, build, extract, verify, repair and edit exFAT, FFPKG, FFPFSC and ZArchive images and debug packages, with presets - Send to console over FTP or by URL on the local network - Command-line mode - Installers for Windows, macOS and Linux, and an opt-in update check See README.md and THIRD_PARTY_NOTICES.md for credits, licenses and the list of changes made to the original project.
149 lines
7.9 KiB
C#
149 lines
7.9 KiB
C#
using System.Buffers.Binary;
|
|
using System.Text;
|
|
|
|
namespace Packwright.Containers;
|
|
|
|
public readonly record struct AmprFileRecord(string RelativePath, long Size, long UnixModifiedTime);
|
|
public sealed record AmprIndexInfo(int RecordCount, int HashSlotCount, long PathBlobLength);
|
|
|
|
/// <summary>Native AMPRIDX3 index builder and structural validator.</summary>
|
|
public static class AmprIndex
|
|
{
|
|
private const int HeaderSize = 48;
|
|
private const int RecordSize = 24;
|
|
private const int SlotSize = 16;
|
|
private const ulong FnvOffset = 1469598103934665603;
|
|
private const ulong FnvPrime = 1099511628211;
|
|
|
|
public static byte[] Build(IEnumerable<AmprFileRecord> sourceFiles)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(sourceFiles);
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
var rows = sourceFiles
|
|
.Where(file => file.Size >= 0)
|
|
.Select(file => new AmprFileRecord(NormalizeRelativePath(file.RelativePath), file.Size, file.UnixModifiedTime))
|
|
.Where(file => !file.RelativePath.Equals("ampr_emu.index", StringComparison.OrdinalIgnoreCase) &&
|
|
!file.RelativePath.Equals("ampr_emu.index.tmp", StringComparison.OrdinalIgnoreCase))
|
|
.Where(file => seen.Add(Key("/app0/" + file.RelativePath)))
|
|
.OrderBy(file => Key("/app0/" + file.RelativePath), StringComparer.Ordinal)
|
|
.ToArray();
|
|
if (rows.Length == 0) return [];
|
|
|
|
var paths = new List<byte[]>(rows.Length);
|
|
long pathBlobLength = 0;
|
|
foreach (AmprFileRecord row in rows)
|
|
{
|
|
byte[] path = Encoding.UTF8.GetBytes("/app0/" + row.RelativePath);
|
|
if (pathBlobLength > uint.MaxValue)
|
|
throw new InvalidDataException("The AMPR path table exceeds its 32-bit format limit.");
|
|
paths.Add(path);
|
|
pathBlobLength = checked(pathBlobLength + path.Length + 1);
|
|
}
|
|
|
|
int slotCount = 2;
|
|
while (slotCount < checked(rows.Length * 2)) slotCount = checked(slotCount * 2);
|
|
long pathEnd = checked(HeaderSize + (long)rows.Length * RecordSize + pathBlobLength);
|
|
long hashOffset = Align(pathEnd, SlotSize);
|
|
long totalLength = checked(hashOffset + (long)slotCount * SlotSize);
|
|
if (totalLength > int.MaxValue) throw new InvalidDataException("The AMPR index is too large to hold in memory.");
|
|
byte[] result = new byte[checked((int)totalLength)];
|
|
"AMPRIDX3"u8.CopyTo(result);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(8, 4), 3);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(12, 4), RecordSize);
|
|
BinaryPrimitives.WriteUInt64LittleEndian(result.AsSpan(16, 8), checked((ulong)rows.Length));
|
|
BinaryPrimitives.WriteUInt64LittleEndian(result.AsSpan(24, 8), checked((ulong)pathBlobLength));
|
|
BinaryPrimitives.WriteUInt64LittleEndian(result.AsSpan(32, 8), checked((ulong)hashOffset));
|
|
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(40, 4), SlotSize);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(result.AsSpan(44, 4), checked((uint)slotCount));
|
|
|
|
int pathOffset = 0;
|
|
int pathBase = checked(HeaderSize + rows.Length * RecordSize);
|
|
for (int index = 0; index < rows.Length; index++)
|
|
{
|
|
Span<byte> record = result.AsSpan(HeaderSize + index * RecordSize, RecordSize);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(0, 4), checked((uint)pathOffset));
|
|
BinaryPrimitives.WriteUInt32LittleEndian(record.Slice(4, 4), checked((uint)paths[index].Length));
|
|
BinaryPrimitives.WriteUInt64LittleEndian(record.Slice(8, 8), checked((ulong)rows[index].Size));
|
|
BinaryPrimitives.WriteInt64LittleEndian(record.Slice(16, 8), rows[index].UnixModifiedTime);
|
|
paths[index].CopyTo(result, pathBase + pathOffset);
|
|
pathOffset += paths[index].Length + 1;
|
|
}
|
|
|
|
int mask = slotCount - 1;
|
|
for (int index = 0; index < rows.Length; index++)
|
|
{
|
|
ulong hash = HashPath("/app0/" + rows[index].RelativePath);
|
|
int position = checked((int)(hash & (uint)mask));
|
|
while (true)
|
|
{
|
|
Span<byte> slot = result.AsSpan(checked((int)hashOffset + position * SlotSize), SlotSize);
|
|
uint existingIndex = BinaryPrimitives.ReadUInt32LittleEndian(slot.Slice(8, 4));
|
|
if (existingIndex == 0)
|
|
{
|
|
BinaryPrimitives.WriteUInt64LittleEndian(slot.Slice(0, 8), hash);
|
|
BinaryPrimitives.WriteUInt32LittleEndian(slot.Slice(8, 4), checked((uint)(index + 1)));
|
|
break;
|
|
}
|
|
if (BinaryPrimitives.ReadUInt64LittleEndian(slot.Slice(0, 8)) == hash)
|
|
BinaryPrimitives.WriteUInt32LittleEndian(slot.Slice(12, 4), 1);
|
|
position = (position + 1) & mask;
|
|
}
|
|
}
|
|
_ = Inspect(result);
|
|
return result;
|
|
}
|
|
|
|
public static AmprIndexInfo Inspect(ReadOnlySpan<byte> data)
|
|
{
|
|
if (data.Length < HeaderSize || !data.Slice(0, 8).SequenceEqual("AMPRIDX3"u8))
|
|
throw new InvalidDataException("AMPRIDX3 magic is missing.");
|
|
uint version = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(8, 4));
|
|
uint recordSize = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(12, 4));
|
|
ulong rowCount = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(16, 8));
|
|
ulong pathLength = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(24, 8));
|
|
ulong hashOffset = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(32, 8));
|
|
uint slotSize = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(40, 4));
|
|
uint slotCount = BinaryPrimitives.ReadUInt32LittleEndian(data.Slice(44, 4));
|
|
if (version != 3 || recordSize != RecordSize || slotSize != SlotSize || rowCount == 0 || slotCount == 0 ||
|
|
(slotCount & (slotCount - 1)) != 0 || rowCount > int.MaxValue || slotCount > int.MaxValue)
|
|
throw new InvalidDataException("The AMPRIDX3 header is invalid.");
|
|
ulong recordsEnd = checked((ulong)HeaderSize + rowCount * recordSize);
|
|
ulong pathEnd = checked(recordsEnd + pathLength);
|
|
ulong expectedEnd = checked(hashOffset + slotCount * slotSize);
|
|
if (hashOffset < pathEnd || hashOffset % slotSize != 0 || expectedEnd != (ulong)data.Length)
|
|
throw new InvalidDataException("The AMPRIDX3 ranges are invalid.");
|
|
|
|
for (int index = 0; index < checked((int)rowCount); index++)
|
|
{
|
|
ReadOnlySpan<byte> record = data.Slice(HeaderSize + index * RecordSize, RecordSize);
|
|
uint offset = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(0, 4));
|
|
uint length = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(4, 4));
|
|
if ((ulong)offset + length >= pathLength || data[checked((int)(recordsEnd + offset + length))] != 0)
|
|
throw new InvalidDataException("An AMPRIDX3 path record is invalid.");
|
|
}
|
|
return new AmprIndexInfo(checked((int)rowCount), checked((int)slotCount), checked((long)pathLength));
|
|
}
|
|
|
|
public static ulong HashPath(string path)
|
|
{
|
|
ulong hash = FnvOffset;
|
|
foreach (Rune value in Key(path).EnumerateRunes())
|
|
{
|
|
hash ^= checked((uint)value.Value);
|
|
hash = unchecked(hash * FnvPrime);
|
|
}
|
|
return hash == 0 ? 1 : hash;
|
|
}
|
|
|
|
private static string NormalizeRelativePath(string path)
|
|
{
|
|
string normalized = path.Replace('\\', '/').TrimStart('/');
|
|
if (normalized.Length == 0 || normalized.Split('/').Any(part => part.Length == 0 || part is "." or ".."))
|
|
throw new InvalidDataException($"The AMPR relative path is invalid: {path}");
|
|
return normalized;
|
|
}
|
|
|
|
private static string Key(string path) => path.Replace('\\', '/').ToLowerInvariant();
|
|
private static long Align(long value, int alignment) => checked((value + alignment - 1) / alignment * alignment);
|
|
}
|