Repository navigation
use RVCsharp #537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
use RVCsharp #537
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
87 changes: 87 additions & 0 deletions
87
WheelWizard.Test/Features/DiscImages/DiscImageServiceTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| using System.Text; | ||
| using Testably.Abstractions.Testing; | ||
| using WheelWizard.DiscImages; | ||
| using WheelWizard.DiscImages.Domain; | ||
|
|
||
| namespace WheelWizard.Test.Features.DiscImages; | ||
|
|
||
| public class DiscImageServiceTests | ||
| { | ||
| [Fact] | ||
| public void ReadInfo_ReadsWiiDiscHeader() | ||
| { | ||
| var fs = Games(); | ||
| fs.File.WriteAllBytes("/games/game.iso", WiiDisc("RMCP01", "MarioKartWii")); | ||
|
|
||
| var info = new DiscImageService(fs).ReadInfo("/games/game.iso"); | ||
|
|
||
| Assert.True(info.IsSuccess, info.IsFailure ? info.Error.Message : null); | ||
| Assert.Equal(new DiscImageInfo("RMCP01", "MarioKartWii", 0, DiscImageFormat.Iso), info.Value); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ReadInfo_RejectsFilesWithoutDiscHeader() | ||
| { | ||
| var fs = Games(); | ||
| fs.File.WriteAllBytes("/games/notes.iso", new byte[0x8000]); | ||
|
|
||
| Assert.True(new DiscImageService(fs).ReadInfo("/games/notes.iso").IsFailure); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ReadInfo_FailsForMissingFile() | ||
| { | ||
| Assert.True(new DiscImageService(new MockFileSystem()).ReadInfo("/missing.rvz").IsFailure); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ReadInfo_RereadsChangedFile() | ||
| { | ||
| var fs = Games(); | ||
| var service = new DiscImageService(fs); | ||
| fs.File.WriteAllBytes("/games/game.iso", WiiDisc("RMCE01", "MarioKartWii")); | ||
| Assert.Equal("RMCE01", service.ReadInfo("/games/game.iso").Value.GameId); | ||
|
|
||
| fs.File.WriteAllBytes("/games/game.iso", WiiDisc("RMCJ01", "MarioKartWii", extraBytes: 1)); | ||
|
|
||
| Assert.Equal("RMCJ01", service.ReadInfo("/games/game.iso").Value.GameId); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void ReadInfo_RetriesAfterFileWasLocked() | ||
| { | ||
| var fs = Games(); | ||
| var service = new DiscImageService(fs); | ||
| fs.File.WriteAllBytes("/games/game.iso", WiiDisc("RMCP01", "MarioKartWii")); | ||
|
|
||
| using (fs.File.Open("/games/game.iso", FileMode.Open, FileAccess.ReadWrite, FileShare.None)) | ||
| Assert.True(service.ReadInfo("/games/game.iso").IsFailure); | ||
|
|
||
| Assert.Equal("RMCP01", service.ReadInfo("/games/game.iso").Value.GameId); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void SupportedExtensions_CoverWiiContainers() | ||
| { | ||
| var extensions = new DiscImageService(new MockFileSystem()).SupportedExtensions; | ||
|
|
||
| Assert.Equal(new[] { ".iso", ".gcm", ".gcz", ".ciso", ".wbfs", ".wia", ".rvz" }.Order(), extensions.Order()); | ||
| } | ||
|
|
||
| // A file can't be written to a drive root the mock hasn't seen yet, which the Windows CI runner hits on D:. | ||
| private static MockFileSystem Games() | ||
| { | ||
| var fs = new MockFileSystem(); | ||
| fs.Directory.CreateDirectory("/games"); | ||
| return fs; | ||
| } | ||
|
|
||
| private static byte[] WiiDisc(string gameId, string name, int extraBytes = 0) | ||
| { | ||
| var data = new byte[0x50000 + extraBytes]; | ||
| Encoding.ASCII.GetBytes(gameId).CopyTo(data, 0); | ||
| new byte[] { 0x5D, 0x1C, 0x9E, 0xA3 }.CopyTo(data, 0x18); | ||
| Encoding.ASCII.GetBytes(name).CopyTo(data, 0x20); | ||
| return data; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| using System.IO.Abstractions; | ||
| using RVZSharp.Blobs; | ||
| using RVZSharp.Interfaces; | ||
| using RVZSharp.Models; | ||
| using WheelWizard.DiscImages.Domain; | ||
|
|
||
| namespace WheelWizard.DiscImages; | ||
|
|
||
| public interface IDiscImageService | ||
| { | ||
| /// <summary>File extensions (with dot) of the disc image formats a Wii game can be read from.</summary> | ||
| IReadOnlyList<string> SupportedExtensions { get; } | ||
|
|
||
| /// <summary>Reads the disc header of a game file in any supported format.</summary> | ||
| OperationResult<DiscImageInfo> ReadInfo(string path); | ||
| } | ||
|
|
||
| public sealed class DiscImageService(IFileSystem fileSystem) : IDiscImageService | ||
| { | ||
| private readonly object _cacheLock = new(); | ||
| private (string Path, long Length, DateTime LastWrite, DiscImageInfo Info)? _cache; | ||
|
|
||
| public IReadOnlyList<string> SupportedExtensions { get; } = Enum.GetValues<BlobType>().SelectMany(ExtensionsFor).ToArray(); | ||
|
|
||
| private static string[] ExtensionsFor(BlobType type) => | ||
| type switch | ||
| { | ||
| BlobType.Plain => [".iso", ".gcm"], | ||
| BlobType.Gcz => [".gcz"], | ||
| BlobType.Ciso => [".ciso"], | ||
| BlobType.Wbfs => [".wbfs"], | ||
| BlobType.Wia => [".wia"], | ||
| BlobType.Rvz => [".rvz"], | ||
| // TGC only wraps GameCube discs, and NFS needs key files from a Wii U title next to it. | ||
| _ => [], | ||
| }; | ||
|
|
||
| public OperationResult<DiscImageInfo> ReadInfo(string path) | ||
| { | ||
| if (string.IsNullOrWhiteSpace(path)) | ||
| return Fail("No game file is set."); | ||
|
|
||
| try | ||
| { | ||
| var file = fileSystem.FileInfo.New(path); | ||
| if (!file.Exists) | ||
| return Fail($"Game file not found: {path}"); | ||
|
|
||
| // The license loader asks on every poll, so only open the image again once the file changes. | ||
| lock (_cacheLock) | ||
| { | ||
| if ( | ||
| _cache is { } cached | ||
| && cached.Path == path | ||
| && cached.Length == file.Length | ||
| && cached.LastWrite == file.LastWriteTimeUtc | ||
| ) | ||
| return cached.Info; | ||
| } | ||
|
|
||
| var result = Read(path); | ||
| // Failures aren't cached: a file that was briefly locked should read fine on the next call. | ||
| if (result.IsSuccess) | ||
| { | ||
| lock (_cacheLock) | ||
| _cache = (path, file.Length, file.LastWriteTimeUtc, result.Value); | ||
| } | ||
| return result; | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| return new OperationError { Message = $"Failed to read game file: {ex.Message}", Exception = ex }; | ||
| } | ||
| } | ||
|
|
||
| private OperationResult<DiscImageInfo> Read(string path) | ||
| { | ||
| try | ||
| { | ||
| var stream = fileSystem.File.OpenRead(path); | ||
| IBlobReader reader; | ||
| try | ||
| { | ||
| reader = Blob.Open(stream, path, leaveOpen: false); | ||
| } | ||
| catch | ||
| { | ||
| // Blob.Open only takes ownership of the stream once it succeeds. | ||
| stream.Dispose(); | ||
| throw; | ||
| } | ||
|
|
||
| using (reader) | ||
| { | ||
| var info = DiscInfo.TryRead(reader); | ||
| if (info == null) | ||
| return Fail("This file is not a GameCube or Wii disc image."); | ||
|
|
||
| return new DiscImageInfo(info.GameId, info.InternalName, info.Revision, ToFormat(reader.Type)); | ||
| } | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| return new OperationError { Message = $"Failed to read game file: {ex.Message}", Exception = ex }; | ||
| } | ||
| } | ||
|
|
||
| private static DiscImageFormat ToFormat(BlobType type) => | ||
| type switch | ||
| { | ||
| BlobType.Gcz => DiscImageFormat.Gcz, | ||
| BlobType.Ciso => DiscImageFormat.Ciso, | ||
| BlobType.Wbfs => DiscImageFormat.Wbfs, | ||
| BlobType.Tgc => DiscImageFormat.Tgc, | ||
| BlobType.Wia => DiscImageFormat.Wia, | ||
| BlobType.Rvz => DiscImageFormat.Rvz, | ||
| BlobType.Nfs => DiscImageFormat.Nfs, | ||
| _ => DiscImageFormat.Iso, | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| namespace WheelWizard.DiscImages; | ||
|
|
||
| public static class DiscImagesExtensions | ||
| { | ||
| public static IServiceCollection AddDiscImages(this IServiceCollection services) | ||
| { | ||
| services.AddSingleton<IDiscImageService, DiscImageService>(); | ||
| return services; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| namespace WheelWizard.DiscImages.Domain; | ||
|
|
||
| public enum DiscImageFormat | ||
| { | ||
| Iso, | ||
| Gcz, | ||
| Ciso, | ||
| Wbfs, | ||
| Tgc, | ||
| Wia, | ||
| Rvz, | ||
| Nfs, | ||
| } | ||
|
|
||
| /// <summary>What the header of a game disc image says about the game on it.</summary> | ||
| public sealed record DiscImageInfo(string GameId, string InternalName, byte Revision, DiscImageFormat Format); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.