6. Extraction & Verification (Technical)
This page covers the internals of the two CHD-consuming workflows. References are to BatchConvertToCHD/MainWindow.xaml.cs unless noted.
6.1 Extraction
Entry & batch loop
StartExtractionButton_ClickAsync (:693) validates paths, reads options (subfolders, delete original), and calls PerformBatchExtractionAsync (:1355), which runs CheckDiskSpace (extraction mode: warns when output free space < total input size) and then loops per file with ExtractChdAsync (:2123).
Format selection
GetSelectedExtractCommandAsync (:2325):
| UI choice | chdman command |
|---|---|
| Auto | DetectChdExtractCommandAsync (:2370) — metadata scan |
| CD (.cue) | extractcd |
| DVD (.iso) | extractdvd |
| GDI (.gdi) | extractcd |
| HDD (.img) | extracthd |
Metadata detection scans the CHD’s metadata entries (CHDSharp): dvd → extractdvd; gd-rom → extractcd; hard disk/hdd → extracthd; default extractcd. Output extension: explicit per radio button, or for Auto derived from the detected command — and when Auto yields extractcd plus the metadata contains gd-rom (IsGdiChdAsync, :2338), the extension becomes .gdi instead of .cue.
Output path
The subfolder structure of the input is preserved under the output folder (GetSafeRelativePath). Nothing existing is deleted to make room: an extraction whose output would land on files of the same name is diverted into a subfolder instead (see below).
Extracting into the source folder
The output folder may be the same as the input folder. Extraction is the workflow where that needs care, because the output takes the CHD’s base name: extracting Game.chd produces Game.cue plus its track files, so left alone it would replace a cue/bin set kept beside the CHD.
Rather than overwrite those files, or stop to ask, the extraction is diverted. When any file it is about to write already exists, the whole set goes into a subfolder named after the disc — Game\Game.cue, Game\Game (Track 1).bin and so on — and the existing files are left untouched. PathUtils.ReserveFreeSubdirectory chooses the name, stepping to Game (2), Game (3) and so on when something already occupies it.
The diversion happens only when there is a real clash. Extractions with nothing in their way still land directly in the output folder, so the layout is unchanged for everyone else, and no setting controls this. One line in the log says where the files went.
Two properties make it safe to do without asking:
- A descriptor’s
FILEentries are relative and the track files travel with it, so a diverted.cue/.gdiset stays valid with no rewriting. - The clash is tested after extraction into the temp directory but before anything is moved, so the decision uses the real output names rather than a guess at the extension.
Single-file extraction (DVD/HDD)
ExtractChdToSingleFile (:2233): a FileStream is created with FileMode.Create, and the CHD is streamed out in 4 MB buffers with a per-chunk token.ThrowIfCancellationRequested(). Cancellation deletes the partially extracted file.
Multi-track extraction (CD/GDI)
ExtractChdTracksToDirectory (:2252):
- Creates
_extract_temp_<guid>inside the target directory. - Calls
chd.ExtractToDirectory(tempExtractDir, baseFileName)(CHDSharp). «««< HEAD - Picks the destination: the target dir, or a fresh subfolder named after the disc when any extracted file would clash with something already there (
ReserveFreeSubdirectory). Then moves each extracted file into it. - On success the temp dir is deleted; on failure the temp dir is kept and a warning logs the number of remaining files (“Partial extraction: N file(s) remain in temp directory: …”) so the user can inspect/clean up.
-
Moves go through
RetryingFileOperations.TryMoveAsync(retry with backoff, ~45 s) so transient locks (antivirus/indexer) don’t abort the whole disc extraction; a move that still fails after retries throws and the partial-extraction path handles the rest. TheTryDeleteAsyncon the destination remains only as a guard against a file appearing between the clash test and the move — after step 3 the destination is expected to be free. - Moves each extracted file into the target dir, overwriting existing files.
- On success the temp dir is deleted; on failure the leftover files are removed best-effort with a single-shot delete per file (deliberately not the ~45 s retrying delete, so a locked file cannot stall the whole batch), the temp dir is then deleted, and only what truly remains is logged as a warning (“Partial extraction: N file(s) remain in temp directory: …”). A
Debuglog records how many leftovers were cleaned up. - Moves and destination-deletes go through
RetryingFileOperations.TryMoveAsync/TryDeleteAsync(retry with backoff, ~45 s) so transient locks (antivirus/indexer) don’t abort the whole disc extraction; a move that still fails after retries throws and the partial-extraction path handles the rest.62504b8aa71f316c2dbf0d22e648ba6223160110
CHD open failures
ChdFile.Open errors are logged with the CHDSharp message and the file is marked failed; the batch continues. Typical messages: “Not a valid CHD file” (bad magic), “Invalid or corrupt data” (structure broken), “Cannot open file” (locked/unreadable). These are user-data conditions — the app never crashes on them and they are excluded from bug reports (see Bug Reporting System).
Decompression failures and the chdman fallback
When CHDSharp fails to decode a hunk during extraction (“Failed to read hunk N: Chderrdecompressionerror”), the error is mapped through GetChdExtractionErrorMessage (:2950) into a user-friendly message, and the extraction is retried with chdman (TryExtractWithChdmanAsync, :2982):
- chdman runs the user’s selected command (
extractcd/extractdvd/extracthd,-fto force overwrite;extractcdalso pins the bin name with-ob). - If the CHD carries no CD/DVD/HDD metadata (
IsAvChdAsync,:3042) it is an A/V (laserdisc) CHD:extractcdcannot handle it, so chdman is retried withextractld(writes an.avi, MAME 0.285+) and thenextractraw(raw dump) for older chdman builds. - On failure, truncated outputs are deleted; on success the file is marked extracted and the batch continues normally (including the “delete original” option).
The CHDSharp failure itself is still reported as a bug — the CHDSharp maintainer wants extraction failures to reach the bug API (see Bug Reporting System); only chdman-side failures are filtered out there.
6.2 Verification
Entry & batch loop
StartVerificationButton_ClickAsync (:1110) reads the move options, creates the Success/Failed folders up front when requested (:2022–2030), and calls PerformBatchVerificationAsync (:2011).
VerifyChdAsync
VerifyChdAsync (:3020) opens the file read-only and calls Chd.CheckFile(stream, fileName, true) (CHDSharp, in-process — no chdman process). On success it logs V{version} — SHA1: {hex}; failures log result.Error.GetMessage() or the exception message. The per-file read speed is sampled via the read performance counter.
Moving verified files
MoveVerifiedFileAsync (:2076):
- Destination:
inputFolder\SuccessorinputFolder\Failed; with subfolder search the relative directory is preserved under the target folder. - Existing destination files are deleted with
RetryingFileOperations.TryDeleteAsync(result checked — a locked destination fails fast with a clear error instead of a misleading move failure). - The move uses
RetryingFileOperations.TryMoveAsync(10 attempts, backoff 500 ms → 8 s, ~45 s total) because the freshly verified file may still be held by antivirus or the indexer. - On persistent failure, the exception is logged and reported via
ReportBugAsync(“Failed to move file …”), but the batch continues.
Scan exclusions
The verification and extraction file lists exclude anything under a first-level Success or Failed subfolder when recursive search is on (:884–896, :941–952), so organized output isn’t reprocessed.
6.3 Startup & Shutdown Housekeeping
- Leftover temp directories from crashed sessions are deleted at startup:
CleanupLeftoverTempDirectories(:304) scansPathUtils.GetPossibleTempBasePaths()(system temp + any existingX:\BatchConvertToCHD_Tempfolders on fixed drives) forBatchConvertToCHD_Temp_*entries. - Legacy files next to the exe are removed by
LegacyCleanupService(logs,Resources,Screenshotfolders;maxcso.exe,psxpackager.exe). - On
Dispose(:3552) the app unregisters the F8 hotkey, cancels the operation token, disposes services, and callsKillOrphanedProcesses(:3579) to kill leftoverchdman/7zaprocesses before exiting.