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..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 @@ -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/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/.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..f7acd3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -################################################################################ +################################################################################ # This .gitignore file was automatically created by Microsoft(R) Visual Studio. ################################################################################ @@ -17,3 +17,8 @@ TestResults/ # Release packaging / wiki staging artifacts/ + +# DocFX generated API metadata and rendered site +docfx/*.yml +docfx/.manifest +_site/ diff --git a/AGENTS.md b/AGENTS.md index a98b4a7..14dbaf0 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 @@ -46,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. @@ -59,8 +62,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 ===` @@ -116,8 +124,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 @@ -140,8 +148,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**: @@ -170,27 +178,33 @@ 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 - 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. 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/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/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/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/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/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/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/CHD.cs b/CHDSharpLib/CHD.cs index 076a3c9..ce7d48d 100644 --- a/CHDSharpLib/CHD.cs +++ b/CHDSharpLib/CHD.cs @@ -394,8 +394,10 @@ 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. /// @@ -678,8 +680,12 @@ 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. /// @@ -834,10 +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. + /// + /// 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. @@ -1119,7 +1131,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. 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/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/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/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/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/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/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/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/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/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 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/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..33b542a 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/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. 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..f941217 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 @@ -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. --- @@ -183,7 +185,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 +195,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 +232,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/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: 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 +}