152 lines
5.7 KiB
C#
152 lines
5.7 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.HLE.FileSystem;
|
|
using Xunit;
|
|
|
|
namespace SharpEmu.Libs.Tests.FileSystem;
|
|
|
|
public sealed class App0ArchiveTests : IDisposable
|
|
{
|
|
private readonly string _directory = Directory.CreateTempSubdirectory("sharpemu-app0-").FullName;
|
|
private readonly string _root;
|
|
|
|
public App0ArchiveTests()
|
|
{
|
|
var archivePath = Path.Combine(_directory, "game.zar");
|
|
File.WriteAllBytes(archivePath, ZArchiveTestWriter.Build(
|
|
[
|
|
("eboot.bin", Bytes(100_000, 1)),
|
|
("sce_sys/param.json", Bytes(500, 2)),
|
|
("data/movies/intro.bk2", Bytes(80_000, 3)),
|
|
("data/level.bin", Bytes(3_000, 4)),
|
|
]));
|
|
_root = Path.Combine(_directory, "cache");
|
|
Directory.CreateDirectory(_root);
|
|
App0Archive.Mount(ZArchive.Open(archivePath)!, _root);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
App0Archive.Unmount();
|
|
Directory.Delete(_directory, recursive: true);
|
|
}
|
|
|
|
private static byte[] Bytes(int length, int seed)
|
|
{
|
|
var bytes = new byte[length];
|
|
for (var index = 0; index < length; index++)
|
|
{
|
|
bytes[index] = (byte)(index * 13 + seed);
|
|
}
|
|
|
|
return bytes;
|
|
}
|
|
|
|
[Fact]
|
|
public void HostPathsUnderTheMountResolveInTheArchive()
|
|
{
|
|
Assert.True(App0Archive.TryGetFile(Path.Combine(_root, "DATA", "level.bin"), out var size));
|
|
Assert.Equal(3_000UL, size);
|
|
Assert.True(App0Archive.IsDirectory(Path.Combine(_root, "data", "movies")));
|
|
Assert.True(App0Archive.IsDirectory(_root));
|
|
Assert.False(App0Archive.Exists(Path.Combine(_root, "data", "missing.bin")));
|
|
Assert.False(App0Archive.Exists(Path.Combine(_directory, "data", "level.bin")));
|
|
Assert.False(App0Archive.Exists(_root + "-other" + Path.DirectorySeparatorChar + "eboot.bin"));
|
|
}
|
|
|
|
[Fact]
|
|
public void AStreamSeeksAndReadsLikeAFile()
|
|
{
|
|
using var stream = App0Archive.OpenRead(Path.Combine(_root, "data", "movies", "intro.bk2"))!;
|
|
var expected = Bytes(80_000, 3);
|
|
|
|
Assert.Equal(expected.Length, stream.Length);
|
|
stream.Seek(-1_000, SeekOrigin.End);
|
|
var tail = new byte[4_000];
|
|
Assert.Equal(1_000, stream.Read(tail));
|
|
Assert.Equal(expected.AsSpan(79_000).ToArray(), tail.AsSpan(0, 1_000).ToArray());
|
|
Assert.Equal(0, stream.Read(tail));
|
|
|
|
var window = new byte[10_000];
|
|
Assert.Equal(window.Length, stream.ReadAt(window, 60_000));
|
|
Assert.Equal(expected.AsSpan(60_000, window.Length).ToArray(), window);
|
|
Assert.Equal(expected.Length, stream.Position);
|
|
Assert.Throws<NotSupportedException>(() => stream.Write(window, 0, 1));
|
|
}
|
|
|
|
[Fact]
|
|
public void ExtractionWritesOneFileAndListingsShowTheArchive()
|
|
{
|
|
var movie = Path.Combine(_root, "data", "movies", "intro.bk2");
|
|
Assert.True(App0Archive.Extract(movie));
|
|
Assert.Equal(Bytes(80_000, 3), File.ReadAllBytes(movie));
|
|
Assert.False(File.Exists(Path.Combine(_root, "data", "level.bin")));
|
|
Assert.False(App0Archive.Extract(Path.Combine(_root, "data", "missing.bin")));
|
|
|
|
App0Archive.ExtractDirectory("sce_sys");
|
|
Assert.Equal(Bytes(500, 2), File.ReadAllBytes(Path.Combine(_root, "sce_sys", "param.json")));
|
|
|
|
var names = App0Archive.List(_root).Select(entry => entry.Name).Order(StringComparer.OrdinalIgnoreCase);
|
|
Assert.Equal(["data", "eboot.bin", "sce_sys"], names);
|
|
}
|
|
|
|
[Fact]
|
|
public void RecursiveListingsJoinTheDiskAndTheArchive()
|
|
{
|
|
File.WriteAllBytes(Path.Combine(_root, "local.bk2"), [1]);
|
|
|
|
var movies = App0Archive.EnumerateFilesRecursive(_root, "*.bk2")
|
|
.Select(path => Path.GetRelativePath(_root, path).Replace('\\', '/'))
|
|
.Order(StringComparer.OrdinalIgnoreCase);
|
|
|
|
Assert.Equal(["data/movies/intro.bk2", "local.bk2"], movies);
|
|
Assert.Equal(
|
|
["data/level.bin", "data/movies/intro.bk2", "eboot.bin", "sce_sys/param.json"],
|
|
App0Archive.ListAllFiles().Order(StringComparer.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void AReadOpenPrefersTheDiskAndFallsBackToTheArchive()
|
|
{
|
|
var level = Path.Combine(_root, "data", "level.bin");
|
|
using (var archived = App0Archive.OpenReadOrFile(level))
|
|
{
|
|
Assert.IsType<ZArchiveFileStream>(archived);
|
|
Assert.Equal(3_000, archived.Length);
|
|
}
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(level)!);
|
|
File.WriteAllBytes(level, [9, 9]);
|
|
using (var onDisk = App0Archive.OpenReadOrFile(level))
|
|
{
|
|
Assert.IsType<FileStream>(onDisk);
|
|
Assert.Equal(2, onDisk.Length);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void TheCacheKeepsItsFilesUntilTheArchiveChanges()
|
|
{
|
|
var cacheParent = Path.Combine(_directory, "caches");
|
|
var archivePath = Path.Combine(_directory, "game.zar");
|
|
|
|
var cacheRoot = ZArchiveCache.ExtractMetadata(archivePath, cacheParent)!;
|
|
Assert.Equal(Path.Combine(cacheParent, "game"), cacheRoot);
|
|
Assert.Equal(Bytes(500, 2), File.ReadAllBytes(Path.Combine(cacheRoot, "sce_sys", "param.json")));
|
|
|
|
var kept = Path.Combine(cacheRoot, "kept.bin");
|
|
File.WriteAllBytes(kept, [1]);
|
|
ZArchiveCache.Prepare(archivePath, cacheParent);
|
|
Assert.True(File.Exists(kept));
|
|
|
|
File.SetLastWriteTimeUtc(archivePath, DateTime.UtcNow.AddMinutes(5));
|
|
ZArchiveCache.Prepare(archivePath, cacheParent);
|
|
Assert.False(File.Exists(kept));
|
|
|
|
var plain = Path.Combine(_directory, "plain.zar");
|
|
File.WriteAllBytes(plain, Bytes(4096, 5));
|
|
Assert.Null(ZArchiveCache.ExtractMetadata(plain, cacheParent));
|
|
}
|
|
}
|