From 003b3a9dcad7fd57d6a02fa4de252acf9eb04a6c Mon Sep 17 00:00:00 2001 From: Peterson Fernandes Date: Mon, 2 Nov 2026 11:30:38 -0300 Subject: [PATCH 1/9] fix: stop reporting expected user/usage errors as bugs The Bug Report API held 383 CHDSharpCli reports and every one of them was a chdman-parity input validation, not a defect: 382x "Data size ... is not divisible by sector size 2048" and 1x "createdvd requires --input ...". Those paths already printed the right text and returned exit code 1, but they logged at Warning, and BugReportSink forwards every Warning+ event. - BugReportSink now skips events tagged with the UserError property. - Add ILogger.UserError(...) (CHDSharpCli/UserErrorLogging.cs): warning level for the console and rolling file logs, tagged so no report is sent. - Tag the 216 usage/validation warnings in the CLI (bad flags, invalid numbers, missing paths, sector-misaligned input, existing output without --force, overridden format options, ...). Console text and exit codes are unchanged, so chdman parity is untouched. - Real runtime failures (Open failed, * failed: {Message}, verify failures, TOC extraction failures) stay on Warning and are still reported. - Regression tests for the two reported paths. - Document the convention in AGENTS.md and docs/logging.md. --- AGENTS.md | 9 +- CHDSharpCli/BugReportSink.cs | 13 + CHDSharpCli/Program.cs | 432 ++++++++++++++-------------- CHDSharpCli/UserErrorLogging.cs | 33 +++ CHDSharpTest/CliIntegrationTests.cs | 41 +++ docs/logging.md | 1 + 6 files changed, 311 insertions(+), 218 deletions(-) create mode 100644 CHDSharpCli/UserErrorLogging.cs diff --git a/AGENTS.md b/AGENTS.md index a98b4a7..495c6a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,8 +59,13 @@ jb inspectcode CSharp_CHDSharp.sln --severity=WARNING # ReSharper inspections - The CLI and Tester configure Serilog at startup (`MinimumLevel.Debug`) and bridge the library with `Chd.LoggerFactory = new SerilogLoggerFactory(...)`; the logger is flushed on exit. - CLI sinks: console + rolling-day file (`%LocalAppData%\CHDSharp\logs\chdsharp-.log`) + - `BugReportSink`. The sink POSTs **every Warning+ event** (except user cancellation) to - `https://www.purelogiccode.com/bugreport/api/send-bug-report`. + `BugReportSink`. The sink POSTs **every Warning+ event** (except user cancellation and expected + user/usage errors) to `https://www.purelogiccode.com/bugreport/api/send-bug-report`. +- Expected user/usage errors (bad flags, missing paths, sector-misaligned input, `--force` needed, + format overrides, ...) are diagnostics about the user's input, not defects. Log them with + `logger.UserError(...)` (`CHDSharpCli/UserErrorLogging.cs`), which keeps them at warning level in + the console/file logs but tags them so `BugReportSink` never reports them. Real runtime failures + (`Open failed`, `* failed: {Message}`, verify failures, ...) stay on `logger.Warning(...)`. - Every report must embed: `=== Environment Details ===` (Date, Application Name, Application Version, OS Version, Architecture, Bitness, Windows, Linux or MacOsX Version, Processor Count, Base Directory, Temp Path), `=== Error Details ===` (message), and `=== Exception Details ===` diff --git a/CHDSharpCli/BugReportSink.cs b/CHDSharpCli/BugReportSink.cs index 54186f4..ff82b6e 100644 --- a/CHDSharpCli/BugReportSink.cs +++ b/CHDSharpCli/BugReportSink.cs @@ -20,6 +20,14 @@ internal sealed class BugReportSink : ILogEventSink, IDisposable private static readonly HttpClient Client = new(); private static readonly string ApiKey = DecodeApiKey(); + /// + /// Property attached to log events that describe an expected user or usage error (for example a bad + /// command-line flag, an input file whose size is not sector-aligned, or a missing path). Those events + /// are diagnostics about the user's input, not defects in this application, so they are never forwarded + /// to the Bug Report API. Attach it with logger.UserError(...). + /// + public const string UserErrorProperty = "UserError"; + private readonly EnvironmentSnapshot _env; private readonly string _environmentLabel; @@ -54,6 +62,11 @@ public void Emit(LogEvent logEvent) { if (logEvent.Level < LogEventLevel.Warning) return; + if ( + logEvent.Properties.TryGetValue(UserErrorProperty, out var userError) + && userError is ScalarValue { Value: true } + ) + return; if (DisabledByEnvironment) return; if (logEvent.Exception is OperationCanceledException or TaskCanceledException) diff --git a/CHDSharpCli/Program.cs b/CHDSharpCli/Program.cs index f88f890..c21504f 100644 --- a/CHDSharpCli/Program.cs +++ b/CHDSharpCli/Program.cs @@ -97,35 +97,35 @@ private static int Main(string[] args) switch (command) { case "random" when cmdArgs.Length < 1: - serilogLogger.Warning("random requires a .chd file path"); + serilogLogger.UserError("random requires a .chd file path"); return 1; case "random": RandomAccessTest(ParseInput(cmdArgs, 0)); serilogLogger.Information("Done: Time = {Time}", sw.Elapsed.TotalSeconds); return 0; case "list" when cmdArgs.Length < 1: - serilogLogger.Warning("list requires a text file of .chd paths"); + serilogLogger.UserError("list requires a text file of .chd paths"); return 1; case "list": VerifyList(ParseInput(cmdArgs, 0)); serilogLogger.Information("Done: Time = {Time}", sw.Elapsed.TotalSeconds); return 0; case "parent" when cmdArgs.Length < 2: - serilogLogger.Warning("parent requires "); + serilogLogger.UserError("parent requires "); return 1; case "parent": ParentTest(ParseInput(cmdArgs, 0), ParseInput(cmdArgs, 1)); serilogLogger.Information("Done: Time = {Time}", sw.Elapsed.TotalSeconds); return 0; case "toc" when cmdArgs.Length < 1: - serilogLogger.Warning("toc requires a .chd file path"); + serilogLogger.UserError("toc requires a .chd file path"); return 1; case "toc": TocTest(ParseInput(cmdArgs, 0)); serilogLogger.Information("Done: Time = {Time}", sw.Elapsed.TotalSeconds); return 0; case "cue" when cmdArgs.Length < 1: - serilogLogger.Warning("cue requires a .chd file path"); + serilogLogger.UserError("cue requires a .chd file path"); return 1; case "cue": CueTest( @@ -135,7 +135,7 @@ private static int Main(string[] args) serilogLogger.Information("Done: Time = {Time}", sw.Elapsed.TotalSeconds); return 0; case "classify" when cmdArgs.Length < 1: - serilogLogger.Warning("classify requires a .chd file path"); + serilogLogger.UserError("classify requires a .chd file path"); return 1; case "classify": ClassifyTest(ParseInput(cmdArgs, 0)); @@ -147,7 +147,7 @@ private static int Main(string[] args) if (inp == null || outp == null) { Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning( + serilogLogger.UserError( "createraw requires --input --output (or positional args)" ); PrintCommandHelp("createraw"); @@ -164,7 +164,7 @@ private static int Main(string[] args) if (inp == null || outp == null) { Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning( + serilogLogger.UserError( "createcd requires --input --output (or positional args)" ); PrintCommandHelp("createcd"); @@ -181,7 +181,7 @@ private static int Main(string[] args) if (outp == null) { Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning("createhd requires --output "); + serilogLogger.UserError("createhd requires --output "); PrintCommandHelp("createhd"); return 1; } @@ -196,7 +196,7 @@ private static int Main(string[] args) if (inp == null || outp == null) { Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning( + serilogLogger.UserError( "createdvd requires --input --output (or positional args)" ); PrintCommandHelp("createdvd"); @@ -213,7 +213,7 @@ private static int Main(string[] args) if (inp == null || outp == null) { Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning( + serilogLogger.UserError( "createld requires --input --output (or positional args)" ); PrintCommandHelp("createld"); @@ -230,7 +230,7 @@ private static int Main(string[] args) if (inp == null || outp == null) { Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning( + serilogLogger.UserError( "{Command} requires --input --output ", command ); @@ -248,7 +248,7 @@ private static int Main(string[] args) if (inp == null || outp == null) { Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning("extractcd requires --input --output "); + serilogLogger.UserError("extractcd requires --input --output "); PrintCommandHelp("extractcd"); return 1; } @@ -263,7 +263,7 @@ private static int Main(string[] args) if (inp == null || outp == null) { Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning("extractld requires --input --output "); + serilogLogger.UserError("extractld requires --input --output "); PrintCommandHelp("extractld"); return 1; } @@ -281,7 +281,7 @@ private static int Main(string[] args) if (inp == null || outp == null) { Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning( + serilogLogger.UserError( "copy requires --input --output (or positional args)" ); PrintCommandHelp("copy"); @@ -294,7 +294,7 @@ private static int Main(string[] args) } case "verify" when cmdArgs.Length < 1: Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning("verify requires a .chd file path"); + serilogLogger.UserError("verify requires a .chd file path"); PrintCommandHelp("verify"); return 1; case "verify": @@ -305,7 +305,7 @@ private static int Main(string[] args) return verified ? 0 : 1; case "info" when cmdArgs.Length < 1: Console.Error.WriteLine("Error: Required parameters missing"); - serilogLogger.Warning("info requires a .chd file path"); + serilogLogger.UserError("info requires a .chd file path"); PrintCommandHelp("info"); return 1; case "info": @@ -313,7 +313,7 @@ private static int Main(string[] args) serilogLogger.Information("Done: Time = {Time}", sw.Elapsed.TotalSeconds); return infoOk ? 0 : 1; case "detect" when cmdArgs.Length < 1: - serilogLogger.Warning("detect requires a file path"); + serilogLogger.UserError("detect requires a file path"); return 1; case "detect": DetectTest(ParseInput(cmdArgs, 0)); @@ -332,7 +332,7 @@ private static int Main(string[] args) var (inp, outp, rest) = ParseCreateArgs(cmdArgs); if (inp == null || outp == null) { - serilogLogger.Warning("batch requires --input --output "); + serilogLogger.UserError("batch requires --input --output "); return 1; } @@ -357,7 +357,7 @@ private static int Main(string[] args) var sDir = arg.Replace("\"", ""); if (!Directory.Exists(sDir)) { - serilogLogger.Warning("Directory not found: {Path}", sDir); + serilogLogger.UserError("Directory not found: {Path}", sDir); continue; } @@ -464,7 +464,7 @@ private static void VerifyList(string listFile) } catch (Exception ex) when (ex is IOException or UnauthorizedAccessException) { - log.Warning("Cannot read list file {Path}: {Message}", listFile, ex.Message); + log.UserError("Cannot read list file {Path}: {Message}", listFile, ex.Message); return; } @@ -865,7 +865,7 @@ private static bool CreateRawTest(string inputPath, string outputPath, string[] var log = Log.Logger; if (!File.Exists(inputPath)) { - log.Warning("--create: input file not found: {Path}", inputPath); + log.UserError("--create: input file not found: {Path}", inputPath); return false; } @@ -910,7 +910,7 @@ ref inputLengthFrames { Console.Error.WriteLine($"Error: file already exists ({outputPath})"); Console.Error.WriteLine("Use --force (or -f) to force overwriting"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outputPath ); @@ -921,13 +921,13 @@ ref inputLengthFrames { if (dvd) { - log.Warning("--create: -tp and -d are mutually exclusive"); + log.UserError("--create: -tp and -d are mutually exclusive"); return false; } var tpl = HardDiskTemplates.GetTemplate(templateId.Value); if (unitBytes != 512u) - log.Warning( + log.UserError( " --unitsize/-us overridden by template {Id} (was {Old}, now {New})", templateId.Value, unitBytes, @@ -935,7 +935,7 @@ ref inputLengthFrames ); if (hunkBytes != 4096u) - log.Warning( + log.UserError( " --hunksize/-hs overridden by template {Id} (was {Old}, now {New})", templateId.Value, hunkBytes, @@ -967,7 +967,7 @@ ref inputLengthFrames if (!unitExplicit && parentPath == null) { - log.Warning("createraw: unit size must be specified if no output parent is supplied (--unitsize/-us)"); + log.UserError("createraw: unit size must be specified if no output parent is supplied (--unitsize/-us)"); return false; } @@ -985,7 +985,7 @@ ref inputLengthFrames { Console.Error.WriteLine( $"Error: Specified unit size {unitBytes} bytes does not match output parent CHD unit size {parentHdrRaw.UnitBytes} bytes"); - log.Warning( + log.UserError( "Specified unit size {Unit} bytes does not match output parent CHD unit size {ParentUnit} bytes", unitBytes, parentHdrRaw.UnitBytes); return false; @@ -996,7 +996,7 @@ ref inputLengthFrames { Console.Error.WriteLine( $"Error: Specified hunk size {hunkBytes} bytes does not match output parent CHD hunk size {parentHdrRaw.HunkBytes} bytes"); - log.Warning( + log.UserError( "Specified hunk size {Hunk} bytes does not match output parent CHD hunk size {ParentHunk} bytes", hunkBytes, parentHdrRaw.HunkBytes); return false; @@ -1014,14 +1014,14 @@ ref inputLengthFrames if (hunkBytes < 16) { Console.Error.WriteLine("Error: Invalid hunk size (minimum 16)"); - log.Warning("Invalid hunk size {Hunk} (minimum 16)", hunkBytes); + log.UserError("Invalid hunk size {Hunk} (minimum 16)", hunkBytes); return false; } if (hunkBytes > 1024 * 1024) { Console.Error.WriteLine("Error: Invalid hunk size (maximum 1048576)"); - log.Warning("Invalid hunk size {Hunk} (maximum 1048576)", hunkBytes); + log.UserError("Invalid hunk size {Hunk} (maximum 1048576)", hunkBytes); return false; } @@ -1029,7 +1029,7 @@ ref inputLengthFrames if (hunkBytes % unitBytes != 0) { Console.Error.WriteLine($"Error: Hunk size {hunkBytes} bytes is not a whole multiple of {unitBytes}"); - log.Warning("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkBytes, unitBytes); + log.UserError("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkBytes, unitBytes); return false; } @@ -1087,7 +1087,7 @@ ref inputLengthFrames // --dvd (createdvd parity): force 'DVD ' metadata and a 2048-byte unit size. encodeOptions.Metadata = [MetadataWriter.BuildDvdMetadata()]; if (unitBytes != 2048u && unitBytes != 512u) - log.Warning( + log.UserError( " --unitsize/-us overridden by --dvd (was {Old}, now 2048)", unitBytes ); @@ -1173,7 +1173,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] // chdman.cpp:3468 Expected option, not parameter if (string.IsNullOrEmpty(arg) || arg[0] != '-') { - log.Warning("Error: Expected option, not parameter"); + log.UserError("Error: Expected option, not parameter"); PrintCommandHelp("createhd"); return false; } @@ -1244,7 +1244,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] break; default: // chdman.cpp:3509 Option not valid - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("createhd"); return false; } @@ -1252,7 +1252,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] // chdman.cpp:3502 Multiple parameters of same type if (createHdSeen.Contains(canonical)) { - log.Warning("Error: Multiple parameters of the same type specified"); + log.UserError("Error: Multiple parameters of the same type specified"); PrintCommandHelp("createhd"); return false; } @@ -1261,7 +1261,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { if (i + 1 >= options.Length || (!string.IsNullOrEmpty(options[i + 1]) && options[i + 1][0] == '-')) { - log.Warning("Error: Option is missing parameter"); + log.UserError("Error: Option is missing parameter"); PrintCommandHelp("createhd"); return false; } @@ -1275,7 +1275,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (!TryParseScanSize(param, out var szPlain) || szPlain == 0) { Console.Error.WriteLine("Error: Invalid size specified"); - log.Warning("--createhd: invalid size: {Value}", param); + log.UserError("--createhd: invalid size: {Value}", param); PrintCommandHelp("createhd"); return false; } @@ -1288,7 +1288,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] !uint.TryParse(chsParts[1], out var h) || h == 0 || !uint.TryParse(chsParts[2], out var s) || s == 0) { - log.Warning("createhd: invalid CHS geometry (expected C,H,S): {Value}", param); + log.UserError("createhd: invalid CHS geometry (expected C,H,S): {Value}", param); return false; } @@ -1300,7 +1300,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (!int.TryParse(param, CultureInfo.InvariantCulture, out var tp) || tp < 0 || tp >= HardDiskTemplates.Templates.Length) { - log.Warning("createhd: invalid template ID (0-{Max}): {Value}", + log.UserError("createhd: invalid template ID (0-{Max}): {Value}", HardDiskTemplates.Templates.Length - 1, param); return false; } @@ -1310,7 +1310,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] case "sectorsize": if (!TryParseSizeWithSuffix(param, out uint ss) || ss == 0) { - log.Warning("--createhd: invalid sector size: {Value}", param); + log.UserError("--createhd: invalid sector size: {Value}", param); return false; } @@ -1323,7 +1323,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] case "hunksize": if (!TryParseSizeWithSuffix(param, out uint hs) || hs == 0) { - log.Warning("createhd: invalid hunk size: {Value}", param); + log.UserError("createhd: invalid hunk size: {Value}", param); return false; } @@ -1333,7 +1333,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] case "numprocessors": if (!int.TryParse(param, CultureInfo.InvariantCulture, out var t) || t < 1 || t > 64) { - log.Warning("createhd: invalid task count (1-64): {Value}", param); + log.UserError("createhd: invalid task count (1-64): {Value}", param); return false; } @@ -1348,7 +1348,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] case "inputstartbyte": if (!TryParseSizeWithSuffix(param, out long isb) || isb < 0) { - log.Warning("createhd: invalid input start byte: {Value}", param); + log.UserError("createhd: invalid input start byte: {Value}", param); return false; } @@ -1357,7 +1357,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] case "inputstarthunk": if (!TryParseSizeWithSuffix(param, out long ish) || ish < 0) { - log.Warning("createhd: invalid input start hunk: {Value}", param); + log.UserError("createhd: invalid input start hunk: {Value}", param); return false; } @@ -1366,7 +1366,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] case "inputbytes": if (!TryParseSizeWithSuffix(param, out long ib) || ib <= 0) { - log.Warning("createhd: invalid input bytes: {Value}", param); + log.UserError("createhd: invalid input bytes: {Value}", param); return false; } @@ -1375,7 +1375,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] case "inputhunks": if (!TryParseSizeWithSuffix(param, out long ih) || ih <= 0) { - log.Warning("createhd: invalid input hunks: {Value}", param); + log.UserError("createhd: invalid input hunks: {Value}", param); return false; } @@ -1398,14 +1398,14 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] // chdman.cpp:1216 Start offset cannot be specified in both bytes and hunks, etc. if (inputStartBytes.HasValue && inputStartHunk.HasValue) { - log.Warning("Error: Start offset cannot be specified in both bytes and hunks"); + log.UserError("Error: Start offset cannot be specified in both bytes and hunks"); PrintCommandHelp("createhd"); return false; } if (inputLengthBytes.HasValue && inputLengthHunks.HasValue) { - log.Warning("Error: Length cannot be specified in both bytes and hunks"); + log.UserError("Error: Length cannot be specified in both bytes and hunks"); PrintCommandHelp("createhd"); return false; } @@ -1414,7 +1414,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine($"Error: file already exists ({outputPath})"); Console.Error.WriteLine("Use --force (or -f) to force overwriting"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outputPath ); @@ -1430,7 +1430,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine( "Error: CHS geometry cannot be specified separately when a template is specified"); - log.Warning("CHS geometry cannot be specified separately when a template is specified"); + log.UserError("CHS geometry cannot be specified separately when a template is specified"); PrintCommandHelp("createhd"); return false; } @@ -1439,7 +1439,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine( "Error: Sector size cannot be specified separately when a template is specified"); - log.Warning("Sector size cannot be specified separately when a template is specified"); + log.UserError("Sector size cannot be specified separately when a template is specified"); PrintCommandHelp("createhd"); return false; } @@ -1449,7 +1449,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine( $"Error: Data size {BigintString(sizeBytes.Value)} is not divisible by sector size {tpl.SectorSize}"); - log.Warning("Data size {Size} is not divisible by sector size {Sector}", sizeBytes.Value, + log.UserError("Data size {Size} is not divisible by sector size {Sector}", sizeBytes.Value, tpl.SectorSize); return false; } @@ -1481,7 +1481,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (templateId.HasValue) { Console.Error.WriteLine("Error: Template cannot be used when a parent CHD is supplied"); - log.Warning("Template cannot be used when a parent CHD is supplied"); + log.UserError("Template cannot be used when a parent CHD is supplied"); PrintCommandHelp("createhd"); return false; } @@ -1489,7 +1489,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (chsCylinders.HasValue) { Console.Error.WriteLine("Error: CHS geometry cannot be specified when a parent CHD is supplied"); - log.Warning("CHS geometry cannot be specified when a parent CHD is supplied"); + log.UserError("CHS geometry cannot be specified when a parent CHD is supplied"); PrintCommandHelp("createhd"); return false; } @@ -1501,7 +1501,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine( $"Error: Sector size {unitBytes} bytes does not match output parent CHD sector size {parentHdrHd.UnitBytes} bytes"); - log.Warning( + log.UserError( "Sector size {Sector} bytes does not match output parent CHD sector size {ParentSector} bytes", unitBytes, parentHdrHd.UnitBytes); return false; @@ -1515,7 +1515,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine( $"Error: Specified hunk size {hunkBytes} bytes does not match output parent CHD hunk size {parentHdrHd.HunkBytes} bytes"); - log.Warning( + log.UserError( "Specified hunk size {Hunk} bytes does not match output parent CHD hunk size {ParentHunk} bytes", hunkBytes, parentHdrHd.HunkBytes); return false; @@ -1532,28 +1532,28 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (hunkBytes < 16) { Console.Error.WriteLine("Error: Invalid hunk size (minimum 16)"); - log.Warning("Invalid hunk size {Hunk} (minimum 16)", hunkBytes); + log.UserError("Invalid hunk size {Hunk} (minimum 16)", hunkBytes); return false; } if (hunkBytes > 1024 * 1024) { Console.Error.WriteLine("Error: Invalid hunk size (maximum 1048576)"); - log.Warning("Invalid hunk size {Hunk} (maximum 1048576)", hunkBytes); + log.UserError("Invalid hunk size {Hunk} (maximum 1048576)", hunkBytes); return false; } if (hunkBytes % unitBytes != 0) { Console.Error.WriteLine($"Error: Hunk size {hunkBytes} bytes is not a whole multiple of {unitBytes}"); - log.Warning("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkBytes, unitBytes); + log.UserError("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkBytes, unitBytes); return false; } // Validate required options — allow blank child with parent to inherit geometry (chdman defers check until after parent GDDD fallback) if (!sizeBytes.HasValue && !chsCylinders.HasValue && inputPath == null && outputParentPath == null) { - log.Warning("createhd: requires --size N, -chs C,H,S, -tp ID, or --input "); + log.UserError("createhd: requires --size N, -chs C,H,S, -tp ID, or --input "); return false; } @@ -1562,14 +1562,14 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { if (!File.Exists(inputPath)) { - log.Warning("createhd: input file not found: {Path}", inputPath); + log.UserError("createhd: input file not found: {Path}", inputPath); return false; } if (sizeBytes.HasValue) { // chdman.cpp:2027 — "Size cannot be specified when an input file is supplied" - log.Warning("createhd: --input cannot be combined with --size"); + log.UserError("createhd: --input cannot be combined with --size"); return false; } @@ -1647,7 +1647,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (!File.Exists(identPath)) { Console.Error.WriteLine($"Error: Ident file '{identPath}' not found"); - log.Warning("createhd: ident file not found: {Path}", identPath); + log.UserError("createhd: ident file not found: {Path}", identPath); return false; } @@ -1657,7 +1657,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (fileData.Length < 14) { Console.Error.WriteLine($"Error: Ident file '{identPath}' is invalid (too short)"); - log.Warning("Ident file '{Path}' is invalid (too short)", identPath); + log.UserError("Ident file '{Path}' is invalid (too short)", identPath); return false; } @@ -1726,7 +1726,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine( $"Error: Data size {BigintString(inputFilesize)} is not divisible by sector size {finalSectorSize}"); - log.Warning("Data size {Size} is not divisible by sector size {Sector}", inputFilesize, + log.UserError("Data size {Size} is not divisible by sector size {Sector}", inputFilesize, finalSectorSize); return false; } @@ -1741,7 +1741,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (inputFilesize == 0) { Console.Error.WriteLine("Error: Can't guess CHS values because there is no input file"); - log.Warning("Can't guess CHS values because there is no input file"); + log.UserError("Can't guess CHS values because there is no input file"); return false; } @@ -1834,7 +1834,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] } catch (OverflowException) { - log.Warning( + log.UserError( "createhd: CHS geometry produces a size exceeding ulong.MaxValue; reduce C/H/S values" ); return false; @@ -1842,7 +1842,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (chsSize == 0) { - log.Warning("createhd: CHS geometry produces zero-byte image"); + log.UserError("createhd: CHS geometry produces zero-byte image"); return false; } @@ -1854,7 +1854,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] // for a blank image (no input file) — not the parsed --size value. Console.Error.WriteLine( $"Error: Data size {BigintString(0)} is not divisible by sector size {unitBytes}"); - log.Warning("Data size {Size} is not divisible by sector size {Sector}", 0, + log.UserError("Data size {Size} is not divisible by sector size {Sector}", 0, unitBytes); return false; } @@ -1874,7 +1874,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] // chdman prints big_int_string(input_end - input_start) = 0 for a blank image Console.Error.WriteLine( $"Error: Data size {BigintString(0)} is not divisible by sector size {unitBytes}"); - log.Warning("Data size {Size} is not divisible by sector size {Sector}", 0, unitBytes); + log.UserError("Data size {Size} is not divisible by sector size {Sector}", 0, unitBytes); return false; } @@ -1882,7 +1882,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (codecs != null && !string.Equals(codecs, "none", StringComparison.OrdinalIgnoreCase)) { Console.Error.WriteLine("Error: Blank hard disk images must be uncompressed"); - log.Warning("Blank hard disk images must be uncompressed"); + log.UserError("Blank hard disk images must be uncompressed"); return false; } @@ -1921,7 +1921,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (!File.Exists(identPath)) { Console.Error.WriteLine($"Error: Ident file '{identPath}' not found"); - log.Warning("--createhd: ident file not found: {Path}", identPath); + log.UserError("--createhd: ident file not found: {Path}", identPath); return false; } @@ -1931,7 +1931,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (fileData.Length < 14) { Console.Error.WriteLine($"Error: Ident file '{identPath}' is invalid (too short)"); - log.Warning("--createhd: ident file is invalid (too short, need >=14 bytes, got {Size})", + log.UserError("--createhd: ident file is invalid (too short, need >=14 bytes, got {Size})", fileData.Length); return false; } @@ -1985,7 +1985,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine( $"Error: Hunk size {hunkBytes} bytes is not a whole multiple of {unitBytes}"); - log.Warning("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkBytes, unitBytes); + log.UserError("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkBytes, unitBytes); return false; } } @@ -1998,7 +1998,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine( $"Error: Data size {BigintString(0)} is not divisible by sector size {unitBytes}"); - log.Warning("Data size {Size} is not divisible by sector size {Sector}", 0, unitBytes); + log.UserError("Data size {Size} is not divisible by sector size {Sector}", 0, unitBytes); return false; } @@ -2009,7 +2009,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] { Console.Error.WriteLine( "Error: Length or CHS geometry must be specified when creating a blank hard disk image"); - log.Warning("Length or CHS geometry must be specified when creating a blank hard disk image"); + log.UserError("Length or CHS geometry must be specified when creating a blank hard disk image"); return false; } @@ -2019,7 +2019,7 @@ private static bool CreateHdTest(string? inputPath, string outputPath, string[] if (blankFilesize == 0) { Console.Error.WriteLine("Error: Can't guess CHS values because there is no input file"); - log.Warning("Can't guess CHS values because there is no input file"); + log.UserError("Can't guess CHS values because there is no input file"); return false; } } @@ -2123,7 +2123,7 @@ private static bool CreateCdTest(string inputPath, string outputPath, string[] o var log = Log.Logger; if (!File.Exists(inputPath)) { - log.Warning("--createcd: input file not found: {Path}", inputPath); + log.UserError("--createcd: input file not found: {Path}", inputPath); return false; } @@ -2169,7 +2169,7 @@ ref inputLengthFrames { Console.Error.WriteLine($"Error: file already exists ({outputPath})"); Console.Error.WriteLine("Use --force (or -f) to force overwriting"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outputPath ); @@ -2188,7 +2188,7 @@ ref inputLengthFrames { Console.Error.WriteLine( $"Error: Output parent CHD sector size {parentHdrCd.UnitBytes} bytes does not match CD-ROM frame size {CdConstants.FrameSize} bytes"); - log.Warning( + log.UserError( "Output parent CHD sector size {ParentUnit} bytes does not match CD-ROM frame size {FrameSize} bytes", parentHdrCd.UnitBytes, CdConstants.FrameSize); return false; @@ -2198,7 +2198,7 @@ ref inputLengthFrames { Console.Error.WriteLine( $"Error: Specified hunk size {hunkSize} bytes does not match output parent CHD hunk size {parentHdrCd.HunkBytes} bytes"); - log.Warning( + log.UserError( "Specified hunk size {Hunk} bytes does not match output parent CHD hunk size {ParentHunk} bytes", hunkSize, parentHdrCd.HunkBytes); return false; @@ -2210,14 +2210,14 @@ ref inputLengthFrames if (hunkSize < 16) { Console.Error.WriteLine("Error: Invalid hunk size (minimum 16)"); - log.Warning("Invalid hunk size {Hunk} (minimum 16)", hunkSize); + log.UserError("Invalid hunk size {Hunk} (minimum 16)", hunkSize); return false; } if (hunkSize > 1024 * 1024) { Console.Error.WriteLine("Error: Invalid hunk size (maximum 1048576)"); - log.Warning("Invalid hunk size {Hunk} (maximum 1048576)", hunkSize); + log.UserError("Invalid hunk size {Hunk} (maximum 1048576)", hunkSize); return false; } @@ -2225,7 +2225,7 @@ ref inputLengthFrames { Console.Error.WriteLine( $"Error: Hunk size {hunkSize} bytes is not a whole multiple of {CdConstants.FrameSize}"); - log.Warning("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkSize, CdConstants.FrameSize); + log.UserError("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkSize, CdConstants.FrameSize); return false; } @@ -2306,7 +2306,7 @@ private static bool CreateLdTest(string inputPath, string outputPath, string[] o var log = Log.Logger; if (!File.Exists(inputPath)) { - log.Warning("--createld: input file not found: {Path}", inputPath); + log.UserError("--createld: input file not found: {Path}", inputPath); return false; } @@ -2324,7 +2324,7 @@ private static bool CreateLdTest(string inputPath, string outputPath, string[] o var arg = options[i]; if (string.IsNullOrEmpty(arg) || arg[0] != '-') { - log.Warning("Error: Expected option, not parameter"); + log.UserError("Error: Expected option, not parameter"); PrintCommandHelp("createld"); return false; } @@ -2366,14 +2366,14 @@ private static bool CreateLdTest(string inputPath, string outputPath, string[] o hasParam = false; break; default: - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("createld"); return false; } if (createldSeen.Contains(canonical)) { - log.Warning("Error: Multiple parameters of the same type specified"); + log.UserError("Error: Multiple parameters of the same type specified"); PrintCommandHelp("createld"); return false; } @@ -2382,7 +2382,7 @@ private static bool CreateLdTest(string inputPath, string outputPath, string[] o { if (i + 1 >= options.Length || (!string.IsNullOrEmpty(options[i + 1]) && options[i + 1][0] == '-')) { - log.Warning("Error: Option is missing parameter"); + log.UserError("Error: Option is missing parameter"); PrintCommandHelp("createld"); return false; } @@ -2394,7 +2394,7 @@ private static bool CreateLdTest(string inputPath, string outputPath, string[] o case "hunksize": if (!TryParseSizeWithSuffix(param, out uint hs) || hs == 0) { - log.Warning("Invalid hunk size: {Value}", param); + log.UserError("Invalid hunk size: {Value}", param); return false; } @@ -2403,7 +2403,7 @@ private static bool CreateLdTest(string inputPath, string outputPath, string[] o case "inputstartframe": if (!TryParseSizeWithSuffix(param, out long isf) || isf < 0) { - log.Warning("Invalid input start frame: {Value}", param); + log.UserError("Invalid input start frame: {Value}", param); return false; } @@ -2412,7 +2412,7 @@ private static bool CreateLdTest(string inputPath, string outputPath, string[] o case "inputframes": if (!TryParseSizeWithSuffix(param, out long ifr) || ifr < 1) { - log.Warning("Invalid input frame count: {Value}", param); + log.UserError("Invalid input frame count: {Value}", param); return false; } @@ -2422,7 +2422,7 @@ private static bool CreateLdTest(string inputPath, string outputPath, string[] o case "numprocessors": if (!int.TryParse(param, CultureInfo.InvariantCulture, out var t) || t < 1 || t > 64) { - log.Warning("Invalid task count (1-64): {Value}", param); + log.UserError("Invalid task count (1-64): {Value}", param); return false; } @@ -2446,7 +2446,7 @@ private static bool CreateLdTest(string inputPath, string outputPath, string[] o { Console.Error.WriteLine($"Error: file already exists ({outputPath})"); Console.Error.WriteLine("Use --force (or -f) to force overwriting"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outputPath ); @@ -2538,7 +2538,7 @@ private static bool ExtractLdTest(string inputPath, string outputPath, string[] var log = Log.Logger; if (!File.Exists(inputPath)) { - log.Warning("extractld: input file not found: {Path}", inputPath); + log.UserError("extractld: input file not found: {Path}", inputPath); return false; } @@ -2552,7 +2552,7 @@ private static bool ExtractLdTest(string inputPath, string outputPath, string[] var arg = options[i]; if (string.IsNullOrEmpty(arg) || arg[0] != '-') { - log.Warning("Error: Expected option, not parameter"); + log.UserError("Error: Expected option, not parameter"); PrintCommandHelp("extractld"); return false; } @@ -2578,14 +2578,14 @@ private static bool ExtractLdTest(string inputPath, string outputPath, string[] hasParam = false; break; default: - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("extractld"); return false; } if (extractLdSeen.Contains(canonical)) { - log.Warning("Error: Multiple parameters of the same type specified"); + log.UserError("Error: Multiple parameters of the same type specified"); PrintCommandHelp("extractld"); return false; } @@ -2594,7 +2594,7 @@ private static bool ExtractLdTest(string inputPath, string outputPath, string[] { if (i + 1 >= options.Length || (!string.IsNullOrEmpty(options[i + 1]) && options[i + 1][0] == '-')) { - log.Warning("Error: Option is missing parameter"); + log.UserError("Error: Option is missing parameter"); PrintCommandHelp("extractld"); return false; } @@ -2605,7 +2605,7 @@ private static bool ExtractLdTest(string inputPath, string outputPath, string[] case "inputstartframe": if (!TryParseSizeWithSuffix(param, out long sf) || sf < 0) { - log.Warning("Invalid input start frame: {Value}", param); + log.UserError("Invalid input start frame: {Value}", param); return false; } @@ -2614,7 +2614,7 @@ private static bool ExtractLdTest(string inputPath, string outputPath, string[] case "inputframes": if (!TryParseSizeWithSuffix(param, out long ifr) || ifr <= 0) { - log.Warning("Invalid input frames: {Value}", param); + log.UserError("Invalid input frames: {Value}", param); return false; } @@ -2637,7 +2637,7 @@ private static bool ExtractLdTest(string inputPath, string outputPath, string[] { Console.Error.WriteLine($"Error: file already exists ({outputPath})"); Console.Error.WriteLine("Use --force (or -f) to force overwriting"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outputPath ); @@ -2822,14 +2822,14 @@ ref long? inputLengthFrames var arg = options[i]; if (string.IsNullOrEmpty(arg) || arg[0] != '-') { - Log.Logger.Warning("Error: Expected option, not parameter"); + Log.Logger.UserError("Error: Expected option, not parameter"); PrintCommandHelp(command); return false; } if (!optionDefs.TryGetValue(arg, out var def)) { - Log.Logger.Warning("Error: Option '{Option}' not valid for this command", arg); + Log.Logger.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp(command); return false; } @@ -2837,14 +2837,14 @@ ref long? inputLengthFrames var canonical = def.canonical; if (!valid.Contains(canonical)) { - Log.Logger.Warning("Error: Option '{Option}' not valid for this command", arg); + Log.Logger.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp(command); return false; } if (seen.Contains(canonical)) { - Log.Logger.Warning("Error: Multiple parameters of the same type specified"); + Log.Logger.UserError("Error: Multiple parameters of the same type specified"); PrintCommandHelp(command); return false; } @@ -2853,7 +2853,7 @@ ref long? inputLengthFrames { if (i + 1 >= options.Length || (!string.IsNullOrEmpty(options[i + 1]) && options[i + 1][0] == '-')) { - Log.Logger.Warning("Error: Option is missing parameter"); + Log.Logger.UserError("Error: Option is missing parameter"); PrintCommandHelp(command); return false; } @@ -2871,7 +2871,7 @@ ref long? inputLengthFrames case "hunksize": if (!TryParseSizeWithSuffix(param, out uint hs) || hs == 0) { - Log.Logger.Warning("Invalid hunk size: {Value}", param); + Log.Logger.UserError("Invalid hunk size: {Value}", param); return false; } @@ -2880,7 +2880,7 @@ ref long? inputLengthFrames case "unitsize": if (!TryParseSizeWithSuffix(param, out uint us) || us == 0) { - Log.Logger.Warning("Invalid unit size: {Value}", param); + Log.Logger.UserError("Invalid unit size: {Value}", param); return false; } @@ -2889,7 +2889,7 @@ ref long? inputLengthFrames case "numprocessors": if (!int.TryParse(param, CultureInfo.InvariantCulture, out var t) || t < 1 || t > 64) { - Log.Logger.Warning("Invalid task count (1-64): {Value}", param); + Log.Logger.UserError("Invalid task count (1-64): {Value}", param); return false; } @@ -2899,7 +2899,7 @@ ref long? inputLengthFrames if (!int.TryParse(param, CultureInfo.InvariantCulture, out var tp) || tp < 0 || tp >= HardDiskTemplates.Templates.Length) { - Log.Logger.Warning("Invalid template ID (0-{Max}): {Value}", + Log.Logger.UserError("Invalid template ID (0-{Max}): {Value}", HardDiskTemplates.Templates.Length - 1, param); return false; } @@ -2909,7 +2909,7 @@ ref long? inputLengthFrames case "inputstartbyte": if (!TryParseSizeWithSuffix(param, out long isb) || isb < 0) { - Log.Logger.Warning("Invalid input start byte: {Value}", param); + Log.Logger.UserError("Invalid input start byte: {Value}", param); return false; } @@ -2918,7 +2918,7 @@ ref long? inputLengthFrames case "inputstarthunk": if (!TryParseSizeWithSuffix(param, out long ish) || ish < 0) { - Log.Logger.Warning("Invalid input start hunk: {Value}", param); + Log.Logger.UserError("Invalid input start hunk: {Value}", param); return false; } @@ -2927,7 +2927,7 @@ ref long? inputLengthFrames case "inputstartframe": if (!TryParseSizeWithSuffix(param, out long isf) || isf < 0) { - Log.Logger.Warning("Invalid input start frame: {Value}", param); + Log.Logger.UserError("Invalid input start frame: {Value}", param); return false; } @@ -2936,7 +2936,7 @@ ref long? inputLengthFrames case "inputbytes": if (!TryParseSizeWithSuffix(param, out long ib) || ib <= 0) { - Log.Logger.Warning("Invalid input bytes: {Value}", param); + Log.Logger.UserError("Invalid input bytes: {Value}", param); return false; } @@ -2945,7 +2945,7 @@ ref long? inputLengthFrames case "inputhunks": if (!TryParseSizeWithSuffix(param, out long ih) || ih <= 0) { - Log.Logger.Warning("Invalid input hunks: {Value}", param); + Log.Logger.UserError("Invalid input hunks: {Value}", param); return false; } @@ -2954,14 +2954,14 @@ ref long? inputLengthFrames case "inputframes": if (!TryParseSizeWithSuffix(param, out long ifr) || ifr <= 0) { - Log.Logger.Warning("Invalid input frames: {Value}", param); + Log.Logger.UserError("Invalid input frames: {Value}", param); return false; } inputLengthFrames = ifr; break; default: - Log.Logger.Warning("Error: Option '{Option}' not valid for this command", arg); + Log.Logger.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp(command); return false; } @@ -2982,7 +2982,7 @@ ref long? inputLengthFrames verbose = true; break; default: - Log.Logger.Warning("Error: Option '{Option}' not valid for this command", arg); + Log.Logger.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp(command); return false; } @@ -2993,42 +2993,42 @@ ref long? inputLengthFrames if (inputStartBytes.HasValue && inputStartHunk.HasValue) { - Log.Logger.Warning("Error: Start offset cannot be specified in both bytes and hunks"); + Log.Logger.UserError("Error: Start offset cannot be specified in both bytes and hunks"); PrintCommandHelp(command); return false; } if (inputStartBytes.HasValue && inputStartFrame.HasValue) { - Log.Logger.Warning("Error: Start offset cannot be specified in both bytes and frames"); + Log.Logger.UserError("Error: Start offset cannot be specified in both bytes and frames"); PrintCommandHelp(command); return false; } if (inputStartHunk.HasValue && inputStartFrame.HasValue) { - Log.Logger.Warning("Error: Start offset cannot be specified in both hunks and frames"); + Log.Logger.UserError("Error: Start offset cannot be specified in both hunks and frames"); PrintCommandHelp(command); return false; } if (inputLengthBytes.HasValue && inputLengthHunks.HasValue) { - Log.Logger.Warning("Error: Length cannot be specified in both bytes and hunks"); + Log.Logger.UserError("Error: Length cannot be specified in both bytes and hunks"); PrintCommandHelp(command); return false; } if (inputLengthBytes.HasValue && inputLengthFrames.HasValue) { - Log.Logger.Warning("Error: Length cannot be specified in both bytes and frames"); + Log.Logger.UserError("Error: Length cannot be specified in both bytes and frames"); PrintCommandHelp(command); return false; } if (inputLengthHunks.HasValue && inputLengthFrames.HasValue) { - Log.Logger.Warning("Error: Length cannot be specified in both hunks and frames"); + Log.Logger.UserError("Error: Length cannot be specified in both hunks and frames"); PrintCommandHelp(command); return false; } @@ -3096,7 +3096,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio var log = Log.Logger; if (!File.Exists(inputPath)) { - log.Warning("--copy: input file not found: {Path}", inputPath); + log.UserError("--copy: input file not found: {Path}", inputPath); return false; } @@ -3118,7 +3118,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio var arg = options[i]; if (string.IsNullOrEmpty(arg) || arg[0] != '-') { - log.Warning("Error: Expected option, not parameter"); + log.UserError("Error: Expected option, not parameter"); PrintCommandHelp("copy"); return false; } @@ -3176,14 +3176,14 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio hasParam = false; break; default: - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("copy"); return false; } if (copySeen.Contains(canonical)) { - log.Warning("Error: Multiple parameters of the same type specified"); + log.UserError("Error: Multiple parameters of the same type specified"); PrintCommandHelp("copy"); return false; } @@ -3192,7 +3192,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio { if (i + 1 >= options.Length || (!string.IsNullOrEmpty(options[i + 1]) && options[i + 1][0] == '-')) { - log.Warning("Error: Option is missing parameter"); + log.UserError("Error: Option is missing parameter"); PrintCommandHelp("copy"); return false; } @@ -3206,7 +3206,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio case "numprocessors": if (!int.TryParse(param, CultureInfo.InvariantCulture, out var t) || t < 1 || t > 64) { - log.Warning("Invalid task count (1-64): {Value}", param); + log.UserError("Invalid task count (1-64): {Value}", param); return false; } @@ -3215,7 +3215,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio case "hunksize": if (!TryParseSizeWithSuffix(param, out uint hs) || hs == 0) { - log.Warning("Invalid hunk size: {Value}", param); + log.UserError("Invalid hunk size: {Value}", param); return false; } @@ -3224,7 +3224,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio case "inputstartbyte": if (!TryParseSizeWithSuffix(param, out long isb) || isb < 0) { - log.Warning("Invalid input start byte: {Value}", param); + log.UserError("Invalid input start byte: {Value}", param); return false; } @@ -3233,7 +3233,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio case "inputstarthunk": if (!TryParseSizeWithSuffix(param, out long ish) || ish < 0) { - log.Warning("Invalid input start hunk: {Value}", param); + log.UserError("Invalid input start hunk: {Value}", param); return false; } @@ -3242,7 +3242,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio case "inputbytes": if (!TryParseSizeWithSuffix(param, out long ib) || ib <= 0) { - log.Warning("Invalid input bytes: {Value}", param); + log.UserError("Invalid input bytes: {Value}", param); return false; } @@ -3251,7 +3251,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio case "inputhunks": if (!TryParseSizeWithSuffix(param, out long ih) || ih <= 0) { - log.Warning("Invalid input hunks: {Value}", param); + log.UserError("Invalid input hunks: {Value}", param); return false; } @@ -3274,14 +3274,14 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio if (inputStartBytes.HasValue && inputStartHunk.HasValue) { - log.Warning("Error: Start offset cannot be specified in both bytes and hunks"); + log.UserError("Error: Start offset cannot be specified in both bytes and hunks"); PrintCommandHelp("copy"); return false; } if (inputLengthBytes.HasValue && inputLengthHunks.HasValue) { - log.Warning("Error: Length cannot be specified in both bytes and hunks"); + log.UserError("Error: Length cannot be specified in both bytes and hunks"); PrintCommandHelp("copy"); return false; } @@ -3290,7 +3290,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio { Console.Error.WriteLine($"Error: file already exists ({outputPath})"); Console.Error.WriteLine("Use --force (or -f) to force overwriting"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outputPath ); @@ -3382,7 +3382,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio { Console.Error.WriteLine( $"Error: Specified hunk size {effectiveHunk} bytes does not match output parent CHD hunk size {outParentHdr.HunkBytes} bytes"); - log.Warning( + log.UserError( "Specified hunk size {Hunk} bytes does not match output parent CHD hunk size {ParentHunk} bytes", effectiveHunk, outParentHdr.HunkBytes); return false; @@ -3400,14 +3400,14 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio if (effectiveHunk < 16) { Console.Error.WriteLine("Error: Invalid hunk size (minimum 16)"); - log.Warning("Invalid hunk size {Hunk} (minimum 16)", effectiveHunk); + log.UserError("Invalid hunk size {Hunk} (minimum 16)", effectiveHunk); return false; } if (effectiveHunk > 1024 * 1024) { Console.Error.WriteLine("Error: Invalid hunk size (maximum 1048576)"); - log.Warning("Invalid hunk size {Hunk} (maximum 1048576)", effectiveHunk); + log.UserError("Invalid hunk size {Hunk} (maximum 1048576)", effectiveHunk); return false; } @@ -3415,7 +3415,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio { Console.Error.WriteLine( $"Error: Hunk size {effectiveHunk} bytes is not a whole multiple of {sourceUnitBytesForCopy}"); - log.Warning("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", effectiveHunk, + log.UserError("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", effectiveHunk, sourceUnitBytesForCopy); return false; } @@ -3425,7 +3425,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio { Console.Error.WriteLine( $"Error: Output parent CHD unit size {outParentHdr.UnitBytes} bytes does not match source unit size {sourceUnitBytesForCopy} bytes"); - log.Warning( + log.UserError( "Output parent CHD unit size {ParentUnit} bytes does not match source unit size {Unit} bytes", outParentHdr.UnitBytes, sourceUnitBytesForCopy); return false; @@ -3434,7 +3434,7 @@ private static bool CopyTest(string inputPath, string outputPath, string[] optio if (effectiveHunk % sourceHunkBytes != 0 && sourceHunkBytes % effectiveHunk != 0) { Console.Error.WriteLine("Error: Hunk size is not a whole multiple or factor of input hunk size"); - log.Warning("Hunk size is not a whole multiple or factor of input hunk size"); + log.UserError("Hunk size is not a whole multiple or factor of input hunk size"); return false; } } @@ -3524,7 +3524,7 @@ private static void DetectTest(string file) var log = Log.Logger; if (!File.Exists(file)) { - log.Warning("--detect: file not found: {Path}", file); + log.UserError("--detect: file not found: {Path}", file); return; } @@ -3580,7 +3580,7 @@ private static bool VerifyTest(string[] args) if (!verifyDefs.TryGetValue(arg, out var def)) { Console.Error.WriteLine($"Error: Option '{arg}' not valid for this command"); - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("verify"); return false; } @@ -3588,7 +3588,7 @@ private static bool VerifyTest(string[] args) if (!verifyValid.Contains(def.canonical)) { Console.Error.WriteLine($"Error: Option '{arg}' not valid for this command"); - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("verify"); return false; } @@ -3596,7 +3596,7 @@ private static bool VerifyTest(string[] args) if (seenVerify.Contains(def.canonical)) { Console.Error.WriteLine("Error: Multiple parameters of the same type specified"); - log.Warning("Error: Multiple parameters of the same type specified"); + log.UserError("Error: Multiple parameters of the same type specified"); PrintCommandHelp("verify"); return false; } @@ -3606,7 +3606,7 @@ private static bool VerifyTest(string[] args) if (i + 1 >= args.Length || (!string.IsNullOrEmpty(args[i + 1]) && args[i + 1][0] == '-')) { Console.Error.WriteLine("Error: Option is missing parameter"); - log.Warning("Error: Option is missing parameter"); + log.UserError("Error: Option is missing parameter"); PrintCommandHelp("verify"); return false; } @@ -3685,7 +3685,7 @@ private static bool InfoTest(string[] args) if (!infoDefs.TryGetValue(arg, out var def)) { Console.Error.WriteLine($"Error: Option '{arg}' not valid for this command"); - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("info"); return false; } @@ -3693,7 +3693,7 @@ private static bool InfoTest(string[] args) if (!infoValid.Contains(def.canonical)) { Console.Error.WriteLine($"Error: Option '{arg}' not valid for this command"); - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("info"); return false; } @@ -3701,7 +3701,7 @@ private static bool InfoTest(string[] args) if (seenInfo.Contains(def.canonical)) { Console.Error.WriteLine("Error: Multiple parameters of the same type specified"); - log.Warning("Error: Multiple parameters of the same type specified"); + log.UserError("Error: Multiple parameters of the same type specified"); PrintCommandHelp("info"); return false; } @@ -3711,7 +3711,7 @@ private static bool InfoTest(string[] args) if (i + 1 >= args.Length || (!string.IsNullOrEmpty(args[i + 1]) && args[i + 1][0] == '-')) { Console.Error.WriteLine("Error: Option is missing parameter"); - log.Warning("Error: Option is missing parameter"); + log.UserError("Error: Option is missing parameter"); PrintCommandHelp("info"); return false; } @@ -4107,7 +4107,7 @@ private static void DumpMetaTest(string[] args) case "--index" or "-ix" when i + 1 < args.Length: if (!uint.TryParse(args[++i], out index)) { - log.Warning("Invalid metadata index: {Value}", args[i]); + log.UserError("Invalid metadata index: {Value}", args[i]); return; } @@ -4125,13 +4125,13 @@ private static void DumpMetaTest(string[] args) break; } - log.Warning("Unknown option: {Option}", args[i]); + log.UserError("Unknown option: {Option}", args[i]); return; } if (file == null) { - log.Warning("dumpmeta requires --input "); + log.UserError("dumpmeta requires --input "); return; } @@ -4161,7 +4161,7 @@ private static void DumpMetaTest(string[] args) { if (File.Exists(outFile) && !force) { - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outFile ); @@ -4225,7 +4225,7 @@ var name in args[++i] types |= ChdHashType.Xxh3; break; default: - log.Warning( + log.UserError( "Unknown hash type: {Name} (valid: sha1, sha256, crc32, xxh3)", name ); @@ -4239,7 +4239,7 @@ var name in args[++i] format = args[++i].ToLowerInvariant(); if (format is not ("text" or "json" or "sfv")) { - log.Warning("Invalid result format [{Format}] (text|json|sfv)", format); + log.UserError("Invalid result format [{Format}] (text|json|sfv)", format); return; } @@ -4254,13 +4254,13 @@ var name in args[++i] break; } - log.Warning("Unknown option: {Option}", args[i]); + log.UserError("Unknown option: {Option}", args[i]); return; } if (file == null) { - log.Warning("hash requires --input "); + log.UserError("hash requires --input "); return; } @@ -4316,7 +4316,7 @@ var name in args[++i] if (r.Crc32 is { } crc) log.Information("{Name} {Crc:X8}", name, crc); else - log.Warning("sfv output requires crc32; use --hashes crc32"); + log.UserError("sfv output requires crc32; use --hashes crc32"); } break; @@ -4357,7 +4357,7 @@ private static void BatchTest(string inputDir, string outputDir, string[] option action = options[++i].ToLowerInvariant(); if (action is not ("extract" or "create")) { - log.Warning("Invalid action [{Action}] (extract|create)", action); + log.UserError("Invalid action [{Action}] (extract|create)", action); return; } @@ -4366,13 +4366,13 @@ private static void BatchTest(string inputDir, string outputDir, string[] option codecs = options[++i]; break; default: - log.Warning("Unknown option: {Option}", options[i]); + log.UserError("Unknown option: {Option}", options[i]); return; } if (!Directory.Exists(inputDir)) { - log.Warning("Input directory not found: {Path}", inputDir); + log.UserError("Input directory not found: {Path}", inputDir); return; } @@ -4391,7 +4391,7 @@ private static void BatchTest(string inputDir, string outputDir, string[] option if (files.Count == 0) { - log.Warning( + log.UserError( "No {Files} found in {Path}", string.Equals(action, "extract", StringComparison.Ordinal) ? ".chd files" @@ -4516,7 +4516,7 @@ private static void AddMetaTest(string[] args) case "--index" or "-ix" when i + 1 < args.Length: if (!uint.TryParse(args[++i], out index)) { - log.Warning("Invalid metadata index: {Value}", args[i]); + log.UserError("Invalid metadata index: {Value}", args[i]); return; } @@ -4531,25 +4531,25 @@ private static void AddMetaTest(string[] args) break; } - log.Warning("Unknown option: {Option}", args[i]); + log.UserError("Unknown option: {Option}", args[i]); return; } if (file == null) { - log.Warning("addmeta requires --input "); + log.UserError("addmeta requires --input "); return; } if (tag is not { Length: 4 }) { - log.Warning("addmeta requires a 4-character tag (--tag)"); + log.UserError("addmeta requires a 4-character tag (--tag)"); return; } if (text != null && inputFile != null) { - log.Warning("addmeta: specify either --valuetext or --valuefile, not both"); + log.UserError("addmeta: specify either --valuetext or --valuefile, not both"); return; } @@ -4571,7 +4571,7 @@ private static void AddMetaTest(string[] args) text ??= ""; if (text.Any(c => c > 127)) { - log.Warning( + log.UserError( "addmeta: --valuetext contains non-ASCII characters; use --valuefile with a binary file instead" ); return; @@ -4626,7 +4626,7 @@ private static void DeleteMetaTest(string[] args) case "--index" or "-ix" when i + 1 < args.Length: if (!uint.TryParse(args[++i], out index)) { - log.Warning("Invalid metadata index: {Value}", args[i]); + log.UserError("Invalid metadata index: {Value}", args[i]); return; } @@ -4638,19 +4638,19 @@ private static void DeleteMetaTest(string[] args) break; } - log.Warning("Unknown option: {Option}", args[i]); + log.UserError("Unknown option: {Option}", args[i]); return; } if (file == null) { - log.Warning("delmeta requires --input "); + log.UserError("delmeta requires --input "); return; } if (tag is not { Length: 4 }) { - log.Warning("delmeta requires a 4-character tag (--tag)"); + log.UserError("delmeta requires a 4-character tag (--tag)"); return; } @@ -4844,7 +4844,7 @@ private static string ExpandChdmanOutputBinTemplate( else { // chdman.cpp:2780 Warning: encountered unknown format value '%s', ignoring - Log.Logger.Warning( + Log.Logger.UserError( "Warning: encountered unknown format value '{Type}', ignoring", type ); @@ -4868,7 +4868,7 @@ private static bool ChdmanTemplateContainsUnescapedTrackVariable(string template continue; if (!string.Equals(type, "t", StringComparison.Ordinal)) { - Log.Logger.Warning( + Log.Logger.UserError( "Warning: encountered unknown format value '{Type}', ignoring", type ); @@ -5372,7 +5372,7 @@ private static bool CreateDvdTest(string inputPath, string outputPath, string[] var log = Log.Logger; if (!File.Exists(inputPath)) { - log.Warning("createdvd: input file not found: {Path}", inputPath); + log.UserError("createdvd: input file not found: {Path}", inputPath); return false; } @@ -5418,7 +5418,7 @@ ref inputLengthFrames { Console.Error.WriteLine($"Error: file already exists ({outputPath})"); Console.Error.WriteLine("Use --force (or -f) to force overwriting"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outputPath ); @@ -5437,7 +5437,7 @@ ref inputLengthFrames { Console.Error.WriteLine( $"Error: Output parent CHD sector size {parentHdrDvd.UnitBytes} bytes does not match DVD-ROM sector size 2048 bytes"); - log.Warning( + log.UserError( "Output parent CHD sector size {ParentUnit} bytes does not match DVD-ROM sector size 2048 bytes", parentHdrDvd.UnitBytes); return false; @@ -5447,7 +5447,7 @@ ref inputLengthFrames { Console.Error.WriteLine( $"Error: Specified hunk size {hunkBytes} bytes does not match output parent CHD hunk size {parentHdrDvd.HunkBytes} bytes"); - log.Warning( + log.UserError( "Specified hunk size {Hunk} bytes does not match output parent CHD hunk size {ParentHunk} bytes", hunkBytes, parentHdrDvd.HunkBytes); return false; @@ -5459,21 +5459,21 @@ ref inputLengthFrames if (hunkBytes < 16) { Console.Error.WriteLine("Error: Invalid hunk size (minimum 16)"); - log.Warning("Invalid hunk size {Hunk} (minimum 16)", hunkBytes); + log.UserError("Invalid hunk size {Hunk} (minimum 16)", hunkBytes); return false; } if (hunkBytes > 1024 * 1024) { Console.Error.WriteLine("Error: Invalid hunk size (maximum 1048576)"); - log.Warning("Invalid hunk size {Hunk} (maximum 1048576)", hunkBytes); + log.UserError("Invalid hunk size {Hunk} (maximum 1048576)", hunkBytes); return false; } if (hunkBytes % 2048 != 0) { Console.Error.WriteLine($"Error: Hunk size {hunkBytes} bytes is not a whole multiple of 2048"); - log.Warning("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkBytes, 2048); + log.UserError("Hunk size {Hunk} bytes is not a whole multiple of {Unit}", hunkBytes, 2048); return false; } @@ -5500,7 +5500,7 @@ ref inputLengthFrames { Console.Error.WriteLine( $"Error: Data size {BigintString(dataLen)} is not divisible by sector size 2048"); - log.Warning("Data size {Size} is not divisible by sector size 2048", dataLen); + log.UserError("Data size {Size} is not divisible by sector size 2048", dataLen); return false; } } @@ -5567,7 +5567,7 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] var log = Log.Logger; if (!File.Exists(inputPath)) { - log.Warning("extractraw: input file not found: {Path}", inputPath); + log.UserError("extractraw: input file not found: {Path}", inputPath); return false; } @@ -5583,7 +5583,7 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] var arg = options[i]; if (string.IsNullOrEmpty(arg) || arg[0] != '-') { - log.Warning("Error: Expected option, not parameter"); + log.UserError("Error: Expected option, not parameter"); PrintCommandHelp("extractraw"); return false; } @@ -5617,14 +5617,14 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] hasParam = false; break; default: - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("extractraw"); return false; } if (extractRawSeen.Contains(canonical)) { - log.Warning("Error: Multiple parameters of the same type specified"); + log.UserError("Error: Multiple parameters of the same type specified"); PrintCommandHelp("extractraw"); return false; } @@ -5633,7 +5633,7 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] { if (i + 1 >= options.Length || (!string.IsNullOrEmpty(options[i + 1]) && options[i + 1][0] == '-')) { - log.Warning("Error: Option is missing parameter"); + log.UserError("Error: Option is missing parameter"); PrintCommandHelp("extractraw"); return false; } @@ -5645,7 +5645,7 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] case "inputstartbyte": if (!TryParseSizeWithSuffix(param, out long sb) || sb < 0) { - log.Warning("Invalid input start byte: {Value}", param); + log.UserError("Invalid input start byte: {Value}", param); return false; } @@ -5654,7 +5654,7 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] case "inputbytes": if (!TryParseSizeWithSuffix(param, out long ib) || ib <= 0) { - log.Warning("Invalid input bytes: {Value}", param); + log.UserError("Invalid input bytes: {Value}", param); return false; } @@ -5663,7 +5663,7 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] case "inputstarthunk": if (!TryParseSizeWithSuffix(param, out long sh) || sh < 0) { - log.Warning("Invalid input start hunk: {Value}", param); + log.UserError("Invalid input start hunk: {Value}", param); return false; } @@ -5672,7 +5672,7 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] case "inputhunks": if (!TryParseSizeWithSuffix(param, out long ih) || ih <= 0) { - log.Warning("Invalid input hunks: {Value}", param); + log.UserError("Invalid input hunks: {Value}", param); return false; } @@ -5690,14 +5690,14 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] if (startByte.HasValue && startHunk.HasValue) { - log.Warning("Error: Start offset cannot be specified in both bytes and hunks"); + log.UserError("Error: Start offset cannot be specified in both bytes and hunks"); PrintCommandHelp("extractraw"); return false; } if (lengthBytes.HasValue && lengthHunks.HasValue) { - log.Warning("Error: Length cannot be specified in both bytes and hunks"); + log.UserError("Error: Length cannot be specified in both bytes and hunks"); PrintCommandHelp("extractraw"); return false; } @@ -5706,7 +5706,7 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] { Console.Error.WriteLine($"Error: file already exists ({outputPath})"); Console.Error.WriteLine("Use --force (or -f) to force overwriting"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outputPath ); @@ -5742,14 +5742,14 @@ private static bool ExtractRawTest(string inputPath, string outputPath, string[] if (readStart >= chd.TotalBytes) { Console.Error.WriteLine("Error: Input start offset is beyond end of input"); - log.Warning("Input start offset is beyond end of input"); + log.UserError("Input start offset is beyond end of input"); return false; } if (readStart + readLength > chd.TotalBytes) { Console.Error.WriteLine("Error: Input length is larger than available input from start offset"); - log.Warning("Input length is larger than available input from start offset"); + log.UserError("Input length is larger than available input from start offset"); return false; } @@ -5823,7 +5823,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] var log = Log.Logger; if (!File.Exists(inputPath)) { - log.Warning("extractcd: input file not found: {Path}", inputPath); + log.UserError("extractcd: input file not found: {Path}", inputPath); return false; } @@ -5838,7 +5838,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] var arg = options[i]; if (string.IsNullOrEmpty(arg) || arg[0] != '-') { - log.Warning("Error: Expected option, not parameter"); + log.UserError("Error: Expected option, not parameter"); PrintCommandHelp("extractcd"); return false; } @@ -5872,7 +5872,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] hasParam = false; break; default: - log.Warning("Error: Option '{Option}' not valid for this command", arg); + log.UserError("Error: Option '{Option}' not valid for this command", arg); PrintCommandHelp("extractcd"); return false; } @@ -5881,7 +5881,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] var dupKey = string.Equals(canonical, "raw", StringComparison.OrdinalIgnoreCase) ? "cooked" : canonical; if (extractCdSeen.Contains(dupKey)) { - log.Warning("Error: Multiple parameters of the same type specified"); + log.UserError("Error: Multiple parameters of the same type specified"); PrintCommandHelp("extractcd"); return false; } @@ -5890,7 +5890,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] { if (i + 1 >= options.Length || (!string.IsNullOrEmpty(options[i + 1]) && options[i + 1][0] == '-')) { - log.Warning("Error: Option is missing parameter"); + log.UserError("Error: Option is missing parameter"); PrintCommandHelp("extractcd"); return false; } @@ -5905,7 +5905,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] if (param.Contains('"')) { Console.Error.WriteLine($"Output bin filename ({param}) must not contain quotation marks"); - log.Warning("Output bin filename ({Param}) must not contain quotation marks", param); + log.UserError("Output bin filename ({Param}) must not contain quotation marks", param); return false; } @@ -5932,7 +5932,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] { Console.Error.WriteLine($"Error: file already exists ({outputPath})"); Console.Error.WriteLine("Use --force (or -f) to force overwriting"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", outputPath ); @@ -5967,7 +5967,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] if (binPath?.Contains('"') == true) { Console.Error.WriteLine($"Output bin filename ({binPath}) must not contain quotation marks"); - log.Warning("Output bin filename ({Bin}) must not contain quotation marks", binPath); + log.UserError("Output bin filename ({Bin}) must not contain quotation marks", binPath); return false; } @@ -5976,7 +5976,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] { Console.Error.WriteLine( "A track number variable (%t) must be specified in the output bin filename when --splitbin is enabled"); - log.Warning( + log.UserError( "A track number variable (%t) must be specified in the output bin filename when --splitbin is enabled"); return false; } @@ -6011,7 +6011,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] if (File.Exists(tocPath) && !force) { Console.Error.WriteLine($"Error: file already exists ({tocPath})"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", tocPath ); @@ -6021,7 +6021,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] if (File.Exists(binTarget) && !force) { Console.Error.WriteLine($"Error: file already exists ({binTarget})"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", binTarget ); @@ -6292,7 +6292,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] if (File.Exists(tocPath) && !force) { Console.Error.WriteLine($"Error: file already exists ({tocPath})"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", tocPath ); @@ -6302,7 +6302,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] if (File.Exists(binTarget) && !force) { Console.Error.WriteLine($"Error: file already exists ({binTarget})"); - log.Warning( + log.UserError( "Output file already exists: {Path} (use --force to overwrite)", binTarget ); @@ -6343,7 +6343,7 @@ private static bool ExtractCdTest(string inputPath, string outputPath, string[] var targetBin = Path.GetFullPath(binPath); if (File.Exists(targetBin) && !force) { - log.Warning( + log.UserError( "Output BIN already exists: {Path} (use --force to overwrite)", targetBin ); diff --git a/CHDSharpCli/UserErrorLogging.cs b/CHDSharpCli/UserErrorLogging.cs new file mode 100644 index 0000000..d9861d2 --- /dev/null +++ b/CHDSharpCli/UserErrorLogging.cs @@ -0,0 +1,33 @@ +using Serilog; + +namespace CHDSharp; + +/// +/// Logging helpers for expected user and usage errors. +/// +/// +/// A user error (an unknown option, an invalid numeric argument, an input file that is not usable as the +/// requested format, ...) is a normal outcome of running a command-line tool. It must be visible in the +/// console and in the rolling file log, but it is not an application defect, so it must never be forwarded +/// to the Bug Report API. logs such an event at +/// while tagging it with +/// , which filters out. +/// +internal static class UserErrorLogging +{ + /// + /// Logs an expected user or usage error. The event keeps warning-level severity for the console and file + /// sinks but is tagged so does not report it as an application bug. + /// + /// The logger to write to. + /// Message template describing the problem. + /// Values for the message template. + /// is . + public static void UserError(this ILogger logger, string messageTemplate, params object?[] args) + { + ArgumentNullException.ThrowIfNull(logger); + logger + .ForContext(BugReportSink.UserErrorProperty, true) + .Warning(messageTemplate, args); + } +} diff --git a/CHDSharpTest/CliIntegrationTests.cs b/CHDSharpTest/CliIntegrationTests.cs index e15ab3b..32026c8 100644 --- a/CHDSharpTest/CliIntegrationTests.cs +++ b/CHDSharpTest/CliIntegrationTests.cs @@ -155,4 +155,45 @@ public void Usage_shows_chdman_style_commands() Assert.Contains("extractcd:", output, StringComparison.Ordinal); Assert.Contains("help ", output, StringComparison.Ordinal); } + + [Fact] + public void Createdvd_rejects_input_size_that_is_not_sector_aligned() + { + // chdman.cpp:2263 - createdvd refuses input whose length is not a multiple of 2048. + var dir = Path.Combine(Path.GetTempPath(), "chdsharp-createdvd-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(dir); + try + { + var input = Path.Combine(dir, "odd.iso"); + File.WriteAllBytes(input, new byte[1000]); + var output = Path.Combine(dir, "out.chd"); + + var (exitCode, log) = RunCli("createdvd", "--input", input, "--output", output); + + Assert.Equal(1, exitCode); + Assert.Contains( + "Data size 1,000 is not divisible by sector size 2048", + log, + StringComparison.Ordinal + ); + Assert.False(File.Exists(output)); + } + finally + { + Directory.Delete(dir, recursive: true); + } + } + + [Fact] + public void Createdvd_reports_usage_error_when_arguments_are_missing() + { + var (exitCode, log) = RunCli("createdvd"); + + Assert.Equal(1, exitCode); + Assert.Contains( + "createdvd requires --input --output ", + log, + StringComparison.Ordinal + ); + } } \ No newline at end of file diff --git a/docs/logging.md b/docs/logging.md index eb1bcb6..ea76e92 100644 --- a/docs/logging.md +++ b/docs/logging.md @@ -110,6 +110,7 @@ Both apps attach a `BugReportSink` (`CHDSharpCli/BugReportSink.cs`, `CHDSharpTes - Fire-and-forget (`Task.Run`, 10 s `HttpClient` timeout): delivery never blocks or throws into the logging pipeline. On `Log.CloseAndFlush()`, the sink waits up to 5 s for in-flight reports so short-lived CLI sessions and crash shutdowns still deliver them. - `OperationCanceledException` / `TaskCanceledException` events are skipped (user cancellation is not a bug). +- Events tagged with the `UserError` property are skipped: they describe expected user/usage errors (bad flags, missing paths, sector-misaligned input, an existing output file without `--force`, overridden format options, ...), which are not application defects. The CLI attaches the tag through `logger.UserError(...)` (`CHDSharpCli/UserErrorLogging.cs`); the event keeps `Warning` severity in the console and file logs, so nothing is hidden locally - it is only kept out of the Bug Report API. Real runtime failures stay on `logger.Warning(...)` and are still reported. - Server throttling (`HTTP 429`) and any transport failure are silently tolerated. Each report embeds the same sections: From 0d140a61535251367b061510f6d991b5b8c88bf9 Mon Sep 17 00:00:00 2001 From: Peterson Fernandes Date: Mon, 2 Nov 2026 11:56:38 -0300 Subject: [PATCH 2/9] chore(deps): bump Meziantou.Analyzer to 3.0.320 --- CHDSharpBattleTest/CHDSharpBattleTest.csproj | 2 +- CHDSharpBenchmark/CHDSharpBenchmark.csproj | 2 +- CHDSharpCli/CHDSharpCli.csproj | 2 +- CHDSharpEncoderTest/CHDSharpEncoderTest.csproj | 2 +- CHDSharpLib/CHDSharpLib.csproj | 4 ++-- CHDSharpTest/CHDSharpTest.csproj | 2 +- CHDSharpTestDataGeneration/CHDSharpTestDataGeneration.csproj | 2 +- CHDSharpTester/CHDSharpTester.csproj | 2 +- VendoredFlac/VendoredFlac.csproj | 2 +- VendoredLZMA/VendoredLZMA.csproj | 2 +- VendoredZLib/VendoredZLib.csproj | 2 +- VendoredZSTD/VendoredZSTD.csproj | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CHDSharpBattleTest/CHDSharpBattleTest.csproj b/CHDSharpBattleTest/CHDSharpBattleTest.csproj index dc51094..2ff8819 100644 --- a/CHDSharpBattleTest/CHDSharpBattleTest.csproj +++ b/CHDSharpBattleTest/CHDSharpBattleTest.csproj @@ -68,7 +68,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CHDSharpBenchmark/CHDSharpBenchmark.csproj b/CHDSharpBenchmark/CHDSharpBenchmark.csproj index 8bd2598..39a1e1f 100644 --- a/CHDSharpBenchmark/CHDSharpBenchmark.csproj +++ b/CHDSharpBenchmark/CHDSharpBenchmark.csproj @@ -9,7 +9,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CHDSharpCli/CHDSharpCli.csproj b/CHDSharpCli/CHDSharpCli.csproj index 53f640c..6c372b5 100644 --- a/CHDSharpCli/CHDSharpCli.csproj +++ b/CHDSharpCli/CHDSharpCli.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CHDSharpEncoderTest/CHDSharpEncoderTest.csproj b/CHDSharpEncoderTest/CHDSharpEncoderTest.csproj index 3d1cba4..6d9d8ad 100644 --- a/CHDSharpEncoderTest/CHDSharpEncoderTest.csproj +++ b/CHDSharpEncoderTest/CHDSharpEncoderTest.csproj @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CHDSharpLib/CHDSharpLib.csproj b/CHDSharpLib/CHDSharpLib.csproj index be0ae3e..b85eece 100644 --- a/CHDSharpLib/CHDSharpLib.csproj +++ b/CHDSharpLib/CHDSharpLib.csproj @@ -18,7 +18,7 @@ git LICENSE.txt - v1.6.0: reliability and supply-chain release. Read-ahead Dispose and ConfigureReadAhead(0) no longer block on in-flight reads, duplicate in-flight hunks can no longer leak a semaphore permit, and shutdown cancellation is not logged as a decompression failure. Truncated CD sources now report track, frame, logical frame, source offset, sector size, bytes read and file length. The CI gate adds InspectCode, packaging, coverage, a Jekyll docs build and a chdman parity smoke; CodeQL (C# and Actions), Dependency Review, OpenSSF Scorecard, Dependabot, NuGet audit, SHA-pinned actions, secret scanning with push protection, private vulnerability reporting and provenance-attested releases harden the supply chain. No public API changes. Pure C# - zero native dependencies. Targets net8.0/net9.0/net10.0. + v1.6.1: bug-fix release. Expected user/usage errors in the CLI (bad flags, missing paths, sector-misaligned input, an existing output without --force, overridden format options) are now logged through ILogger.UserError and tagged so BugReportSink never posts them to the Bug Report API; genuine runtime failures are still reported. Adds a DocFX-generated per-member API reference published with the docs site (0 metadata warnings) and moves Pages deployment to a dedicated workflow. Console text and exit codes are unchanged (chdman parity). No public API changes. Pure C# - zero native dependencies. Targets net8.0/net9.0/net10.0. true $(WarningsAsErrors);CS1591 @@ -29,7 +29,7 @@ true - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CHDSharpTest/CHDSharpTest.csproj b/CHDSharpTest/CHDSharpTest.csproj index eaf1b08..bcd1210 100644 --- a/CHDSharpTest/CHDSharpTest.csproj +++ b/CHDSharpTest/CHDSharpTest.csproj @@ -13,7 +13,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CHDSharpTestDataGeneration/CHDSharpTestDataGeneration.csproj b/CHDSharpTestDataGeneration/CHDSharpTestDataGeneration.csproj index 0a87ae1..0daa403 100644 --- a/CHDSharpTestDataGeneration/CHDSharpTestDataGeneration.csproj +++ b/CHDSharpTestDataGeneration/CHDSharpTestDataGeneration.csproj @@ -12,7 +12,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/CHDSharpTester/CHDSharpTester.csproj b/CHDSharpTester/CHDSharpTester.csproj index cebd652..a32f992 100644 --- a/CHDSharpTester/CHDSharpTester.csproj +++ b/CHDSharpTester/CHDSharpTester.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/VendoredFlac/VendoredFlac.csproj b/VendoredFlac/VendoredFlac.csproj index 7e48069..a090fa5 100644 --- a/VendoredFlac/VendoredFlac.csproj +++ b/VendoredFlac/VendoredFlac.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/VendoredLZMA/VendoredLZMA.csproj b/VendoredLZMA/VendoredLZMA.csproj index 669923b..41ffdb3 100644 --- a/VendoredLZMA/VendoredLZMA.csproj +++ b/VendoredLZMA/VendoredLZMA.csproj @@ -10,7 +10,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/VendoredZLib/VendoredZLib.csproj b/VendoredZLib/VendoredZLib.csproj index 495c4d7..de14b15 100644 --- a/VendoredZLib/VendoredZLib.csproj +++ b/VendoredZLib/VendoredZLib.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/VendoredZSTD/VendoredZSTD.csproj b/VendoredZSTD/VendoredZSTD.csproj index 28237e6..33d41ec 100644 --- a/VendoredZSTD/VendoredZSTD.csproj +++ b/VendoredZSTD/VendoredZSTD.csproj @@ -17,7 +17,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From e46b57cb59b48e74890824e70409ec93b37b2fb3 Mon Sep 17 00:00:00 2001 From: Peterson Fernandes Date: Mon, 2 Nov 2026 11:56:41 -0300 Subject: [PATCH 3/9] docs(lib): remove duplicate blocks from CHD.cs The out-parameter overloads combined with explicit tags for parameters the inherited comment already documents, so DocFX reported 14 "Duplicate parameter" warnings while generating metadata. Keep for the parameters that are identical and document only the extra out parameters (chdVersion/chdSha1/chdMd5). ReadHeader(Stream,...) had no inherited parameters in common, so it now documents itself fully. docfx metadata: 0 warnings. --- CHDSharpLib/CHD.cs | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/CHDSharpLib/CHD.cs b/CHDSharpLib/CHD.cs index 076a3c9..45b0fcc 100644 --- a/CHDSharpLib/CHD.cs +++ b/CHDSharpLib/CHD.cs @@ -396,13 +396,6 @@ out var decompressionOk /// - /// A readable, seekable stream positioned at the start of the CHD file. - /// The filename associated with the stream, used only for logging. - /// - /// If true, performs full decompression of every hunk plus SHA1/MD5 hash - /// verification (using up to parallel workers); if false, only the header is - /// validated. - /// /// /// When this method returns, contains the CHD version (1-5), or null if the header was /// invalid. @@ -415,14 +408,6 @@ out var decompressionOk /// When this method returns, contains the MD5 hash from the header, or null if not available /// (V4/V5). /// - /// - /// An optional receiving a - /// report after each decompressed hunk during deep verification. null (default) disables progress reporting. - /// - /// - /// A token to cancel deep verification. - /// is thrown if cancellation is requested while hunks are being decompressed or hashed. - /// /// on success; otherwise an error code describing the failure. public static ChdError CheckFile( Stream s, @@ -680,8 +665,6 @@ public static ChdResult CheckFileWithParent( /// - /// Path to the CHD file to verify. - /// Path to the parent CHD, or null/empty for a standalone CHD. /// /// When this method returns, contains the CHD version (1-5), or null if the file could not /// be opened. @@ -691,14 +674,6 @@ public static ChdResult CheckFileWithParent( /// available. /// /// When this method returns, contains the MD5 hash from the header, or null if not available. - /// - /// An optional receiving a - /// report after each decompressed hunk. null (default) disables progress reporting. - /// - /// - /// A token to cancel verification. - /// is thrown if cancellation is requested while hunks are being read. - /// /// on success; otherwise an error code describing the failure. public static ChdError CheckFileWithParent( string filename, @@ -836,8 +811,6 @@ public static ChdResult CheckFileWithParent( /// - /// Path to the CHD file to verify. - /// A callback that resolves parent CHDs by SHA1/MD5 hash, or null. /// /// When this method returns, contains the CHD version (1-5), or null if the file could not /// be opened. @@ -847,8 +820,6 @@ public static ChdResult CheckFileWithParent( /// available. /// /// When this method returns, contains the MD5 hash from the header, or null if not available. - /// An optional progress reporter, or null. - /// A token to cancel verification. /// on success; otherwise an error code describing the failure. public static ChdError CheckFileWithParent( string filename, @@ -1119,7 +1090,6 @@ public static ChdError ReadHeader(string filename, out ChdHeaderInfo? header) return err; } - /// /// /// Reads and parses the full CHD header from an existing seekable stream /// (libchdr chd_read_header_file parity). The stream is seeked as needed and left open. From 8443c36f0574143b33b6cd52969cb383b2d9c30a Mon Sep 17 00:00:00 2001 From: Peterson Fernandes Date: Mon, 2 Nov 2026 11:56:58 -0300 Subject: [PATCH 4/9] docs(api): publish a DocFX API reference with the docs site The guides had a hand-written API page but no per-member reference. - DocFX 2.81.0 is pinned in .config/dotnet-tools.json; docfx.json generates the managed reference for CHDSharpLib (net10.0) into docfx/ and renders the site to _site/api. - scripts/build-api-docs.ps1 runs metadata + build + the theme step, so local and CI output are identical. - .github/workflows/pages.yml builds the Jekyll guides and the API reference into one artifact and deploys it with actions/deploy-pages. GitHub Pages moves to build_type: workflow, so the wiki job no longer requests a legacy rebuild (ci.yml, release.yml). - The CI docs job now also builds DocFX, so broken XML docs fail the gate. - docs/_config.yml pins baseurl: /CHDSharp: Jekyll is no longer run by the Pages builder, which used to inject the project prefix. - The sidebar and the hand-written API page link to /api/, and the reference uses the same GitHub-dark palette as the guides. - Generated metadata (docfx/*.yml) and the rendered _site/ are git-ignored. --- .config/dotnet-tools.json | 13 ++++ .github/workflows/ci.yml | 23 +++++-- .github/workflows/pages.yml | 88 ++++++++++++++++++++++++ .github/workflows/release.yml | 7 +- .gitignore | 6 +- AGENTS.md | 27 +++++--- docfx.json | 53 ++++++++++++++ docfx/index.md | 24 +++++++ docfx/theme.css | 125 ++++++++++++++++++++++++++++++++++ docs/README.md | 1 + docs/_config.yml | 4 ++ docs/_includes/sidebar.html | 3 + docs/api-reference.md | 5 ++ docs/building.md | 38 +++++++++-- scripts/build-api-docs.ps1 | 52 ++++++++++++++ 15 files changed, 439 insertions(+), 30 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 .github/workflows/pages.yml create mode 100644 docfx.json create mode 100644 docfx/index.md create mode 100644 docfx/theme.css create mode 100644 scripts/build-api-docs.ps1 diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..1490bfb --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "docfx": { + "version": "2.81.0", + "commands": [ + "docfx" + ], + "rollForward": false + } + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0276cc2..f76cadd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -204,9 +204,9 @@ jobs: retention-days: 14 docs_build: - name: docs build + name: docs build (Jekyll + DocFX) runs-on: ubuntu-24.04 - timeout-minutes: 20 + timeout-minutes: 30 steps: - name: Checkout @@ -214,6 +214,13 @@ jobs: with: persist-credentials: false + - name: Setup .NET SDK 10 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: "10.0.x" + cache: true + cache-dependency-path: "**/*.csproj" + - name: Setup Ruby uses: ruby/setup-ruby@de492cf36cbab201753d720ce4f4e336cd688c94 # v1.336.0 with: @@ -225,6 +232,10 @@ jobs: gem install jekyll -v 4.3.4 --no-document jekyll build --source docs --destination _site --strict_front_matter --trace + - name: Build DocFX API reference + shell: pwsh + run: ./scripts/build-api-docs.ps1 + battle: name: chdman parity (smoke) runs-on: windows-latest @@ -286,7 +297,7 @@ jobs: done wiki: - name: Wiki & Pages + name: Wiki sync if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') needs: ci runs-on: ubuntu-24.04 @@ -324,7 +335,5 @@ jobs: git push fi - - name: Request Pages rebuild - env: - GH_TOKEN: ${{ secrets.WIKI_TOKEN }} - run: gh api -X POST repos/purelogiccode/CHDSharp/pages/builds --jq '.status' + # GitHub Pages is deployed by .github/workflows/pages.yml (build_type: workflow), + # which rebuilds the Jekyll guides and the DocFX API reference on every push. diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..c3edeb6 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,88 @@ +# Builds the complete documentation site and deploys it to GitHub Pages: +# - the Jekyll guides from docs/ +# - the DocFX API reference generated from CHDSharpLib's XML documentation (-> +# /api/) +# +# GitHub Pages is configured with `build_type: workflow`, so this workflow owns the +# deployment. It runs on every push to main that can affect the site, plus manual dispatch. +name: Pages + +on: + push: + branches: [main] + paths: + - "docs/**" + - "CHDSharpLib/**" + - "docfx/**" + - "docfx.json" + - ".config/dotnet-tools.json" + - "scripts/build-api-docs.ps1" + - ".github/workflows/pages.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +env: + DOTNET_CLI_TELEMETRY_OPTOUT: "1" + DOTNET_NOLOGO: "true" + +jobs: + build: + name: Build docs site + runs-on: ubuntu-24.04 + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup .NET SDK 10 + uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 + with: + dotnet-version: "10.0.x" + cache: true + cache-dependency-path: "**/*.csproj" + + - name: Setup Ruby + uses: ruby/setup-ruby@de492cf36cbab201753d720ce4f4e336cd688c94 # v1.336.0 + with: + ruby-version: "3.3" + bundler-cache: false + + - name: Build Jekyll guides + run: | + gem install jekyll -v 4.3.4 --no-document + jekyll build --source docs --destination _site --strict_front_matter --trace + + - name: Build DocFX API reference + shell: pwsh + run: ./scripts/build-api-docs.ps1 + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: _site + + deploy: + name: Deploy to Pages + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a6c6d69..aaeaa76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ # - six platform bundles (framework-dependent single-file CLI) attached to the GitHub release # - build provenance attestations for the bundles and the NuGet package # - the CHDSharp NuGet package -# - the docs/ folder synced to the GitHub wiki (and a Pages rebuild request) +# - the docs/ folder synced to the GitHub wiki (Pages is deployed by pages.yml) # # Assets are named CHDSharp_-_v.zip - the exact names the CLI's # built-in update check (VersionCheckService) looks up via the GitHub releases API. @@ -162,8 +162,3 @@ jobs: git commit -m "wiki: sync from docs/ (${GITHUB_REF_NAME})" git push fi - - - name: Request Pages rebuild - env: - GH_TOKEN: ${{ secrets.WIKI_TOKEN }} - run: gh api -X POST repos/purelogiccode/CHDSharp/pages/builds --jq '.status' diff --git a/.gitignore b/.gitignore index 52769b1..fd59199 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -################################################################################ +################################################################################ # This .gitignore file was automatically created by Microsoft(R) Visual Studio. ################################################################################ @@ -17,3 +17,7 @@ TestResults/ # Release packaging / wiki staging artifacts/ + +# DocFX generated API metadata and rendered site +docfx/*.yml +_site/ diff --git a/AGENTS.md b/AGENTS.md index 495c6a8..09453cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,6 +27,7 @@ the CHD (Compressed Hunks of Data) disk-image format used by MAME. | `CHDSharpBenchmark/` | BenchmarkDotNet suite | | `CHDSharpTestDataGeneration/` | Deterministic corpus generator | | `docs/` | Jekyll documentation site (deployed to GitHub Pages) | +| `docfx/`, `docfx.json` | DocFX API reference source + config (generated `/api/` site, pinned tool in `.config/dotnet-tools.json`) | | `References/` | Local chdman reference material; excluded from builds | ## Build / test / run @@ -121,8 +122,8 @@ matches `Directory.Build.props`. ``` 4. **Commit and push `main`**, then wait for the `CI gate` (three-OS build/test, InspectCode, - package, coverage, docs build, and the chdman parity smoke), after which the `Wiki & Pages` job - syncs the wiki and requests a Pages rebuild. + package, coverage, Jekyll + DocFX docs build, and the chdman parity smoke), after which the + `Wiki sync` job mirrors `docs/` to the wiki and the `Pages` workflow deploys the site. 5. **Build the bundles locally** (optional before tagging, required when the user asks for them): ```bash @@ -145,8 +146,8 @@ matches `Directory.Build.props`. `release.yml` verifies the tag against `Directory.Build.props`, rebuilds the six bundles, creates the GitHub release with `WhatsNew.md` as the body, uploads the assets, publishes the - NuGet package (skipped when the version already exists), syncs the wiki and requests a Pages - rebuild. + NuGet package (skipped when the version already exists), syncs the wiki and + deploys the documentation site (`pages.yml`). 7. **Verify the release**: @@ -175,20 +176,24 @@ Pages rebuild request; needs `repo` scope). `jb inspectcode --severity=WARNING` finding, uploading the SARIF to code scanning. - **`package`** - packs `CHDSharpLib`, checks the `.nupkg` contents, and uploads it as an artifact. - **`coverage`** - collects merged Cobertura coverage for both suites on `net10.0`. -- **`docs build`** - builds the Jekyll site with `--strict_front_matter`. +- **`docs build (Jekyll + DocFX)`** - builds the Jekyll site with `--strict_front_matter`, then + generates the DocFX API reference (`scripts/build-api-docs.ps1`). - **`chdman parity (smoke)`** - Windows; runs the quick battle harness against `CHDSharpTester/chdman.exe`. - **`CI gate`** - aggregates all of the above; this is the status check to require. -- **`Wiki & Pages`** - on `main` only, after the gate, regenerates the GitHub wiki from `docs/` - and requests a Pages rebuild. +- **`Wiki sync`** - on `main` only, after the gate, regenerates the GitHub wiki from `docs/`. Tests that need `chdman` no-op when it is absent, so the unit suites only run chdman validation on Windows; the quick battle job always passes the checked-in `CHDSharpTester/chdman.exe` explicitly. CI sets `CHDSHARP_NO_BUG_REPORTS=1`/`CHDSHARP_NO_TELEMETRY=1` so test-spawned CLI -runs never reach the production bug-report API or usage stats. The same wiki/Pages steps run in -`release.yml` for tags, so code, wiki and the documentation site stay in sync. GitHub Pages is -still served by the repository-managed `pages-build-deployment` workflow from `main`/`docs` -(build type `legacy`). +runs never reach the production bug-report API or usage stats. The same wiki step runs in +`release.yml` for tags, so code and wiki stay in sync. + +`.github/workflows/pages.yml` owns the documentation site: it builds the Jekyll guides plus the +DocFX API reference (`/api/`, from `CHDSharpLib` XML docs, DocFX pinned in +`.config/dotnet-tools.json`, driven by `scripts/build-api-docs.ps1`) and deploys the combined +artifact with `actions/deploy-pages`. GitHub Pages is configured with `build_type: workflow`; +there is no legacy `pages-build-deployment` build any more. ## Security diff --git a/docfx.json b/docfx.json new file mode 100644 index 0000000..0694d84 --- /dev/null +++ b/docfx.json @@ -0,0 +1,53 @@ +{ + "metadata": [ + { + "src": [ + { + "src": ".", + "files": [ + "CHDSharpLib/CHDSharpLib.csproj" + ] + } + ], + "dest": "docfx", + "outputFormat": "mref", + "properties": { + "TargetFramework": "net10.0" + } + } + ], + "build": { + "content": [ + { + "src": "docfx", + "files": [ + "index.md", + "toc.yml", + "*.yml" + ] + } + ], + "dest": "_site/api", + "template": [ + "default", + "modern" + ], + "globalMetadata": { + "_appTitle": "CHDSharp API Reference", + "_appName": "CHDSharp", + "_appFooter": "CHDSharp - pure C# CHD reader/writer. MIT licensed.", + "_enableSearch": true, + "_disableContribution": false, + "_gitContribute": { + "repo": "https://github.com/purelogiccode/CHDSharp", + "branch": "main", + "apiFolder": "docfx" + }, + "_gitUrlPattern": "github" + }, + "markdownEngineName": "markdig", + "noLangKeyword": false, + "keepFileLink": false, + "cleanFileCache": true + } +} diff --git a/docfx/index.md b/docfx/index.md new file mode 100644 index 0000000..0bb2f82 --- /dev/null +++ b/docfx/index.md @@ -0,0 +1,24 @@ +--- +_layout: landing +--- + +# CHDSharp API Reference + +Generated from the XML documentation of `CHDSharpLib` (NuGet package **CHDSharp**) by DocFX on every +push to `main`. + +Use the tree on the left, or jump straight to a namespace: + +| Namespace | Contents | +|---|---| +| [`CHDSharp`](CHDSharp.yml) | `Chd` entry point, `ChdFile` random-access reader, result and progress models | +| [`CHDSharp.Models`](CHDSharp.Models.yml) | `ChdError`, `ChdResult`, `ChdHeaderInfo`, `ChdProgress`, track and metadata models | +| [`CHDSharp.Utils`](CHDSharp.Utils.yml) | `CdRomAddress` MSF/LBA helpers, CRC-16, general utilities | +| [`CHDSharp.Encoder`](CHDSharp.Encoder.yml) | `ChdEncoder` - raw, CD/GD-ROM, DVD and laserdisc creation | +| [`CHDSharp.Encoder.Models`](CHDSharp.Encoder.Models.yml) | Encoder options, CUE/GDI models, progress types | +| [`CHDSharp.Encoder.Interfaces`](CHDSharp.Encoder.Interfaces.yml) | Codec extension points | + +The hand-written guides live on the [main documentation site](https://purelogiccode.github.io/CHDSharp/), +including [Getting Started](https://purelogiccode.github.io/CHDSharp/getting-started.html), the +[Encoder guide](https://purelogiccode.github.io/CHDSharp/encoder.html) and +[chdman parity](https://purelogiccode.github.io/CHDSharp/chdman-parity.html). diff --git a/docfx/theme.css b/docfx/theme.css new file mode 100644 index 0000000..a17008c --- /dev/null +++ b/docfx/theme.css @@ -0,0 +1,125 @@ +/* CHDSharp API reference theme - matches the main docs site (GitHub dark palette). */ + +:root { + color-scheme: dark; + --chd-bg: #0d1117; + --chd-bg-elevated: #161b22; + --chd-bg-sidebar: #010409; + --chd-border: #30363d; + --chd-text: #e6edf3; + --chd-text-muted: #8b949e; + --chd-link: #58a6ff; + --chd-link-hover: #79c0ff; +} + +html, +body { + background-color: var(--chd-bg) !important; + color: var(--chd-text); +} + +body, +article, +.main-container, +.content { + background-color: var(--chd-bg) !important; + color: var(--chd-text); +} + +a, +a:visited { + color: var(--chd-link); +} + +a:hover, +a:focus { + color: var(--chd-link-hover); +} + +h1, h2, h3, h4, h5, h6, +.h1, .h2, .h3, .h4, .h5, .h6 { + color: #f0f6fc; +} + +.navbar, +.navbar-dark, +.sidebar, +.affix, +.toc, +#affix, +#toc { + background-color: var(--chd-bg-sidebar) !important; + border-color: var(--chd-border) !important; +} + +.navbar a, +.sidebar a, +.toc a, +#affix a, +#toc a { + color: var(--chd-text); +} + +.navbar a:hover, +.sidebar a:hover, +.toc a:hover, +#affix a:hover, +#toc a:hover, +.sidebar a.active, +.toc a.active { + color: var(--chd-link); +} + +table, +.table { + color: var(--chd-text); + --bs-table-color: var(--chd-text); + --bs-table-bg: transparent; + --bs-table-border-color: var(--chd-border); + --bs-table-striped-bg: var(--chd-bg-elevated); + --bs-table-striped-color: var(--chd-text); +} + +thead th { + border-bottom-color: var(--chd-border) !important; +} + +code, +pre, +.inheritance, +.class, +.namespace { + color: var(--chd-text); +} + +pre { + background-color: var(--chd-bg-elevated) !important; + border: 1px solid var(--chd-border); + border-radius: 6px; +} + +.alert-info, +.alert-warning, +.alert-light { + background-color: var(--chd-bg-elevated); + border-color: var(--chd-border); + color: var(--chd-text); +} + +.footer, +footer { + color: var(--chd-text-muted); + border-top: 1px solid var(--chd-border); +} + +hr { + border-color: var(--chd-border); +} + +input, +.form-control, +.search-input { + background-color: var(--chd-bg-elevated); + border-color: var(--chd-border); + color: var(--chd-text); +} diff --git a/docs/README.md b/docs/README.md index 5cff9ab..dc7e5e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -69,6 +69,7 @@ CHDSharp is a pure C# implementation that reads and writes every CHD version wit | Page | Description | |------|-------------| | [API Reference](api-reference.md) | Complete reference for `Chd`, `ChdFile`, and all public models/enums. | +| [Generated API Reference](/api/) | Per-member DocFX reference generated from the library's XML documentation. | | [Metadata](metadata.md) | Reading and querying CHD metadata tags (`GDDD`, `CHT2`, `AVAV`, …). | | [Verification](verification.md) | Full/parallel and header-only verification, checksum semantics. | | [Extraction](extraction.md) | TOC parsing, CUE/GDI/ISO/IMG/RAW extraction, classification. | diff --git a/docs/_config.yml b/docs/_config.yml index 20bd1b8..4f9c3e5 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -2,3 +2,7 @@ title: CHDSharp description: Pure C# CHD (Compressed Hunks of Data) reader and writer — V1–V5, all codecs, parent/child chaining, 100% match with MAME chdman. markdown: kramdown url: https://github.com/purelogiccode/CHDSharp +# Project site root. GitHub Pages injected this implicitly for the legacy build; the +# `pages.yml` workflow builds Jekyll itself, so the prefix must be explicit or every +# `relative_url` link would resolve to the user-site root. +baseurl: "/CHDSharp" diff --git a/docs/_includes/sidebar.html b/docs/_includes/sidebar.html index 16c5f90..c3c5ad3 100644 --- a/docs/_includes/sidebar.html +++ b/docs/_includes/sidebar.html @@ -41,6 +41,9 @@
  • API Reference
  • +
  • + Generated API Reference (DocFX) → +
  • Metadata
  • diff --git a/docs/api-reference.md b/docs/api-reference.md index 8dabfa5..c284cd8 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -6,6 +6,11 @@ layout: default Complete reference for the public API of the `CHDSharp` package. All types live in the `CHDSharp` namespace (models in `CHDSharp.Models`, address helpers in `CHDSharp.Utils`). +> **Generated reference:** this page is the guided tour. The full, per-member reference generated +> from the XML documentation by DocFX is published at +> [purelogiccode.github.io/CHDSharp/api/](https://purelogiccode.github.io/CHDSharp/api/) and rebuilt +> on every push to `main`. + --- ## `Chd` — static class diff --git a/docs/building.md b/docs/building.md index 3eb89c5..8f9b7d3 100644 --- a/docs/building.md +++ b/docs/building.md @@ -32,7 +32,7 @@ CSharp_CHDSharp.sln ├── VendoredLZMA/ LZMA SDK C# port ├── VendoredFlac/ Pure C# FLAC encoder/decoder ├── VendoredZSTD/ Pure C# zstd 1.5.5 encoder/decoder (MAME parity) -└── Directory.Build.props Centralized versioning (1.6.0) and analyzer setup +└── Directory.Build.props Centralized versioning (1.6.1) and analyzer setup ``` --- @@ -52,7 +52,7 @@ dotnet build CHDSharpLib/CHDSharpLib.csproj -c Release ### Centralized versioning -`Directory.Build.props` defines a single `` (currently `1.6.0`) that all projects inherit, so `AssemblyVersion`, `FileVersion`, and the NuGet package version stay in sync automatically. +`Directory.Build.props` defines a single `` (currently `1.6.1`) that all projects inherit, so `AssemblyVersion`, `FileVersion`, and the NuGet package version stay in sync automatically. ### Code style & analyzers @@ -183,7 +183,7 @@ For every runtime identifier the script publishes the CLI as a **framework-depen single-file** binary (`--self-contained false -p:PublishSingleFile=true` — the .NET runtime is *not* embedded), appends `LICENSE.txt`, `README.md`, `README.txt` and `WhatsNew.md`, and writes `CHDSharp__v.zip` (the exact asset name the CLI's -built-in update check looks up on GitHub releases, e.g. `CHDSharp_win-x64_v1.6.0.zip`). +built-in update check looks up on GitHub releases, e.g. `CHDSharp_win-x64_v1.6.1.zip`). Existing files in the output path are never deleted. The binary is named `CHDSharp` (`CHDSharp.exe` on Windows); bundles require the .NET 10 runtime. @@ -193,6 +193,34 @@ wiki — see [CI](#ci). --- +## Documentation site (Jekyll + DocFX) + +The published site at is the combination of two builds: + +- the Jekyll guides in `docs/` (dark theme, sidebar, and the same pages mirrored to the wiki), and +- the DocFX API reference generated from `CHDSharpLib`'s XML documentation, published under + [`/api/`](https://purelogiccode.github.io/CHDSharp/api/). + +```bash +# Guides only +gem install jekyll -v 4.3.4 --no-document +jekyll build --source docs --destination _site --strict_front_matter + +# Complete site: guides + API reference, with the CHDSharp theme applied +pwsh scripts/build-api-docs.ps1 +``` + +DocFX is pinned in `.config/dotnet-tools.json`, so `dotnet tool restore` installs exactly the +version CI uses. `scripts/build-api-docs.ps1` runs `docfx metadata`, `docfx build` and appends +`docfx/theme.css` to the template stylesheet so the reference matches the guides. Generated +metadata (`docfx/*.yml`) and the rendered site (`_site/`) are git-ignored. + +The `Pages` workflow (`.github/workflows/pages.yml`) builds both halves into one artifact and deploys +it with `actions/deploy-pages`. GitHub Pages uses `build_type: workflow`, so the wiki job no longer +requests a legacy Pages rebuild. + +--- + ## CI The repository is set up for GitHub Actions (`ContinuousIntegrationBuild=true`), producing deterministic builds and SourceLink-enabled symbols. @@ -202,9 +230,9 @@ The repository is set up for GitHub Actions (`ContinuousIntegrationBuild=true`), - `quality`: `jb inspectcode --severity=WARNING`, failing on any finding and uploading SARIF to code scanning. - `package`: `dotnet pack` plus a `.nupkg` content check, uploaded as an artifact. - `coverage`: merged Cobertura coverage for both suites on `net10.0` (the wall-clock parallel-encode test is excluded because instrumentation invalidates its timing). - - `docs build`: Jekyll build of `docs/` with `--strict_front_matter`. + - `docs build (Jekyll + DocFX)`: builds `docs/` with `--strict_front_matter`, then generates the DocFX API reference, so broken XML docs or a broken `docfx.json` fail the gate. - `chdman parity (smoke)`: the quick battle harness against the checked-in `CHDSharpTester/chdman.exe`. - - `Wiki & Pages` (on `main` only, after the gate): syncs `docs/` to the wiki and requests a Pages rebuild. + - `Wiki sync` (on `main` only, after the gate): mirrors `docs/` to the wiki. The site itself is deployed by `pages.yml`. Test-spawned CLI runs are isolated from the production bug-report API via `CHDSHARP_NO_BUG_REPORTS=1` / `CHDSHARP_NO_TELEMETRY=1`. - **`codeql.yml`** - CodeQL C# analysis (`security-extended`) plus GitHub Actions analysis on `main`, pull requests, and a weekly schedule. diff --git a/scripts/build-api-docs.ps1 b/scripts/build-api-docs.ps1 new file mode 100644 index 0000000..c4925fa --- /dev/null +++ b/scripts/build-api-docs.ps1 @@ -0,0 +1,52 @@ +#!/usr/bin/env pwsh +<# +.SYNOPSIS + Builds the DocFX API reference into `_site/api` and applies the CHDSharp theme. + +.DESCRIPTION + Restores the pinned DocFX tool from `.config/dotnet-tools.json`, generates the managed + reference metadata for `CHDSharpLib`, renders the site and appends `docfx/theme.css` to the + template stylesheet so the reference matches the main documentation site. + + The `pages` workflow calls this script; contributors can run the same command locally: + + pwsh scripts/build-api-docs.ps1 + + The Jekyll site is built separately (`jekyll build --source docs --destination _site`), so the + final site is `_site` with the API reference under `_site/api`. + +.NOTES + Requires the .NET SDK from `global.json`. Generated metadata (`docfx/api`) is git-ignored. +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' + +$repoRoot = Split-Path -Parent $PSScriptRoot +Push-Location $repoRoot +try { + dotnet tool restore + if ($LASTEXITCODE -ne 0) { throw "dotnet tool restore failed with exit code $LASTEXITCODE" } + + dotnet docfx metadata docfx.json + if ($LASTEXITCODE -ne 0) { throw "docfx metadata failed with exit code $LASTEXITCODE" } + + dotnet docfx build docfx.json + if ($LASTEXITCODE -ne 0) { throw "docfx build failed with exit code $LASTEXITCODE" } + + $mainCss = Join-Path $repoRoot '_site/api/public/main.css' + if (-not (Test-Path -LiteralPath $mainCss)) { + throw "DocFX did not produce $mainCss" + } + + Add-Content -LiteralPath $mainCss -Value '' + Add-Content -LiteralPath $mainCss -Value '/* CHDSharp theme - see docfx/theme.css */' + Get-Content -LiteralPath (Join-Path $repoRoot 'docfx/theme.css') -Raw | + Add-Content -LiteralPath $mainCss + + Write-Host 'API reference written to _site/api' +} +finally { + Pop-Location +} From 20839dfe85b2bc95a4d7d51a762b448a56ce2b23 Mon Sep 17 00:00:00 2001 From: Peterson Fernandes Date: Mon, 2 Nov 2026 11:57:12 -0300 Subject: [PATCH 5/9] release: v1.6.1 Bug-fix and documentation release. - Expected user/usage errors no longer reach the Bug Report API (v1.6.0 behavior change is already in main's fix commit). - DocFX API reference published with the docs site. - Version 1.6.1 in Directory.Build.props; WhatsNew.md, docs/ReleaseNotes.md, README.md, CHDSharpLib/README.md, per-project banners and PackageReleaseNotes updated; docs/building.md version examples refreshed. --- .gitignore | 1 + CHDSharpCli/README.md | 2 +- CHDSharpLib/README.md | 6 ++ CHDSharpTest/README.md | 2 +- CHDSharpTestDataGeneration/README.md | 2 +- Directory.Build.props | 6 +- README.md | 8 +++ WhatsNew.md | 84 +++++++++++++++------------- docs/ReleaseNotes.md | 49 ++++++++++++++++ 9 files changed, 115 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index fd59199..f7acd3a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,4 +20,5 @@ artifacts/ # DocFX generated API metadata and rendered site docfx/*.yml +docfx/.manifest _site/ diff --git a/CHDSharpCli/README.md b/CHDSharpCli/README.md index 192a8cf..24d8953 100644 --- a/CHDSharpCli/README.md +++ b/CHDSharpCli/README.md @@ -17,7 +17,7 @@ The binary is named `CHDSharp` (e.g. `CHDSharp.exe` on Windows). It accepts the same subcommands and options as MAME's `chdman`, plus additional convenience commands. -> **v1.6.0** - read-ahead teardown no longer blocks on in-flight I/O, truncated CD sources report the exact track/frame/offset, and the project ships a full CI/security pipeline. Six platform bundles are attached to every release (`CHDSharp_-_v.zip`). Release bundles are framework-dependent single-file binaries and require the .NET 10 runtime; the library targets `net8.0` / `net9.0` / `net10.0`. +> **v1.6.1** - expected user errors (bad flags, missing paths, sector-misaligned input, an existing output without `--force`) are logged locally but no longer posted to the Bug Report API; real failures still are. Six platform bundles are attached to every release (`CHDSharp_-_v.zip`). Release bundles are framework-dependent single-file binaries and require the .NET 10 runtime; the library targets `net8.0` / `net9.0` / `net10.0`. --- diff --git a/CHDSharpLib/README.md b/CHDSharpLib/README.md index 8734555..92e914e 100644 --- a/CHDSharpLib/README.md +++ b/CHDSharpLib/README.md @@ -20,6 +20,12 @@ --- +## What's New in v1.6.1 + +- **Automatic bug reports now only cover real bugs** - CLI diagnostics for expected user input (bad flags, invalid numbers, missing paths, sector-misaligned sources, an existing output without `--force`, overridden format options) are logged at warning level but tagged so the Bug Report API never receives them; runtime failures are still reported. No library code changed. +- **Generated API reference** - a per-member DocFX reference built from the XML documentation is published with the docs site at [purelogiccode.github.io/CHDSharp/api/](https://purelogiccode.github.io/CHDSharp/api/). The library's duplicate `` docs were removed, so DocFX metadata builds with 0 warnings. +- **No public API changes** - behavior and documentation only; verified with the full suite across `net8.0`/`net9.0`/`net10.0`, `jb inspectcode` 0 findings and the quick battle harness (905/905 checks). + ## What's New in v1.6.0 - **Read-ahead teardown is non-blocking** - `Dispose()` and `ConfigureReadAhead(0)` no longer wait for in-flight reads, duplicate in-flight hunks can no longer leak a semaphore permit, and shutdown cancellation is not logged as a decompression failure. diff --git a/CHDSharpTest/README.md b/CHDSharpTest/README.md index 76d3db7..32eca94 100644 --- a/CHDSharpTest/README.md +++ b/CHDSharpTest/README.md @@ -14,7 +14,7 @@ **xUnit unit and corpus test suite for CHDSharp — tests header parsing, CRC checksums, and full decompression against a deterministic 30-file CHD corpus (V1–V5, all codecs).** -> **v1.6.0** - targets `net8.0` / `net9.0` / `net10.0`. 4,216 tests per target framework; encoder suite 455 tests; full battle harness 3,174/3,174 checks vs `chdman` 0.289; solution clean under `jb inspectcode`. +> **v1.6.1** - targets `net8.0` / `net9.0` / `net10.0`. 4,216 tests per target framework; encoder suite 455 tests; full battle harness 3,174/3,174 checks vs `chdman` 0.289; solution clean under `jb inspectcode`. v1.6.1 adds CLI regression tests for the `createdvd` validation paths. --- diff --git a/CHDSharpTestDataGeneration/README.md b/CHDSharpTestDataGeneration/README.md index 8af5e26..4945307 100644 --- a/CHDSharpTestDataGeneration/README.md +++ b/CHDSharpTestDataGeneration/README.md @@ -14,7 +14,7 @@ **Deterministic CHD test corpus generator for the CHDSharp xUnit test suite.** -> **v1.6.0** - targets `net8.0` / `net9.0` / `net10.0`. Generates the 30-file corpus for V1-V5 via MAME 0.289 `chdman`; version-aligned with the v1.6.0 release. +> **v1.6.1** - targets `net8.0` / `net9.0` / `net10.0`. Generates the 30-file corpus for V1-V5 via MAME 0.289 `chdman`; version-aligned with the v1.6.1 release. Generates the 30-file CHD corpus in `CHDSharpTest/TestData/`, covering every format version (V1–V5), all compression codecs (zlib, lzma, huffman, flac, zstd, avhu + CD variants), every map type, and parent/child chains. All source images are built deterministically (seeded PRNG) so regenerating the corpus produces byte-identical output. diff --git a/Directory.Build.props b/Directory.Build.props index 1d676db..4adcb08 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,8 +1,8 @@ - 1.6.0 - 1.6.0 - 1.6.0 + 1.6.1 + 1.6.1 + 1.6.1 enable enable embedded diff --git a/README.md b/README.md index f2ef8a7..c0328e1 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,14 @@ Supports every CHD format version (V1–V5), all 10 compression codecs, parent/c --- +## What's New in v1.6.1 + +- **The bug-report API only receives real bugs** - expected user/usage errors are tagged via `logger.UserError(...)` and stay in the console and file logs without being posted as defects. That covers bad flags, invalid numbers, missing paths, sector-misaligned input, an existing output without `--force`, overridden format options and unsupported combinations (216 call sites). Genuine failures (`Open failed`, `* failed: {Message}`, verification and extraction failures) are still reported. +- **Generated API reference** - the docs site now publishes a per-member DocFX reference at [purelogiccode.github.io/CHDSharp/api/](https://purelogiccode.github.io/CHDSharp/api/), rebuilt on every push to `main`, with DocFX pinned in `.config/dotnet-tools.json` and driven by `scripts/build-api-docs.ps1`. +- **Pages deploy via Actions** - a new `Pages` workflow builds the Jekyll guides and the DocFX reference into one artifact and deploys it, replacing the legacy Pages rebuild request. +- **Cleaner XML docs** - 14 duplicate `` blocks in `CHD.cs` were removed; DocFX metadata builds with 0 warnings. +- **Verification** - `CI gate` green across `net8.0`/`net9.0`/`net10.0`, `jb inspectcode` 0 findings, quick battle harness 905/905 checks. + ## What's New in v1.6.0 - **Read-ahead teardown is non-blocking** - `Dispose()` and `ConfigureReadAhead(0)` no longer wait for in-flight reads, and duplicate in-flight hunks can no longer leak a semaphore permit. Shutdown cancellation is no longer logged as a decompression failure. diff --git a/WhatsNew.md b/WhatsNew.md index 75077f6..584ece6 100644 --- a/WhatsNew.md +++ b/WhatsNew.md @@ -1,11 +1,10 @@ -# CHDSharp v1.6.0 - What's New +# CHDSharp v1.6.1 - What's New -**Released:** 2026-10-29 - [Full changelog](docs/ReleaseNotes.md) - [NuGet](https://www.nuget.org/packages/CHDSharp/1.6.0) +**Released:** 2026-11-02 - [Full changelog](docs/ReleaseNotes.md) - [NuGet](https://www.nuget.org/packages/CHDSharp/1.6.1) -CHDSharp v1.6.0 is a reliability and supply-chain release: read-ahead teardown no longer blocks on -in-flight I/O, truncated CD sources report exactly where the layout ran out, and the repository -gained a full CI gate with CodeQL, Dependency Review, OpenSSF Scorecard, Dependabot, NuGet audit, -SHA-pinned actions and provenance-attested release assets. +CHDSharp v1.6.1 is a bug-fix release for the CLI's automatic bug reporting and a documentation +release for the API reference. Expected user mistakes no longer reach the Bug Report API, and the +full per-member API reference is now generated by DocFX and published with the docs site. --- @@ -13,12 +12,12 @@ SHA-pinned actions and provenance-attested release assets. | Platform | Bundle | |----------|--------| -| Windows x64 | [`CHDSharp_win-x64_v1.6.0.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.0/CHDSharp_win-x64_v1.6.0.zip) | -| Windows ARM64 | [`CHDSharp_win-arm64_v1.6.0.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.0/CHDSharp_win-arm64_v1.6.0.zip) | -| Linux x64 | [`CHDSharp_linux-x64_v1.6.0.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.0/CHDSharp_linux-x64_v1.6.0.zip) | -| Linux ARM64 | [`CHDSharp_linux-arm64_v1.6.0.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.0/CHDSharp_linux-arm64_v1.6.0.zip) | -| macOS Intel | [`CHDSharp_osx-x64_v1.6.0.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.0/CHDSharp_osx-x64_v1.6.0.zip) | -| macOS Apple Silicon | [`CHDSharp_osx-arm64_v1.6.0.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.0/CHDSharp_osx-arm64_v1.6.0.zip) | +| Windows x64 | [`CHDSharp_win-x64_v1.6.1.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.1/CHDSharp_win-x64_v1.6.1.zip) | +| Windows ARM64 | [`CHDSharp_win-arm64_v1.6.1.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.1/CHDSharp_win-arm64_v1.6.1.zip) | +| Linux x64 | [`CHDSharp_linux-x64_v1.6.1.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.1/CHDSharp_linux-x64_v1.6.1.zip) | +| Linux ARM64 | [`CHDSharp_linux-arm64_v1.6.1.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.1/CHDSharp_linux-arm64_v1.6.1.zip) | +| macOS Intel | [`CHDSharp_osx-x64_v1.6.1.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.1/CHDSharp_osx-x64_v1.6.1.zip) | +| macOS Apple Silicon | [`CHDSharp_osx-arm64_v1.6.1.zip`](https://github.com/purelogiccode/CHDSharp/releases/download/v1.6.1/CHDSharp_osx-arm64_v1.6.1.zip) | Every bundle contains a **framework-dependent single-file** `CHDSharp` / `CHDSharp.exe` build (the .NET runtime is **not** embedded) plus `LICENSE.txt`, `README.md`, `README.txt` @@ -27,38 +26,45 @@ is required. --- -## Fixed: read-ahead disposal no longer blocks or leaks permits +## Fixed: the bug-report API only receives real bugs -`ChdFile.Dispose()` and `ConfigureReadAhead(0)` used to wait for every in-flight read-ahead read. -A stream that ignored cancellation could stall teardown indefinitely. The manager now cancels, -returns immediately, and lets the last background task dispose its semaphore and token source. -In-flight hunks are deduplicated, so two tasks can no longer race to cache the same hunk and leak a -permit. Shutdown cancellation is no longer logged as a decompression failure (important because -Warning+ events are forwarded to the CLI bug-report sink). +The Bug Report API had 383 `CHDSharpCli` reports and every one was a normal validation outcome, not +a defect: 382 `Data size ... is not divisible by sector size 2048` and one +`createdvd requires --input --output `. Both paths already matched `chdman` +(`chdman.cpp:2263`, `chdman.cpp:2088`): same console text, same exit code `1`, no partial output. +They were logged at `Warning`, and the sink forwards every `Warning+` event. -## Fixed: truncated CD sources are diagnosable +- The CLI now logs expected user/usage errors through `logger.UserError(...)`, which keeps them at + warning level in the console and in the rolling file log but tags them so `BugReportSink` does + not post them. +- 216 call sites were reclassified: unknown options, invalid numeric arguments, missing paths, + sector-misaligned input, an existing output file without `--force`, overridden format options, + unsupported option combinations, and similar usage diagnostics. +- Genuine runtime failures stay on `logger.Warning(...)` and are still reported: `Open failed`, + `* failed: {Message}`, verification failures, TOC and track extraction failures, and + access-denied errors. +- Console text and exit codes are unchanged, so `chdman` parity is untouched (quick battle harness: + 905/905 checks). -A short read while encoding a CD now reports the track, frame within the track, logical frame, -source offset (hex and decimal), sector size, bytes needed/read and the file length. Split-pregap -reads name the next track's file only when that file was actually used. +## New: generated API reference on the documentation site -## Full CI and security (new in v1.6.0) +The public surface is now documented per member at +[purelogiccode.github.io/CHDSharp/api/](https://purelogiccode.github.io/CHDSharp/api/), generated by +DocFX from the XML documentation of `CHDSharpLib`. -- **`CI gate`** - three-OS build/test across .NET 8/9/10, InspectCode, NuGet packaging, merged - coverage, a Jekyll docs build and a quick `chdman` parity battle. -- **CodeQL** for C# and GitHub Actions, **Dependency Review** on pull requests, and - **OpenSSF Scorecard** results published to code scanning. -- **Dependabot** alerts, security updates and grouped version updates; **NuGet audit** of direct - and transitive packages, with `-warnaserror` in CI. -- **Secret scanning** with push protection, **private vulnerability reporting**, read-only default - workflow permissions, mandatory full-SHA action pinning and artifact provenance attestations on - releases. -- **`SECURITY.md`** documents the reporting process and how to verify a release. +- DocFX is pinned in `.config/dotnet-tools.json` and driven by `scripts/build-api-docs.ps1`, so the + same command produces the same output locally and in CI. +- A new `Pages` workflow builds the Jekyll guides and the DocFX reference into one artifact and + deploys it (Pages now uses `build_type: workflow`); the wiki sync no longer requests a legacy + Pages rebuild. +- The reference uses the same GitHub-dark palette as the rest of the site. +- 14 duplicate `` XML-documentation blocks in `CHD.cs` were removed, so DocFX metadata now + generates with **0 warnings**. ## Verification -- `dotnet test` across `net8.0`/`net9.0`/`net10.0`: **14,013/14,013 passed** - (4,216 library tests + 455 encoder tests per target framework). -- Full battle harness: **3,174/3,174 checks**, byte-identical `chdman` output. -- `jb inspectcode CSharp_CHDSharp.sln --severity=WARNING`: **0 findings**. -- No public API changes - behavior fixes and repository infrastructure only. +- `dotnet build -c Release`: 0 warnings, 0 errors; `jb inspectcode --severity=WARNING`: 0 findings. +- Quick `chdman` parity battle: 905/905 checks passed. +- `CI gate`: three-OS build/test across `net8.0`/`net9.0`/`net10.0`, InspectCode, NuGet packaging, + merged coverage, Jekyll + DocFX docs build, and the `chdman` parity smoke. +- No public API changes - behavior, diagnostics and documentation only. diff --git a/docs/ReleaseNotes.md b/docs/ReleaseNotes.md index e8ed428..3ab2a2c 100644 --- a/docs/ReleaseNotes.md +++ b/docs/ReleaseNotes.md @@ -1,5 +1,54 @@ # CHDSharp Release Notes +## CHDSharp v1.6.1 + +Bug-fix and documentation release on top of v1.6.0. No public API changes. + +**Released:** 2026-11-02 - [NuGet](https://www.nuget.org/packages/CHDSharp/1.6.1) - +[CLI bundles](https://github.com/purelogiccode/CHDSharp/releases/tag/v1.6.1) + +### Bug-report API no longer receives expected user errors + +Every one of the 383 `CHDSharpCli` reports in the Bug Report API was a normal validation outcome +rather than a defect: 382 `Data size ... is not divisible by sector size 2048` and one +`createdvd requires --input --output `. Both paths already matched `chdman` +(`chdman.cpp:2263`, `chdman.cpp:2088`): identical console text, exit code `1`, no output file. +They were logged at `Warning`, and `BugReportSink` forwards every `Warning+` event. + +- `BugReportSink` now skips events tagged with the `UserError` property. +- New `ILogger.UserError(...)` extension (`CHDSharpCli/UserErrorLogging.cs`) keeps the event at + `Warning` for the console and file sinks but tags it, so nothing is hidden locally and nothing is + posted. +- 216 usage/validation call sites were reclassified: unknown options, invalid numeric arguments, + missing paths, sector-misaligned input, an existing output file without `--force`, overridden + format options and unsupported option combinations. +- Genuine runtime failures stay on `logger.Warning(...)` and are still reported: `Open failed`, + `* failed: {Message}`, verification failures, TOC/track extraction failures and access-denied. +- Console text and exit codes are unchanged; the quick battle harness stays at 905/905 checks. +- Regression tests cover the two reported paths in `CliIntegrationTests`. + +### Generated API reference + +- DocFX (pinned in `.config/dotnet-tools.json`) generates the per-member reference from the + `CHDSharpLib` XML documentation; `scripts/build-api-docs.ps1` drives metadata, build and the + theme step locally and in CI. +- New `Pages` workflow builds the Jekyll guides plus the DocFX reference into a single artifact and + deploys it; GitHub Pages now uses `build_type: workflow`, and the wiki jobs no longer request a + legacy Pages rebuild. +- The reference is published at + [purelogiccode.github.io/CHDSharp/api/](https://purelogiccode.github.io/CHDSharp/api/) and uses + the same GitHub-dark palette as the guides, with a sidebar link from the main site. +- Removed 14 duplicate `` XML-documentation blocks in `CHD.cs`; DocFX metadata now builds + with 0 warnings. + +### Verification + +- `dotnet build -c Release`: 0 warnings, 0 errors. +- `jb inspectcode CSharp_CHDSharp.sln --severity=WARNING`: 0 findings. +- Quick battle harness: 905/905 checks, byte-identical `chdman` output. +- `CI gate`: three-OS build/test across `net8.0`/`net9.0`/`net10.0`, InspectCode, packaging, + coverage, Jekyll + DocFX docs build and the `chdman` parity smoke. + ## CHDSharp v1.6.0 Reliability and supply-chain release on top of v1.5.2. No public API changes. From f945cd725c9998989fb84b1cb7d1f5346ba031ef Mon Sep 17 00:00:00 2001 From: Peterson Fernandes Date: Mon, 2 Nov 2026 11:58:57 -0300 Subject: [PATCH 6/9] docs: use a relative link to the generated API reference --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index dc7e5e0..33b542a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -69,7 +69,7 @@ CHDSharp is a pure C# implementation that reads and writes every CHD version wit | Page | Description | |------|-------------| | [API Reference](api-reference.md) | Complete reference for `Chd`, `ChdFile`, and all public models/enums. | -| [Generated API Reference](/api/) | Per-member DocFX reference generated from the library's XML documentation. | +| [Generated API Reference](api/) | Per-member DocFX reference generated from the library's XML documentation. | | [Metadata](metadata.md) | Reading and querying CHD metadata tags (`GDDD`, `CHT2`, `AVAV`, …). | | [Verification](verification.md) | Full/parallel and header-only verification, checksum semantics. | | [Extraction](extraction.md) | TOC parsing, CUE/GDI/ISO/IMG/RAW extraction, classification. | From af16853fce516b69e112c067fd0731c5d9c865c7 Mon Sep 17 00:00:00 2001 From: Peterson Fernandes Date: Mon, 2 Nov 2026 12:01:36 -0300 Subject: [PATCH 7/9] fix(lib): document all parameters explicitly on the out-parameter overloads Replacing the duplicate tags with removed the DocFX warnings but introduced CS1573: Roslyn does not expand inheritdoc when deciding whether a partially documented method is complete, and CHDSharpLib turns that warning into an error for pack/build with -warnaserror. Drop on the three out-parameter overloads and document every parameter (and a summary) explicitly, so both DocFX and the compiler are clean. ReadHeader(Stream,...) already documents its two parameters. --- CHDSharpLib/CHD.cs | 53 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/CHDSharpLib/CHD.cs b/CHDSharpLib/CHD.cs index 45b0fcc..ce7d48d 100644 --- a/CHDSharpLib/CHD.cs +++ b/CHDSharpLib/CHD.cs @@ -394,8 +394,17 @@ out var decompressionOk return new ChdResult(ChdError.Chderrnone, ver, combined ?? computedRawSha1, headerMd5); } - /// + /// + /// Validates a CHD file from a and returns the CHD version plus the + /// SHA1/MD5 hashes stored in its header. + /// + /// A readable, seekable stream positioned at the start of the CHD file. + /// The filename associated with the stream, used only for logging. + /// + /// If true, performs full decompression of every hunk plus SHA1/MD5 hash + /// verification (using up to parallel workers); if false, only the header is + /// validated. + /// /// /// When this method returns, contains the CHD version (1-5), or null if the header was /// invalid. @@ -408,6 +417,14 @@ out var decompressionOk /// When this method returns, contains the MD5 hash from the header, or null if not available /// (V4/V5). /// + /// + /// An optional receiving a + /// report after each decompressed hunk during deep verification. null (default) disables progress reporting. + /// + /// + /// A token to cancel deep verification. + /// is thrown if cancellation is requested while hunks are being decompressed or hashed. + /// /// on success; otherwise an error code describing the failure. public static ChdError CheckFile( Stream s, @@ -663,8 +680,14 @@ public static ChdResult CheckFileWithParent( return new ChdResult(err, ver, sha1, md5); } - /// + /// + /// Fully verifies a (possibly child/differential) CHD by decompressing the whole image and + /// comparing the computed hashes against the values stored in the header, resolving parent + /// references against the CHD at . Also returns the header + /// version and hashes. + /// + /// Path to the CHD file to verify. + /// Path to the parent CHD, or null/empty for a standalone CHD. /// /// When this method returns, contains the CHD version (1-5), or null if the file could not /// be opened. @@ -674,6 +697,14 @@ public static ChdResult CheckFileWithParent( /// available. /// /// When this method returns, contains the MD5 hash from the header, or null if not available. + /// + /// An optional receiving a + /// report after each decompressed hunk. null (default) disables progress reporting. + /// + /// + /// A token to cancel verification. + /// is thrown if cancellation is requested while hunks are being read. + /// /// on success; otherwise an error code describing the failure. public static ChdError CheckFileWithParent( string filename, @@ -809,8 +840,16 @@ public static ChdResult CheckFileWithParent( return new ChdResult(err, ver, sha1, md5); } - /// + /// + /// Verifies a (possibly child) CHD file by decompressing all hunks and comparing hashes, + /// resolving parent references lazily via a callback. Also + /// returns the header version and hashes. + /// + /// Path to the CHD file to verify. + /// + /// A callback that resolves parent CHDs by SHA1/MD5 hash, or null to fail on child + /// CHDs. + /// /// /// When this method returns, contains the CHD version (1-5), or null if the file could not /// be opened. @@ -820,6 +859,8 @@ public static ChdResult CheckFileWithParent( /// available. /// /// When this method returns, contains the MD5 hash from the header, or null if not available. + /// An optional progress reporter, or null. + /// A token to cancel verification. /// on success; otherwise an error code describing the failure. public static ChdError CheckFileWithParent( string filename, From 06da787b9abaf6c3558e1dc6d2ab99bcf8c4b1ce Mon Sep 17 00:00:00 2001 From: Peterson Fernandes Date: Mon, 2 Nov 2026 12:11:02 -0300 Subject: [PATCH 8/9] chore(inspectcode): exclude the vendored codec projects entirely The four vendored ports must stay verbatim for byte-for-byte chdman parity, so inspecting them only invites "fixes" that would break the guarantee. Skip VendoredFlac, VendoredLZMA, VendoredZLib and VendoredZSTD wholesale (projects and sources) with --exclude="Vendored*/**" instead of only their .cs files, and spell the rule out in AGENTS.md and docs/building.md. --- .github/workflows/ci.yml | 2 +- AGENTS.md | 12 +++++++----- docs/building.md | 16 +++++++++------- 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f76cadd..ae79ff5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,7 +100,7 @@ jobs: shell: pwsh run: | $env:PATH = "$env:USERPROFILE\.dotnet\tools;$env:PATH" - jb inspectcode CSharp_CHDSharp.sln --severity=WARNING --format=Sarif --output=inspectcode.sarif --exclude="Vendored*/**/*.cs" --no-updates + jb inspectcode CSharp_CHDSharp.sln --severity=WARNING --format=Sarif --output=inspectcode.sarif --exclude="Vendored*/**" --no-updates - name: Fail on inspection findings shell: pwsh diff --git a/AGENTS.md b/AGENTS.md index 09453cb..2454701 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,11 +47,13 @@ jb inspectcode CSharp_CHDSharp.sln --severity=WARNING # ReSharper inspections - Keep builds warning-free: Meziantou.Analyzer and Roslynator.Analyzers run in every project. - `CHDSharpLib` treats missing XML docs (CS1591) as errors — document every public API addition. - Keep ReSharper inspections clean too: `jb inspectcode CSharp_CHDSharp.sln --severity=WARNING` - must report zero findings. The policy lives in the root `.editorconfig`: the vendored codec - folders are exempt from style inspections and must stay verbatim for `chdman` parity — never - "fix" a warning there, suppress the inspection instead. InspectCode 2026.2 logs a few internal - `AssertionException`s while analyzing `VendoredZSTD` (an analyzer bug, not a finding); the - report is still empty, and `--exclude="Vendored*/**/*.cs"` silences the console. + must report zero findings. The four vendored codec projects - `VendoredFlac`, `VendoredLZMA`, + `VendoredZLib` and `VendoredZSTD` - are excluded from InspectCode entirely + (`--exclude="Vendored*/**"`, projects and sources). They must stay verbatim for byte-for-byte + `chdman` parity: never edit them to satisfy an inspection, and never remove the exclusion. The + same rule lives in the root `.editorconfig` so IDE inspections stay quiet inside those folders. + InspectCode 2026.2 also logs a few internal `AssertionException`s while analyzing + `VendoredZSTD` (an analyzer bug, not a finding); the report is still empty. - Always run `dotnet test` before committing. Run the battle harness for changes touching encoding, decoding, CLI argument parsing, or anything chdman-visible. diff --git a/docs/building.md b/docs/building.md index 8f9b7d3..f941217 100644 --- a/docs/building.md +++ b/docs/building.md @@ -70,16 +70,18 @@ the [JetBrains command-line tools](https://www.jetbrains.com/help/resharper/Insp (`jb`), from the repository root: ```bash -jb inspectcode CSharp_CHDSharp.sln --severity=WARNING --output=inspectcode.xml +# The four vendored codec projects are excluded outright: they are checked in verbatim +# and editing them for style would risk the byte-for-byte chdman parity guarantee. +jb inspectcode CSharp_CHDSharp.sln --severity=WARNING --exclude="Vendored*/**" --output=inspectcode.xml ``` The inspection policy lives in the root `.editorconfig`: -- **The vendored codecs are exempt.** `VendoredZLib`, `VendoredLZMA`, `VendoredFlac` and - `VendoredZSTD` are third-party ports checked in verbatim; byte-for-byte `chdman` parity depends - on them staying untouched, so their style/quality inspections (redundant code, unreachable - branches, naming, …) are disabled there. Never "fix" a finding inside those folders — exempt it - in `.editorconfig` instead. +- **The vendored codecs are excluded from InspectCode.** `VendoredZLib`, `VendoredLZMA`, + `VendoredFlac` and `VendoredZSTD` (projects and sources) are third-party ports checked in + verbatim; byte-for-byte `chdman` parity depends on them staying untouched. They are skipped by + `--exclude="Vendored*/**"` in CI and by `.editorconfig`, and must never be edited to satisfy an + inspection. - **Consumer-facing models are exempt from "never used" inspections.** Public library records (`CHDSharpLib/Models`), the JSON corpus manifest (`CHDSharpTestDataGeneration/Models`) and the WPF data models (`CHDSharpTester/Models`) expose @@ -87,7 +89,7 @@ The inspection policy lives in the root `.editorconfig`: > InspectCode 2026.2 logs internal `AssertionException`s while its nullable-flow analyzer walks > the vendored ZSTD sources. This is a bug in the analyzer, not a finding: the report is still -> empty. Add `--exclude="Vendored*/**/*.cs"` if you want a silent console. +> empty, and the `--exclude="Vendored*/**"` switch also silences the console. --- From 70654d85ec454c88eb4c148e874fbe484083d8a5 Mon Sep 17 00:00:00 2001 From: Peterson Fernandes Date: Mon, 2 Nov 2026 12:11:34 -0300 Subject: [PATCH 9/9] ci: skip dependency review while the repository dependency graph is disabled GitHub returns 404 from the dependency-graph API for this repository and does not expose a way to enable the graph for user-owned repositories, so the review action could only ever fail with "not supported on this repository". Probe the SBOM endpoint first: when the graph is unavailable, emit a warning annotation and skip the review so the check reflects reality. Enabling the graph in Settings > Code security and analysis restores enforcement with no workflow change. --- .github/workflows/dependency-review.yml | 21 +++++++++++++++++++++ AGENTS.md | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 621dd58..3a5f297 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,5 +1,12 @@ # Blocks pull requests that introduce dependencies with known vulnerabilities # or incompatible licenses. Requires the repository dependency graph. +# +# GitHub does not expose an API to enable the dependency graph for user-owned +# repositories; when it is off the review action can only fail with +# "Dependency review is not supported on this repository". Detect that case, +# emit a warning annotation and skip the review so the check is honest instead of +# permanently red. Enable it in Settings > Code security and analysis to turn the +# enforcement back on - no workflow change is needed. name: Dependency Review on: @@ -21,7 +28,21 @@ jobs: with: persist-credentials: false + - name: Check dependency graph availability + id: graph + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + if gh api "repos/${{ github.repository }}/dependency-graph/sbom" >/dev/null 2>&1; then + echo "available=true" >> "$GITHUB_OUTPUT" + else + echo "available=false" >> "$GITHUB_OUTPUT" + echo "::warning title=Dependency graph disabled::The repository dependency graph is not enabled, so dependency review cannot run. Enable it in Settings > Code security and analysis > Dependency graph." + fi + - name: Review dependency changes + if: steps.graph.outputs.available == 'true' uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 with: fail-on-severity: moderate diff --git a/AGENTS.md b/AGENTS.md index 2454701..14dbaf0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -202,7 +202,9 @@ there is no legacy `pages-build-deployment` build any more. - CodeQL (`.github/workflows/codeql.yml`), Dependency Review (`.github/workflows/dependency-review.yml`), and OpenSSF Scorecard (`.github/workflows/scorecard.yml`) run in CI; Dependabot is configured in - `.github/dependabot.yml`. + `.github/dependabot.yml`. Dependency Review self-skips with a warning when the repository + dependency graph is disabled (GitHub exposes no API to enable it for user-owned repos - it is a + Settings > Code security and analysis toggle); the enforcement returns as soon as it is on. - Repository settings: Dependabot alerts/security updates, secret scanning with push protection, private vulnerability reporting, read-only default workflow permissions, and mandatory full-SHA action pinning with GitHub-owned/verified actions only.