10. Embedded Libraries
The solution ships five in-house libraries that replace external tools (maxcso, psxpackager), add CloneCD support, and cover Alcohol 120% and UltraISO images. The app references them as project references. All five are published as NuGet packages for outside consumers — PBPSharp (https://www.nuget.org/packages/PBPSharp, 1.1.3), CSOSharp (https://www.nuget.org/packages/CSOSharp, 1.0.1), CCDSharp (https://www.nuget.org/packages/CCDSharp, 1.0.1), MDSSharp (https://www.nuget.org/packages/MDSSharp, 1.2.0) and ISZSharp (https://www.nuget.org/packages/ISZSharp, 1.0.2) — and they all multi-target net8.0;net9.0;net10.0, each shipping its XML docs, README and icon; releases are manual (see the repository’s AGENTS.md). All five expose internals to CHDStudio.Tests via InternalsVisibleTo. A sixth in-house library, CHDSharp, is consumed as a NuGet package and is covered in §10.4.
| Library | Purpose | Replaces |
|---|---|---|
| CCDSharp | CloneCD .ccd/.img/.sub parsing + CUE/BIN conversion | — (new capability) |
| CSOSharp | CSO/CISO decompression (deflate/zlib + LZ4) | maxcso.exe |
| PBPSharp | PlayStation PBP extraction + SFO/TOC parsing | psxpackager.exe |
| MDSSharp | Alcohol 120% .mds/.mdf parsing, subchannel stripping, split-volume joining, cue writing | — (new capability) |
| ISZSharp | UltraISO ISZ decompression back to the plain image | — (new capability) |
10.1 CCDSharp
Purpose: read CloneCD disc-image sets (.ccd descriptor + .img data + optional .sub subchannel) and convert them to CUE/BIN for chdman.
- Main type:
CcdConverter—Parse(inputFile)returns a parsed disc model (DiscImagewithImgFilePath, subchannel info, track table);ConvertToCueBin(inputFile, tempCuePath)writes the CUE/BIN pair. - Integration:
ProcessCcdFileForConversionAsync(MainWindow.axaml.cs:1702) parses the.ccd, converts to CUE/BIN in a temp dir, then converts the cue with chdman. On success the.ccd/.img/.sub/.cdtset is deleted when “delete originals” is enabled. - Archive extractions skip
.imgfiles that belong to a.ccdset to avoid double conversion (MainWindow.axaml.cs:1562–1573). - Failure messages are prefixed
"CCDSharp: Conversion error"and are excluded from bug reports. - Reference sources live under
References/(ccd2cue-master,ccd2iso-main,myccd2cue-main) — third-party material used to build the library, not part of the build. - Testing note: the test project does not reference CCDSharp, so there are currently no CCDSharp unit tests (see Testing).
10.2 CSOSharp
Purpose: read and decompress CISO (Compressed ISO, .cso) images.
- Main type:
CsoFile—Open(path/stream, out CsoFile)returns aCsoError; exposesUncompressedSize, block metadata,ReadBlock,ExtractToIso(path, progress?, token), and a seekableCsoStreamimplementing the stream contract. - Supports v1 and v2 headers, deflate/zlib and LZ4 compression (
K4os.Compression.LZ4dependency). Block flags are version-dependent: v1 marks stored blocks with index bit 31; v2 decides by stored length (smaller than a full block = compressed) and uses bit 31 to select LZ4 over deflate. A short final stored block is zero-padded. - Integration:
ArchiveService.ExtractCsoAsync(Services/ArchiveService.cs:52) decompresses to a temp ISO for the conversion pipeline. - Error enum:
CsoError { None, FileNotFound, InvalidHeader, UnsupportedVersion, InvalidBlockSize, ... }. - Tests:
CsoFileTests,CsoStreamTests,CsoHeaderTests, plus byte-for-byte integration tests against real.cso/.isopairs (CsoFileIntegrationTests).
10.3 PBPSharp
Purpose: parse PlayStation PBP (PSP/PSX eboot) containers and extract PlayStation disc images to CUE/BIN.
- Main type:
PbpFile—Open(path, out PbpFile)/Open(stream, ownsStream, out PbpFile); propertiesHeader,SfoData,Discs(IReadOnlyList<PbpDiscInfo>),IsMultiDisc,Title,DiscId,Category. - Header: magic
0x50425000, 40-byte header with offsets for SFO/ICON0/ICON1/PIC0/PIC1/SND0/DATA.PSP/DATA.PSAR. - Disc detection: PSAR header
PSISOIMG0000→ single disc;PSTITLEIMG000000→ multi-disc (reads 5 position slots at +0x200); anything else →PbpError.InvalidPsarHeader(the app treats this as “not a PlayStation disc image — PSP application, unsupported variant, or corrupt file” and skips informatively). The four “template” DWORDs of thePSTITLEIMGheader (fixed values in popstation/PSX2PSP/iPoPS output) are not validated: pop-fe writes zeros there and its multi-disc PBPs parse from the position table alone. PbpDiscInfo—ReadBlock,ExtractTo(stream, progress?, token),ExtractToBinCue(binPath, cuePath?, progress?, token); TOC parsed from the PSAR TOC (A0/A1/A2 markers, BCD track numbers, best effort — a bad TOC never aborts extraction). A disc container whose header parsed but that carries no ISO index entries throwsNoIsoIndexException(public, derives fromException) so callers can report the likely cause: a truncated or incomplete download.- Index entries (32 bytes at PSAR+0x4000, data at PSAR+0x100000) are read in both authoring layouts: the popstation/PSX2PSP/iPoPS layout writes the block size as a 32-bit int at bytes 4–7, the pop-fe layout writes a 16-bit size at bytes 4–5, a stored-flag byte at 6 (bit 0 = uncompressed block) and a SHA-1 at 8–23. Block offsets use 64-bit math so multi-gigabyte files cannot overflow. Corrupt entries are rejected with
InvalidDataException(oversized lengths, stored blocks larger than a full 16-sector block). - Block decompression uses SharpZipLib’s raw
Inflater(the same decompressor the popstation reference implementation uses for PSAR blocks,windowBits −15); a block that fails raw inflation is retried as a zlib-wrapped stream (2-byte header + Adler-32, as written by tools usingzlib.compress). Blocks flagged stored, or exactly one full block (16 × 0x930) in size, are copied verbatim. A failed block surfaces asPbpError.DecompressionError. CueSheetWriter.GenerateCueSheet(binFileName, tocEntries)— emitsFILE ... BINARY,TRACK nn MODE2/2352(data) /AUDIO(audio), withINDEX 00for audio tracks computed as track start minus 150-frame lead-in (clamped ≥ 0).- SFO model:
SfoData(magic0x46535000;GetString/GetUInt32;Size— the total SFO section size derived from the largest data entry, populated on parse; staticKeyswithBOOTABLE,CATEGORY,DISC_ID,DISC_VERSION,LICENSE,PARENTAL_LEVEL,PSP_SYSTEM_VER,REGION,TITLE),SfoEntry(formats 0x0204 string / 0x0404 uint32),TocEntry,TrackType { Data = 0x41, Audio = 0x01 }. - SFO parsing is best effort: a missing or corrupt PARAM.SFO (bad magic, malformed table, offsets beyond EOF) leaves
Title/DiscIdnull with emptyEntriesinstead of failingOpen— none of the reference tools read the SFO when extracting disc images. PbpErrorenum:None=0, InvalidHeader=1, FileNotFound=2, IoError=3, CorruptFile=4, InvalidPsarHeader=5, DiscOutOfRange=6, ResourceNotFound=7, DecompressionError=8, TruncatedPsar=9, InvalidSfo=10.TruncatedPsaris returned when the PSAR container parses but no ISO index follows (seeNoIsoIndexException), when an index points past the end of the file, or when a block read hits end-of-stream — any file that ends before the data it declares.InvalidSfois retained for API compatibility but is no longer returned byOpen(SFO problems are tolerated). The app treatsTruncatedPsarandInvalidPsarHeaderas user-data conditions (“truncated or incomplete — re-download”) that are logged without a bug report.- Block decompression uses SharpZipLib’s raw
Inflater(the same decompressor the popstation reference implementation uses for PSAR blocks), which tolerates a few streams the stricter .NETDeflateStreamrejects; a failed block surfaces asPbpError.DecompressionError. - Integration:
ExtractPbpToCueBinAsync(MainWindow.axaml.cs:2959) — multi-disc PBPs produce"{name} - Disc N.bin/.cue"sets; the result (PbpExtractionResult) carriesErrorCode+ a human-readableErrorso the caller can distinguish skippable conditions from real failures. - Tests:
PbpFileTests,PbpHeaderTests,SfoDataTests,SfoEntryTests,TocEntryTests,CueSheetWriterTests, plus real-file integration tests (PbpFileIntegrationTests).
10.4 CHDSharp (NuGet)
Purpose: pure C# CHD (Compressed Hunks of Data) reading, verification, extraction, and creation — the engine behind the app’s extraction and verification tabs.
- Consumed as a NuGet package (
CHDSharpv1.4.3), not a project reference. There is no bundled CHDSharp CLI: verification and extraction use the library directly, and creation goes through the in-process encoderServices/ChdSharpEncoderService.cs(see Conversion Pipeline §5.3). - Capabilities used by the app: CHD V1–V5, all 10 compression codecs (zlib, lzma, huffman, flac, zstd, avhu + CD variants), parent/child chaining, parallel verification, full CHD creation (
createcd/createdvd/createhd/createraw/createld) with output that is byte-identical tochdman, laserdisc extraction (ExtractLaserDisc, AVI), per-hunk encode progress (ChdEncodeOptions.HunkCompleted), per-hunk reader progress (IProgress<ChdProgress>), per-track/whole-image hashing (Chd.ComputeHashes, SHA-1/CRC-32/XXH3), and sequential read-ahead (ChdFile.ConfigureReadAhead). - The byte-parity claim was validated by the (since-removed)
CHDBattleTestbattleground project — see Testing §11.6 — which reported zero mismatches againstchdman0.289 across decode, encode, and cross-verification battles on a 56-disc corpus. - In the conversion pipeline CHDSharp is the encoder for every file on Linux/macOS and the automatic fallback on Windows: bundled
chdmanis the primary encoder there, and a file chdman cannot convert (or when chdman is missing) is encoded byChdSharpEncoderService.Encode— see Conversion Pipeline §5.3 and Services Reference §7.10. - A/V (laserdisc) CHDs are extracted in-process with
ChdEncoder.ExtractLaserDisc; chdman remains the fallback for CHDs the library cannot decode — see Extraction & Verification. The Explorer’s Image Info report and the verification checksum report are built directly from the header, metadata, track and map APIs (Chd.ReadHeader,ChdFile.GetMetadata,Tracks,GetHunkCodecName).
10.5 MDSSharp
Purpose: turn an Alcohol 120% or Daemon Tools image (.mds descriptor + .mdf data, split .i00/.i01 volumes, or a single-file .mdx container) into something the encoder can read.
- Main types:
MdsParser(IsMdsFile,Parse),MdsMedium,MdsDisc/MdsTrack(parsed model with medium type, sector-size classification, pregap/length fields and, for v2, theStoredDataSectorsfooter count),MdsInputPreparer(PrepareAsync→ cue / DVD image / failure;StripSubchannelAsync,WriteCueAsync,FormatMsf),MdsV2DataDecoder(v2/MDX track-data decode),MdxCrypto(descriptor/data-header decryption), andSplitImageJoiner(TryGetVolumeSet,JoinAsync,GetTotalBytesfor.001/.i00volume sets). - Reads the medium type, the track extra blocks (pregap/length) and the footer blocks that name the data files (single-byte or UTF-16), so renamed and multi-file descriptors resolve without guessing; several declared files are joined in order.
- Track modes follow libmirage’s reverse engineering (low nibble, folded by 8): audio, Mode 1 and the Mode 2 forms; CD media with 2048-byte sectors becomes a
MODE1/2048cue, DVD media stays a direct image. - Pregaps the data file does not contain are rebuilt as zeros into a
.pregap.binsoINDEX 00can be written; pregaps already in the file are referenced in place. For MDS v2 the footer’strack_data_lengthrecords whether each track’s pregap is stored (libMirage validates it as the extra block’s length plus the pregap), so the layout is resolved per track: only the missing, representable pregaps are materialized and a mixed image still gets everyINDEX 00right. The first track’s pregap before LBA 0 is never materialized, matching libMirage’s NULL pregap for track 1. - MDS v2 / MDX (Daemon Tools): the descriptor is AES-256-CBC decrypted (password-less key derived from the file salt) and zlib-inflated; track data marked compressed is inflated through a per-footer compression table (stored / RLE / deflate) and track data marked encrypted is decrypted with AES-256-LRW using the key data from the descriptor (TAGES images decode without a password, password-protected images need one). A
.mdxis a single-file container holding the whole image; the parser and decoder read only the regions they need, so multi-gigabyte containers never load into memory. - Each footer’s
track_data_length(the sectors actually stored) is authoritative rather than the extra block’s logical length, so a pregap stored in the data file is decoded rather than truncated; a track split across several data files decodes every footer in order, with only the first fragment starting at the track’s start offset. - The three preparation shapes (plain 2352, subchannel strip, ISO-as-DVD) plus the cooked-CD, pregap and v2/MDX paths are documented in Utilities Reference §8.12.
- Integration:
ProcessMdsFileForConversionAsyncprepares the work set, then the conversion funnel takes the cue (or DVD image). - Tests:
MdsTests.cs,MdsV2Tests.cs(real mdsx v2/MDX/encrypted fixtures decoded byte-for-byte),SplitImageJoinerTests.cs.
10.6 ISZSharp
Purpose: decompress UltraISO .isz images back to the plain images they were made from, per EZB Systems’ ISZ File Format Specification 1.00 plus the real-file behaviours the specification omits.
- Main types:
IszHeader(48/64-byte header withTryReadvalidation and the extended UltraISO checksum fields),IszDecoder(TryReadHeaderAsync,DecodeAsync,GetSegmentPath,GetDecodedFileName,ReadChunkEntry),IszSegment/IszChunkType/IszDecodeResult. - Supports whole and multi-segment images (the spec’s
.i01/.i02naming and the.part01.isz/.part001.iszforms), zlib / bzip2 / stored / zero-elided chunks, de-obfuscates the segment and chunk tables (XOR withB6 8C A5 DE), restores theBZhheader stripped from bzip2 chunks, and decodes images whose header declares no chunk table (one raw run). - Validates UltraISO’s CRC32 of the restored image when the 64-byte header carries one; a mismatch deletes the output and fails like a size shortfall. Encryption is refused by name (AES-128/192/256, password), later segments are matched by volume serial number, and truncation or damaged tables are reported rather than guessed at. A decode that fails after writing starts deletes its partial output.
- Integration:
ResolveIszAsyncdecodes the ISZ to a temp image, which is then classified and converted like any other image. - Tests:
IszHeaderTests.cs,IszDecoderTests.cs.