This commit is contained in:
@@ -0,0 +1,444 @@
|
||||
using System.Text;
|
||||
using System.Buffers.Binary;
|
||||
using System.IO.Compression;
|
||||
using Ludarium.Application;
|
||||
using Ludarium.Domain;
|
||||
using OpenMcdf;
|
||||
|
||||
namespace Ludarium.UnitTests;
|
||||
|
||||
public sealed class AnalysisTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("3DS/LIMBO/BLUEROMS.WS.html")]
|
||||
[InlineData("3DS/World of Goo/download.htm")]
|
||||
public void WebPagesAreSupportArtifactsRatherThanGames(string path)
|
||||
{
|
||||
var result = Assert.IsType<NonGamePath>(LibraryContentPolicy.IdentifyNonGamePath(path));
|
||||
Assert.Equal("NonGameSupport", result.Category);
|
||||
Assert.StartsWith("web-page:", result.Evidence);
|
||||
}
|
||||
[Theory]
|
||||
[InlineData("roms/Tools/Switch/prod.keys")]
|
||||
[InlineData("PC/.gameyfin/cache/cover.jpg")]
|
||||
[InlineData("Game/runtime/libSceAudioOut.prx")]
|
||||
[InlineData("roms/PS2/BIOS/scph39001.bin")]
|
||||
[InlineData("roms/Switch/keys/prod.keys")]
|
||||
[InlineData("PC/Game/manuals/manual.pdf")]
|
||||
[InlineData("roms/N64/Game/readme.txt")]
|
||||
public void NonGameDirectoriesAreDeterministicallyIdentified(string path)
|
||||
{
|
||||
Assert.NotNull(LibraryContentPolicy.IdentifyNonGamePath(path));
|
||||
Assert.NotNull(LibraryContentPolicy.IdentifyNonGamePath("PC/FMT-v23.23.2/FMT/FMT.exe"));
|
||||
Assert.NotNull(LibraryContentPolicy.IdentifyNonGamePath("PC/Steam/steam.exe"));
|
||||
Assert.NotNull(LibraryContentPolicy.IdentifyNonGamePath("PC/Game DLC Unlocker/tool.exe"));
|
||||
Assert.NotNull(LibraryContentPolicy.IdentifyNonGamePath("PC/Steam/steamapps/common/tool.dll"));
|
||||
Assert.NotNull(LibraryContentPolicy.IdentifyNonGamePath("roms/Tools/Emulators/retroarch.exe"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Super Mario 64 (USA) (Rev 1).z64", "Super Mario 64")]
|
||||
[InlineData("Asterix.and.Obelix.Mission.Babylon-DUPLEX", "Asterix and Obelix Mission Babylon")]
|
||||
[InlineData("Fallout 4 [FitGirl Repack]", "Fallout 4")]
|
||||
[InlineData("Metal Gear Solid (Disc 2)", "Metal Gear Solid")]
|
||||
[InlineData("0002 - Need for Speed - Underground 2 (USA).zip", "Need for Speed - Underground 2")]
|
||||
[InlineData("Animal Crossing- City Folk [RUUE01].wbfs", "Animal Crossing- City Folk")]
|
||||
[InlineData("Pokemon Diamond USA NDS-LGC.nds", "Pokemon Diamond")]
|
||||
[InlineData("Asterix and Obelix Mission Babylon PS5", "Asterix and Obelix Mission Babylon")]
|
||||
[InlineData("Assassins's Creed 4 - Black Flag", "Assassin's Creed 4 - Black Flag")]
|
||||
[InlineData("Call of Duty - Black Ops 6 PS5 iNTERNAL-PS5B", "Call of Duty - Black Ops 6")]
|
||||
[InlineData("Crash Bandicoot - The Wrath of Cortex (USA) (v1", "Crash Bandicoot - The Wrath of Cortex")]
|
||||
[InlineData("Crash Team Racing [U] [SCUS-94426]", "Crash Team Racing")]
|
||||
[InlineData("Burnout Legends", "Burnout Legends")]
|
||||
[InlineData("The Darkness", "The Darkness")]
|
||||
public void CanonicalTitlesRemovePackagingNoise(string raw, string expected)
|
||||
{
|
||||
Assert.Equal(expected, LibraryContentPolicy.CanonicalGameTitle(raw));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RomHeaderEvidenceNeverContainsDatabaseInvalidControlCharacters()
|
||||
{
|
||||
var header = new byte[0xC0];
|
||||
"TEST\0TITLE"u8.CopyTo(header.AsSpan(0xA0));
|
||||
header[0xB2] = 0x96;
|
||||
var result = ArtifactAnalysis.Classify("synthetic.gba", header);
|
||||
Assert.DoesNotContain(result.Evidence, item => item.Value.Any(char.IsControl));
|
||||
}
|
||||
[Fact]
|
||||
public async Task HashesAreStreamedAndMatchKnownVectors()
|
||||
{
|
||||
await using var stream = new MemoryStream("abc"u8.ToArray());
|
||||
var result = await ArtifactAnalysis.HashAsync(stream, true, CancellationToken.None);
|
||||
Assert.Equal("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", result.Sha256);
|
||||
Assert.Equal("a9993e364706816aba3e25717850c26c9cd0d89d", result.Sha1);
|
||||
Assert.Equal("c2412435", result.Crc32);
|
||||
Assert.Equal(3, result.BytesRead);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("game.nes", new byte[] { 0x4e, 0x45, 0x53, 0x1a }, MediaType.Rom, Confidence.Deterministic)]
|
||||
[InlineData("setup.exe", new byte[] { 0x4d, 0x5a }, MediaType.WindowsPackage, Confidence.Deterministic)]
|
||||
[InlineData("archive.zip", new byte[] { 0x50, 0x4b, 0x03, 0x04 }, MediaType.Archive, Confidence.Deterministic)]
|
||||
[InlineData("mystery.bin", new byte[] { 0x00 }, MediaType.Unknown, Confidence.None)]
|
||||
public void ClassificationUsesSignatureEvidence(string name, byte[] header, MediaType expected, Confidence confidence)
|
||||
{
|
||||
var result = ArtifactAnalysis.Classify(name, header);
|
||||
Assert.Equal(expected, result.MediaType); Assert.Equal(confidence, result.Confidence);
|
||||
Assert.Equal(expected != MediaType.Unknown, result.Supported);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CueParserHandlesQuotedAndUnquotedReferences()
|
||||
{
|
||||
var refs = ArtifactAnalysis.ParseCueReferences("FILE \"Disc 1.bin\" BINARY\n FILE Disc2.bin BINARY");
|
||||
Assert.Equal(["Disc 1.bin", "Disc2.bin"], refs);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void M3uParserPreservesOrderAndIgnoresComments()
|
||||
{
|
||||
Assert.Equal(["Disc 2.cue", "Disc 1.cue"], ArtifactAnalysis.ParseM3uReferences("#EXTM3U\nDisc 2.cue\nDisc 1.cue"));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("../outside.bin", false)]
|
||||
[InlineData("tracks/disc.bin", true)]
|
||||
[InlineData("/absolute.bin", false)]
|
||||
public void DescriptorReferencesCannotEscapeRoot(string path, bool safe) => Assert.Equal(safe, ArtifactAnalysis.IsSafeRelativeReference(path));
|
||||
|
||||
[Fact]
|
||||
public void ScanStateMachineRejectsInvalidTransitions()
|
||||
{
|
||||
var run = new ScanRun(Guid.NewGuid(), Guid.NewGuid(), ScanMode.Quick, ScanState.Queued, "Queued", 0, null, 0, null, null, false, DateTimeOffset.UtcNow);
|
||||
Assert.Equal(ScanState.Verifying, run.Transition(ScanState.Verifying, "Verify").State);
|
||||
Assert.Throws<InvalidOperationException>(() => run.Transition(ScanState.Completed, "Complete"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LibraryRequiresAbsolutePath() => Assert.Throws<ArgumentException>(() => LibraryRoot.Create("ROMs", "relative", LibraryKind.Rom));
|
||||
|
||||
[Fact]
|
||||
public void PeInspectionReadsArchitectureWithoutExecutingImage()
|
||||
{
|
||||
using var stream = File.OpenRead(typeof(AnalysisTests).Assembly.Location);
|
||||
var result = WindowsPackageAnalysis.InspectPe(stream);
|
||||
Assert.True(result.IsManaged);
|
||||
Assert.True(result.Kind is "library" or "executable");
|
||||
Assert.NotEqual(Confidence.None, result.Confidence);
|
||||
Assert.Contains("ProductVersion", result.VersionInfo.Keys);
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "pe.version.ProductVersion");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NumberedInstallerPayloadGapsAreDetected() =>
|
||||
Assert.Equal([2], WindowsPackageAnalysis.MissingNumberedPayloads(["setup-1.bin", "setup-3.bin"]));
|
||||
|
||||
[Fact]
|
||||
public void MsiSummaryInformationIsReadWithoutInvokingWindowsInstaller()
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), $"ludarium-{Guid.NewGuid():N}.msi");
|
||||
try
|
||||
{
|
||||
using (var root = RootStorage.Create(path))
|
||||
using (var summary = root.CreateStream("\u0005SummaryInformation"))
|
||||
summary.Write(CreateSummaryPropertySet((2, "Synthetic Installer"), (4, "Ludarium Tests"),
|
||||
(7, "x64;1033"), (9, "{11111111-2222-3333-4444-555555555555}")));
|
||||
|
||||
var result = WindowsPackageAnalysis.InspectMsiSummary(path);
|
||||
|
||||
Assert.Equal("Synthetic Installer", result["Title"]);
|
||||
Assert.Equal("Ludarium Tests", result["Author"]);
|
||||
Assert.Equal("x64", result["Architecture"]);
|
||||
Assert.Equal("1033", result["Language"]);
|
||||
Assert.Equal("{11111111-2222-3333-4444-555555555555}", result["PackageCode"]);
|
||||
}
|
||||
finally { File.Delete(path); }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("game.z64", new byte[] { 0x80, 0x37, 0x12, 0x40 }, "n64")]
|
||||
[InlineData("game.v64", new byte[] { 0x37, 0x80, 0x40, 0x12 }, "n64")]
|
||||
[InlineData("game.n64", new byte[] { 0x40, 0x12, 0x37, 0x80 }, "n64")]
|
||||
[InlineData("game.gba", new byte[] { 0, 0, 0, 0, 0x24, 0xff, 0xae, 0x51 }, "gba")]
|
||||
public void RomHeadersOverrideExtensionHints(string name, byte[] header, string platform)
|
||||
{
|
||||
var result = ArtifactAnalysis.Classify(name, header);
|
||||
Assert.Equal(MediaType.Rom, result.MediaType);
|
||||
Assert.Equal(platform, result.Platform);
|
||||
Assert.Equal(Confidence.Deterministic, result.Confidence);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("game.cso", new byte[] { 0x43, 0x49, 0x53, 0x4f }, "psp", "PSP CSO")]
|
||||
[InlineData("game.nsp", new byte[] { 0x50, 0x46, 0x53, 0x30 }, "switch", "Switch PFS0")]
|
||||
public void ModernContainerSignaturesProvideDeterministicPlatformEvidence(string name, byte[] header,
|
||||
string platform, string signature)
|
||||
{
|
||||
var result = ArtifactAnalysis.Classify(name, header);
|
||||
|
||||
Assert.Equal(MediaType.DiscImage, result.MediaType);
|
||||
Assert.Equal(platform, result.Platform);
|
||||
Assert.Equal(Confidence.Deterministic, result.Confidence);
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "signature" && item.Value == signature);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SwitchXciHeaderIsDetectedAtItsSpecifiedOffset()
|
||||
{
|
||||
var header = new byte[0x104];
|
||||
"HEAD"u8.CopyTo(header.AsSpan(0x100));
|
||||
|
||||
var result = ArtifactAnalysis.Classify("game.xci", header);
|
||||
|
||||
Assert.Equal("switch", result.Platform);
|
||||
Assert.Equal(Confidence.Deterministic, result.Confidence);
|
||||
Assert.Contains(result.Evidence, item => item.Value == "Switch XCI");
|
||||
|
||||
var misplaced = ArtifactAnalysis.Classify("roms/PS2/game.xci", header, "ps2");
|
||||
Assert.Equal("switch", misplaced.Platform);
|
||||
Assert.Contains(misplaced.Evidence, item => item.Kind == "platform.conflict" &&
|
||||
item.Value == "directory:ps2;signature:switch");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("psx")]
|
||||
[InlineData("ps2")]
|
||||
[InlineData("psp")]
|
||||
public async Task Iso9660ContentsIdentifyPlayStationFamiliesWithoutDirectoryHints(string expectedPlatform)
|
||||
{
|
||||
await using var iso = new MemoryStream(CreatePlayStationIso(expectedPlatform));
|
||||
|
||||
var result = await DiscImageAnalysis.InspectIsoAsync(iso, CancellationToken.None);
|
||||
|
||||
Assert.Equal(expectedPlatform, result.Platform);
|
||||
Assert.Equal(Confidence.Deterministic, result.Confidence);
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "disc.signature" && item.Value == "ISO9660-CD001");
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "disc.platform" && item.Value.StartsWith(expectedPlatform + ":", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TruncatedOrGenericIsoNeverInventsAPlatform()
|
||||
{
|
||||
await using var truncated = new MemoryStream(new byte[1024]);
|
||||
Assert.Null((await DiscImageAnalysis.InspectIsoAsync(truncated, CancellationToken.None)).Platform);
|
||||
|
||||
await using var generic = new MemoryStream(CreatePlayStationIso(null));
|
||||
var result = await DiscImageAnalysis.InspectIsoAsync(generic, CancellationToken.None);
|
||||
Assert.Null(result.Platform);
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "disc.signature");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(false, "psp")]
|
||||
[InlineData(true, "psx")]
|
||||
public async Task PbpPayloadDeterministicallyDistinguishesPspHomebrewFromPsx(bool psx, string expected)
|
||||
{
|
||||
var bytes = new byte[96];
|
||||
"\0PBP"u8.CopyTo(bytes);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(4, 4), 0x00010000);
|
||||
var offsets = psx
|
||||
? new uint[] { 40, 40, 40, 40, 40, 40, 40, 64 }
|
||||
: new uint[] { 40, 40, 40, 40, 40, 40, 40, 96 };
|
||||
for (var index = 0; index < offsets.Length; index++)
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(8 + index * 4, 4), offsets[index]);
|
||||
if (psx) "PSISOIMG"u8.CopyTo(bytes.AsSpan(64));
|
||||
else new byte[] { 0x7f, (byte)'E', (byte)'L', (byte)'F' }.CopyTo(bytes.AsSpan(40));
|
||||
|
||||
await using var stream = new MemoryStream(bytes);
|
||||
var result = await DiscImageAnalysis.InspectPbpAsync(stream, CancellationToken.None);
|
||||
|
||||
Assert.Equal(expected, result.Platform);
|
||||
Assert.Equal(Confidence.Deterministic, result.Confidence);
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "disc.signature" && item.Value == "PBP");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReproduciblePspHomebrewFixturePassesTheExactPbpInspector()
|
||||
{
|
||||
await using var compressed = File.OpenRead(Path.Combine(AppContext.BaseDirectory, "fixtures", "ludarium-psp-fixture.pbp.gz"));
|
||||
await using var gzip = new GZipStream(compressed, CompressionMode.Decompress);
|
||||
await using var fixture = new MemoryStream();
|
||||
await gzip.CopyToAsync(fixture);
|
||||
fixture.Position = 0;
|
||||
|
||||
var result = await DiscImageAnalysis.InspectPbpAsync(fixture, CancellationToken.None);
|
||||
|
||||
Assert.Equal(130008, fixture.Length);
|
||||
Assert.Equal("psp", result.Platform);
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "disc.platform" && item.Value == "psp:PBP:ELF");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PbpInspectorRejectsNonMonotonicOffsets()
|
||||
{
|
||||
var bytes = new byte[64];
|
||||
"\0PBP"u8.CopyTo(bytes);
|
||||
for (var index = 0; index < 8; index++)
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(8 + index * 4, 4), (uint)(40 + index));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes.AsSpan(24, 4), 39);
|
||||
|
||||
await using var stream = new MemoryStream(bytes);
|
||||
var result = await DiscImageAnalysis.InspectPbpAsync(stream, CancellationToken.None);
|
||||
|
||||
Assert.Null(result.Platform);
|
||||
Assert.Equal(Confidence.None, result.Confidence);
|
||||
Assert.Empty(result.Evidence);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NintendoDsHeaderProvidesSemanticEvidence()
|
||||
{
|
||||
var header = new byte[0x160];
|
||||
Encoding.ASCII.GetBytes("SYNTHETIC DS").CopyTo(header, 0);
|
||||
Encoding.ASCII.GetBytes("ALPE").CopyTo(header, 12);
|
||||
Encoding.ASCII.GetBytes("01").CopyTo(header, 16);
|
||||
header[18] = 0; header[30] = 2;
|
||||
new byte[] { 0x24, 0xff, 0xae, 0x51 }.CopyTo(header, 0xC0);
|
||||
|
||||
var result = ArtifactAnalysis.Classify("unknown.bin", header);
|
||||
|
||||
Assert.Equal("nds", result.Platform);
|
||||
Assert.Equal(Confidence.Deterministic, result.Confidence);
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "rom.title" && item.Value == "SYNTHETIC DS");
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "rom.gameCode" && item.Value == "ALPE");
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "rom.region" && item.Value == "usa");
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x7FC0)]
|
||||
[InlineData(0x81C0)]
|
||||
public void SnesHeadersWithAndWithoutCopierPrefixAreDetected(int headerOffset)
|
||||
{
|
||||
var header = new byte[headerOffset + 0x20];
|
||||
Encoding.ASCII.GetBytes("SYNTHETIC SNES").CopyTo(header, headerOffset);
|
||||
header[headerOffset + 0x15] = 0x20;
|
||||
header[headerOffset + 0x19] = 1;
|
||||
BitConverter.GetBytes((ushort)0xEDCB).CopyTo(header, headerOffset + 0x1C);
|
||||
BitConverter.GetBytes((ushort)0x1234).CopyTo(header, headerOffset + 0x1E);
|
||||
|
||||
var result = ArtifactAnalysis.Classify("cartridge.dat", header);
|
||||
|
||||
Assert.Equal(MediaType.Rom, result.MediaType);
|
||||
Assert.Equal("snes", result.Platform);
|
||||
Assert.Contains(result.Evidence, item => item.Kind == "rom.title" && item.Value == "SYNTHETIC SNES");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InvalidSnesChecksumDoesNotCreateFalseSignature()
|
||||
{
|
||||
var header = new byte[0x8000];
|
||||
header[0x7FD5] = 0x20;
|
||||
Assert.Equal(MediaType.Unknown, ArtifactAnalysis.Classify("unknown.dat", header).MediaType);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Games", "/library/games", "PC/Example/setup.bin", "windows")]
|
||||
[InlineData("Games", "/library/games", "roms/3ds/Example/game.3ds", "3ds")]
|
||||
[InlineData("Games", "/library/games", "roms/PS1/Example/disc.cue", "psx")]
|
||||
[InlineData("Games", "/library/games", "roms/ngc/Example/game.rvz", "gamecube")]
|
||||
[InlineData("PS4 Games", "/library/ps4", "Example/game.pkg", "ps4")]
|
||||
[InlineData("PS5 Games", "/library/ps5", "Example/game.pkg", "ps5")]
|
||||
public void PlatformHintsFollowConfiguredDirectoryTaxonomy(string libraryName, string libraryPath, string relativePath, string expected) =>
|
||||
Assert.Equal(expected, ArtifactAnalysis.InferPlatformHint(libraryName, libraryPath, relativePath));
|
||||
|
||||
[Fact]
|
||||
public void DirectoryHintClassifiesPlatformWithoutOverridingDeterministicFormatEvidence()
|
||||
{
|
||||
var hinted = ArtifactAnalysis.Classify("roms/PS2/game.iso", ReadOnlySpan<byte>.Empty, "ps2");
|
||||
Assert.Equal("ps2", hinted.Platform);
|
||||
Assert.Contains(hinted.Evidence, evidence => evidence.Kind == "directory.platform" && evidence.Value == "ps2");
|
||||
|
||||
var signatureWins = ArtifactAnalysis.Classify("roms/PS2/misplaced.nes", new byte[] { 0x4e, 0x45, 0x53, 0x1a }, "ps2");
|
||||
Assert.Equal("nes", signatureWins.Platform);
|
||||
|
||||
var multipartVolume = ArtifactAnalysis.Classify("PS5-Release/part.v64", ReadOnlySpan<byte>.Empty, "ps5");
|
||||
Assert.Equal("ps5", multipartVolume.Platform);
|
||||
Assert.Equal(MediaType.Unknown, multipartVolume.MediaType);
|
||||
Assert.Contains(multipartVolume.Evidence, evidence => evidence.Kind == "platform.conflict");
|
||||
}
|
||||
|
||||
private static byte[] CreateSummaryPropertySet(params (int Id, string Value)[] properties)
|
||||
{
|
||||
var encoded = properties.Select(property =>
|
||||
{
|
||||
var value = Encoding.Latin1.GetBytes(property.Value + '\0');
|
||||
var length = 8 + value.Length;
|
||||
var padded = (length + 3) & ~3;
|
||||
var bytes = new byte[padded];
|
||||
BitConverter.GetBytes(30).CopyTo(bytes, 0);
|
||||
BitConverter.GetBytes(value.Length).CopyTo(bytes, 4);
|
||||
value.CopyTo(bytes, 8);
|
||||
return (property.Id, Bytes: bytes);
|
||||
}).ToArray();
|
||||
var sectionOffset = 48;
|
||||
var valuesOffset = 8 + encoded.Length * 8;
|
||||
var sectionSize = valuesOffset + encoded.Sum(item => item.Bytes.Length);
|
||||
var result = new byte[sectionOffset + sectionSize];
|
||||
BitConverter.GetBytes((ushort)0xfffe).CopyTo(result, 0);
|
||||
BitConverter.GetBytes(1).CopyTo(result, 24);
|
||||
BitConverter.GetBytes(sectionOffset).CopyTo(result, 44);
|
||||
BitConverter.GetBytes(sectionSize).CopyTo(result, sectionOffset);
|
||||
BitConverter.GetBytes(encoded.Length).CopyTo(result, sectionOffset + 4);
|
||||
var cursor = valuesOffset;
|
||||
for (var index = 0; index < encoded.Length; index++)
|
||||
{
|
||||
BitConverter.GetBytes(encoded[index].Id).CopyTo(result, sectionOffset + 8 + index * 8);
|
||||
BitConverter.GetBytes(cursor).CopyTo(result, sectionOffset + 12 + index * 8);
|
||||
encoded[index].Bytes.CopyTo(result, sectionOffset + cursor);
|
||||
cursor += encoded[index].Bytes.Length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static byte[] CreatePlayStationIso(string? platform)
|
||||
{
|
||||
const int sector = 2048;
|
||||
const int rootLba = 20;
|
||||
const int configLba = 22;
|
||||
var image = new byte[24 * sector];
|
||||
var pvd = image.AsSpan(16 * sector, sector);
|
||||
pvd[0] = 1;
|
||||
"CD001"u8.CopyTo(pvd[1..]);
|
||||
pvd[6] = 1;
|
||||
|
||||
var directory = image.AsSpan(rootLba * sector, sector);
|
||||
var directoryLength = 0;
|
||||
if (platform is "psp")
|
||||
{
|
||||
directoryLength += WriteIsoRecord(directory[directoryLength..], 21, sector, "PSP_GAME");
|
||||
directoryLength += WriteIsoRecord(directory[directoryLength..], 23, 32, "UMD_DATA.BIN;1");
|
||||
}
|
||||
else if (platform is "psx" or "ps2")
|
||||
{
|
||||
var config = platform == "ps2" ? "BOOT2 = cdrom0:\\\\SLUS_000.00;1\r\n" : "BOOT = cdrom:\\\\SCUS_000.00;1\r\n";
|
||||
var configBytes = Encoding.ASCII.GetBytes(config);
|
||||
configBytes.CopyTo(image, configLba * sector);
|
||||
directoryLength += WriteIsoRecord(directory[directoryLength..], configLba, configBytes.Length, "SYSTEM.CNF;1");
|
||||
}
|
||||
else
|
||||
{
|
||||
directoryLength += WriteIsoRecord(directory, 23, 16, "README.TXT;1");
|
||||
}
|
||||
WriteIsoRecord(pvd[156..], rootLba, directoryLength, "\0");
|
||||
return image;
|
||||
}
|
||||
|
||||
private static int WriteIsoRecord(Span<byte> destination, int extent, int dataLength, string name)
|
||||
{
|
||||
var nameBytes = Encoding.ASCII.GetBytes(name);
|
||||
var length = 33 + nameBytes.Length + (nameBytes.Length % 2 == 0 ? 1 : 0);
|
||||
destination[..length].Clear();
|
||||
destination[0] = (byte)length;
|
||||
BitConverter.GetBytes(extent).CopyTo(destination[2..]);
|
||||
BitConverter.GetBytes(BinaryPrimitives.ReverseEndianness(extent)).CopyTo(destination[6..]);
|
||||
BitConverter.GetBytes(dataLength).CopyTo(destination[10..]);
|
||||
BitConverter.GetBytes(BinaryPrimitives.ReverseEndianness(dataLength)).CopyTo(destination[14..]);
|
||||
destination[25] = 0;
|
||||
destination[28] = 1;
|
||||
destination[31] = 1;
|
||||
destination[32] = (byte)nameBytes.Length;
|
||||
nameBytes.CopyTo(destination[33..]);
|
||||
return length;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user