Skip to content

Commit cda2fae

Browse files
Add failure diagnostics and tolerant metadata matching (v1.4.0)
- ChdContainer.LastError reports open/parse failures (CHDSharp errors, track/metadata parsing, missing parser, unrecognized file system, no-data-track hint) - Open catches track/metadata parsing exceptions instead of crashing - Invariant-culture WAV track index parsing - Case-insensitive matching for well-known track types (AUDIO/MODE2/MODE_2/CDI/PAD), IP.BIN and PC Engine boot signatures - Version bumped to 1.4.0; WhatsNew and package release notes updated
1 parent 45255f9 commit cda2fae

6 files changed

Lines changed: 77 additions & 28 deletions

File tree

‎VideoGameFileSystemParser/Parsers/ChdContainer.cs‎

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ private enum CueExportMode
4949
private Dictionary<int, byte[]>? _wavHeaders;
5050
private Dictionary<int, ulong>? _wavDataSizes;
5151

52+
/// <summary>
53+
/// Gets the reason the most recent open or parse operation failed, or <c>null</c> if it succeeded.
54+
/// </summary>
55+
public string? LastError { get; private set; }
56+
5257
/// <summary>
5358
/// Gets the read-only list of all file and directory entries in the container.
5459
/// </summary>
@@ -101,25 +106,38 @@ public ChdContainer(string chdPath)
101106
public bool Open(ConsoleType consoleType)
102107
{
103108
ConsoleType = consoleType;
109+
LastError = null;
104110

105111
var err = ChdFile.Open(_chdPath, out var chd);
106112
if (err != ChdError.Chderrnone || chd is null)
113+
{
114+
LastError = $"CHDSharp failed to open the file: {err} ({err.GetMessage()})";
107115
return false;
116+
}
108117

109118
_primaryChd = chd;
119+
try
120+
{
121+
var unitBytes = chd.UnitBytes;
122+
var reader = new SectorReader(chd, unitBytes);
123+
UnitBytes = unitBytes;
124+
HunkBytes = chd.HunkBytes;
125+
VolumeSize = chd.TotalBytes;
126+
VolumeName = Path.GetFileNameWithoutExtension(_chdPath);
110127

111-
var unitBytes = chd.UnitBytes;
112-
var reader = new SectorReader(chd, unitBytes);
113-
UnitBytes = unitBytes;
114-
HunkBytes = chd.HunkBytes;
115-
VolumeSize = chd.TotalBytes;
116-
VolumeName = Path.GetFileNameWithoutExtension(_chdPath);
117-
118-
_readerPool.Add(reader);
119-
HasDataTracks = reader.Tracks.Any(static t => t.IsDataTrack);
120-
lock (_poolLock)
128+
_readerPool.Add(reader);
129+
HasDataTracks = reader.Tracks.Any(static t => t.IsDataTrack);
130+
lock (_poolLock)
131+
{
132+
_availableReaders.Add(reader);
133+
}
134+
}
135+
catch (Exception ex)
121136
{
122-
_availableReaders.Add(reader);
137+
_primaryChd.Dispose();
138+
_primaryChd = null;
139+
LastError = $"CHDSharp opened the file but track/metadata parsing failed: {ex.Message}";
140+
return false;
123141
}
124142

125143
return true;
@@ -132,6 +150,8 @@ public bool Open(ConsoleType consoleType)
132150
/// <returns><c>true</c> if parsing succeeded; otherwise <c>false</c>.</returns>
133151
public bool MountAndParse(ConsoleType consoleType)
134152
{
153+
LastError = null;
154+
135155
if (!Open(consoleType))
136156
return false;
137157

@@ -164,11 +184,21 @@ or ConsoleType.GenericCueBinWav2352 or ConsoleType.GenericCueBinWav2048
164184

165185
var parser = ParserFactory.CreateParser(consoleType, _readerPool[0]);
166186
if (parser is null)
187+
{
188+
LastError = $"No file system parser is available for console type '{consoleType}'.";
167189
return false;
190+
}
168191

169192
var parsedRoot = new FsNode();
170193
if (!parser.Parse(parsedRoot))
194+
{
195+
var noDataTracksHint = HasDataTracks
196+
? string.Empty
197+
: " The CHD has no data tracks; it may not be a CD/disc image (it could be a hard-drive image).";
198+
LastError =
199+
$"The '{consoleType}' file system parser could not find a recognizable file system on this disc.{noDataTracksHint}";
171200
return false;
201+
}
172202

173203
BuildFromFsNode(parsedRoot);
174204

@@ -536,15 +566,16 @@ CueExportMode.CueBin2048 or CueExportMode.CueIso2048
536566
var dataFileExt = isIsoMode ? "iso" : "bin";
537567
var dataFileName = $"{_cueStemName}.{dataFileExt}";
538568

539-
if (currentFile != dataFileName)
569+
if (!string.Equals(currentFile, dataFileName, StringComparison.OrdinalIgnoreCase))
540570
{
541571
if (!freshFile)
542572
sb.AppendLine();
543573
sb.AppendLine(CultureInfo.InvariantCulture, $"FILE \"{dataFileName}\" BINARY");
544574
currentFile = dataFileName;
545575
}
546576

547-
var modeStr = t.TrackType.Contains("MODE2") || t.TrackType.Contains("CDI")
577+
var modeStr = t.TrackType.Contains("MODE2", StringComparison.OrdinalIgnoreCase) ||
578+
t.TrackType.Contains("CDI", StringComparison.OrdinalIgnoreCase)
548579
? $"MODE2/{_cueSectorSize}"
549580
: $"MODE1/{_cueSectorSize}";
550581

@@ -578,7 +609,7 @@ CueExportMode.CueBin2048 or CueExportMode.CueIso2048
578609
{
579610
var wavFileName = $"{_cueStemName}_Track{trackNum:D2}.wav";
580611

581-
if (currentFile != wavFileName)
612+
if (!string.Equals(currentFile, wavFileName, StringComparison.OrdinalIgnoreCase))
582613
{
583614
if (!freshFile)
584615
sb.AppendLine();
@@ -597,7 +628,7 @@ CueExportMode.CueBin2048 or CueExportMode.CueIso2048
597628
else
598629
{
599630
var containerFile = isIsoMode ? $"{_cueStemName}.iso" : $"{_cueStemName}.bin";
600-
if (currentFile != containerFile)
631+
if (!string.Equals(currentFile, containerFile, StringComparison.OrdinalIgnoreCase))
601632
{
602633
if (!freshFile)
603634
sb.AppendLine();
@@ -696,7 +727,7 @@ private bool TryParseWavTrackIndex(string entryName, out int trackIndex)
696727
if (!entryName.EndsWith(suffix, StringComparison.OrdinalIgnoreCase)) return false;
697728

698729
var numStr = entryName.Substring(prefix.Length, entryName.Length - prefix.Length - suffix.Length);
699-
return int.TryParse(numStr, out trackIndex) && _wavHeaders.ContainsKey(trackIndex);
730+
return int.TryParse(numStr, CultureInfo.InvariantCulture, out trackIndex) && _wavHeaders.ContainsKey(trackIndex);
700731
}
701732

702733
private static byte[] BuildWavHeader(ulong pcmDataSize)

‎VideoGameFileSystemParser/Parsers/SectorReader.cs‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ private bool PrepareHunk(uint lba, out uint rawOffsetInHunk)
440440
var hunkNum = chdFrame / sectorsPerHunk;
441441
var sectorInHunk = chdFrame % sectorsPerHunk;
442442

443-
var needsSwap = CurrentTrack?.TrackType == "AUDIO";
443+
var needsSwap = string.Equals(CurrentTrack?.TrackType, "AUDIO", StringComparison.OrdinalIgnoreCase);
444444

445445
if (hunkNum != _cachedHunkNum || _cachedHunkSwapped != needsSwap)
446446
{
@@ -722,8 +722,9 @@ public static uint GetSectorDataOffset(TrackInfo? track)
722722
{
723723
if (track is null) return 16;
724724
if (!track.IsDataTrack) return 0;
725-
if (track.TrackType.Contains("MODE2") || track.TrackType.Contains("MODE_2")) return 24;
726-
if (track.TrackType.Contains("CDI")) return 24;
725+
if (track.TrackType.Contains("MODE2", StringComparison.OrdinalIgnoreCase) ||
726+
track.TrackType.Contains("MODE_2", StringComparison.OrdinalIgnoreCase)) return 24;
727+
if (track.TrackType.Contains("CDI", StringComparison.OrdinalIgnoreCase)) return 24;
727728

728729
return 16;
729730
}
@@ -774,7 +775,8 @@ internal static List<TrackInfo> ParseTracksWithLba(ChdFile chd, uint unitBytes)
774775
Pregap = pregap,
775776
Postgap = postgap,
776777
TrackType = typeStr,
777-
IsDataTrack = typeStr.Contains("MODE") || typeStr.Contains("CDI"),
778+
IsDataTrack = typeStr.Contains("MODE", StringComparison.OrdinalIgnoreCase) ||
779+
typeStr.Contains("CDI", StringComparison.OrdinalIgnoreCase),
778780
Metadata = metaText
779781
});
780782
}
@@ -804,7 +806,7 @@ internal static List<TrackInfo> ParseTracksWithLba(ChdFile chd, uint unitBytes)
804806
{
805807
foreach (var t in tracks)
806808
{
807-
if (t.Metadata.Contains("PAD:"))
809+
if (t.Metadata.Contains("PAD:", StringComparison.OrdinalIgnoreCase))
808810
{
809811
isGdrom = true;
810812
break;

‎VideoGameFileSystemParser/Parsers/Systems/DreamcastAndOthers.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ private bool HasIpBin(TrackInfo track)
8585

8686
var sec = new byte[2048];
8787
var ok = _reader.ReadSector(track.StartLba, sec) &&
88-
Encoding.ASCII.GetString(sec, 0, IpBinSignature.Length) == IpBinSignature;
88+
string.Equals(Encoding.ASCII.GetString(sec, 0, IpBinSignature.Length), IpBinSignature,
89+
StringComparison.OrdinalIgnoreCase);
8990

9091
_reader.Reset();
9192
return ok;

‎VideoGameFileSystemParser/Parsers/Systems/PcEngineCdParser.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ private static bool HasBootSignature(byte[] sector)
220220
return false;
221221

222222
var descriptor = Encoding.ASCII.GetString(sector, 0x20, Signature.Length);
223-
if (descriptor == Signature)
223+
if (string.Equals(descriptor, Signature, StringComparison.OrdinalIgnoreCase))
224224
return true;
225225

226226
var gamesExpress = Encoding.ASCII.GetString(sector, 0, Math.Min(sector.Length, 128));

‎VideoGameFileSystemParser/VideoGameFileSystemParser.csproj‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
<!-- NuGet Package Metadata -->
1212
<PackageId>VideoGameFileSystemParser</PackageId>
13-
<Version>1.3.0</Version>
13+
<Version>1.4.0</Version>
1414
<Description>Video game file system parsing library for console disc images. Supports ISO 9660, UDF, XDVDFS, OperaFS, CD-i, and other formats through CHD, ISO and raw sector readers.</Description>
1515
<Title>VideoGameFileSystemParser</Title>
1616
<Copyright>Copyright (c) 2026 Peterson Fernandes (drpetersonfernandes)</Copyright>
@@ -20,9 +20,9 @@
2020
<PackageIcon>icon\icon.png</PackageIcon>
2121
<RepositoryUrl>https://github.com/drpetersonfernandes/VideoGameFileSystemParser</RepositoryUrl>
2222
<PackageTags>chd;mame;iso9660;udf;xdvdfs;operafs;cdi;ps1;ps2;ps3;psp;xbox;dreamcast;retro;videogame;disc;filesystem;emulation;emulator;emulator-development;emulators;filesystem;filetree;parser;parsing;parsing-algorithm;parsing-engine;parsing-library;parsing-table;video-game</PackageTags>
23-
<PackageReleaseNotes>v1.3.0: Updated the CHDSharp dependency from 1.2.0 to 1.4.3, picking up the latest CHD decoder fixes, performance work and CancellationToken-aware I/O APIs. No public API changes. Requires CHDSharp 1.4.3 or newer at runtime.</PackageReleaseNotes>
24-
<AssemblyVersion>1.3.0</AssemblyVersion>
25-
<FileVersion>1.3.0</FileVersion>
23+
<PackageReleaseNotes>v1.4.0: Added ChdContainer.LastError with detailed open/parse failure diagnostics; Open now reports CHDSharp and track/metadata errors instead of returning silently. Invariant-culture WAV track index parsing and case-insensitive matching for well-known track types (AUDIO/MODE2/CDI/PAD), IP.BIN and PC Engine boot signatures. Includes the v1.3.0 CHDSharp 1.4.3 dependency update.</PackageReleaseNotes>
24+
<AssemblyVersion>1.4.0</AssemblyVersion>
25+
<FileVersion>1.4.0</FileVersion>
2626
</PropertyGroup>
2727

2828
<ItemGroup>

‎WhatsNew.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
# What's New in VideoGameFileSystemParser
22

3-
This file tracks the highlights of each release. The latest version is **v1.3.0**.
3+
This file tracks the highlights of each release. The latest version is **v1.4.0**.
4+
5+
---
6+
7+
## v1.4.0 — Failure diagnostics and tolerant metadata matching
8+
9+
**Release date:** 2026
10+
11+
### New features
12+
13+
- **`ChdContainer.LastError`** — gets the reason the most recent open or parse operation failed, or `null` on success. `Open` and `MountAndParse` now report what went wrong (CHDSharp open failure with the CHDSharp error message, track/metadata parsing exceptions, missing parser for a console type, and unrecognized file systems) instead of only returning `false`. When the image has no data tracks, the parse-failure message now hints that the file may not be a CD/disc image.
14+
15+
### Fixes
16+
17+
- **Invariant-culture WAV track index parsing** — the virtual CUE/WAV export parses track indices from file names with `CultureInfo.InvariantCulture`, so numbers are read consistently regardless of the host locale.
18+
- **Tolerant matching for well-known metadata tokens** — track types (`AUDIO`, `MODE2`, `MODE_2`, `CDI`, `PAD:`), the Sega IP.BIN signature and the PC Engine boot signature are now compared case-insensitively; disc file/volume names keep their exact, ordinal matching.
419

520
---
621

0 commit comments

Comments
 (0)