Publish Ludarium source
Public source validation / source (push) Successful in 2m16s

This commit is contained in:
Ludarium release export
2026-09-03 02:08:58 +02:00
commit df869819ce
339 changed files with 46455 additions and 0 deletions
+444
View File
@@ -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;
}
}