FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

CHDStudio/CSOSharp at master · purelogiccode/CHDStudio · GitHub

Repository navigation

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

CSOSharp

CSOSharp is a managed C# library for reading and extracting CSO/CISO (Compressed ISO) files. It supports both the classic CSO v1 (deflate/zlib) format and CSO v2, also known as ZSO (LZ4), and can decode individual blocks, expose the decompressed image as a seekable Stream, or extract the whole ISO to disk.

The library is the CSO extraction engine used by CHD Studio, where it serves as an intermediate step before converting CSO images to CHD with chdman or CHDSharp.

Table of contents

Features

  • CSO v1 (deflate/zlib) and CSO v2/ZSO (LZ4) — both formats are decoded transparently; the header tells you which one you are reading.
  • Block-level access — ReadBlock decompresses a single block at a time, with an overload that writes at a byte offset in your buffer.
  • Seekable stream — OpenStream returns a read-only Stream (including a Span<byte> overload on modern .NET) that decompresses blocks on demand and caches the current block.
  • One-call ISO extraction — ExtractToIso writes the full image to disk with a progress callback and cancellation support.
  • Stream-friendly — open a CSO from a path or from any readable, seekable Stream you own.
  • Stored blocks — uncompressed blocks flagged in the index table are copied verbatim.
  • No native dependencies — pure managed code, cross-platform.

Requirements

Requirement Value
Target frameworks net8.0, net9.0, net10.0
Runtime .NET 8, .NET 9 or .NET 10
Platforms Windows, Linux, macOS (pure managed code)
Dependencies K4os.Compression.LZ4 1.3.8

The package contains a separate assembly for each target framework, so the correct build is selected automatically by NuGet.

Read-only: CSOSharp can open, inspect and extract CSO files. It cannot create, modify or repack them.

Installation

dotnet add package CSOSharp

or with the Package Manager console:

Install-Package CSOSharp

Quick start

using CSOSharp;
using CSOSharp.Models;

var error = CsoFile.Open(@"C:\Games\Game.cso", out var cso);
if (error != CsoError.None || cso is null)
{
    Console.Error.WriteLine($"Could not open the CSO: {error}");
    return;
}

using (cso)
{
    Console.WriteLine(
        $"{cso.Header.UncompressedSize:N0} bytes in {cso.Header.TotalBlocks} blocks " +
        $"({(cso.IsLz4 ? "CSO v2/LZ4" : "CSO v1/deflate")})"
    );

    var result = cso.ExtractToIso(
        @"C:\Games\Game.iso",
        (processed, total) => Console.Write($"\r{processed}/{total} blocks")
    );

    Console.WriteLine();
    Console.WriteLine(result == CsoError.None ? "Extracted." : $"Failed: {result}");
}

Usage examples

Open and inspect a CSO header

CsoFile.Open reads and validates the 24-byte header and the block index table up front, so every header property is available without touching the compressed payload.

using CSOSharp;
using CSOSharp.Models;

var error = CsoFile.Open(csoPath, out var cso);
if (error != CsoError.None || cso is null)
{
    Console.Error.WriteLine($"Could not open '{csoPath}': {error}");
    return;
}

using (cso)
{
    var header = cso.Header;

    Console.WriteLine($"Magic:            0x{header.Magic:X8} (valid: {header.IsValid})");
    Console.WriteLine($"Version:          {header.Version} ({(cso.IsLz4 ? "LZ4" : "deflate")})");
    Console.WriteLine($"Block size:       {header.BlockSize:N0} bytes");
    Console.WriteLine($"Total blocks:     {header.TotalBlocks:N0}");
    Console.WriteLine($"Uncompressed size:{header.UncompressedSize:N0} bytes");
    Console.WriteLine($"Index shift:      {header.IndexOffsetShift} bits");
}

The high bit of an index entry is a version-dependent flag: in CSO v1 it marks a block stored uncompressed, while in CSO v2 it marks an LZ4-compressed block. The remaining 31 bits are the file offset, left-shifted by IndexOffsetShift. CSOSharp handles this for you.

Read individual blocks

Each block decompresses to exactly Header.BlockSize bytes (typically 2,048). Use the returned bytesRead value rather than assuming the buffer was filled.

using CSOSharp;
using CSOSharp.Models;

var error = CsoFile.Open(csoPath, out var cso);
if (error != CsoError.None || cso is null)
    return;

using (cso)
{
    var buffer = new byte[cso.Header.BlockSize];

    for (uint i = 0; i < cso.Header.TotalBlocks; i++)
    {
        error = cso.ReadBlock(i, buffer, out var bytesRead);
        if (error != CsoError.None)
        {
            Console.Error.WriteLine($"Block {i} failed: {error}");
            break;
        }

        // Process buffer[0..bytesRead]
    }
}

The offset overload lets you fill a larger buffer without a copy:

var batch = new byte[cso.Header.BlockSize * 16];
var offset = 0;

for (uint i = 0; i < 16; i++)
{
    error = cso.ReadBlock(i, batch, offset, out var bytesRead);
    if (error != CsoError.None)
        break;

    offset += bytesRead;
}

Stream the decompressed ISO

OpenStream returns a read-only, seekable CsoStream over the decompressed image. Blocks are decoded on demand and the most recently used block is cached, so sequential reads do not re-decompress the same block.

using CSOSharp;
using CSOSharp.Models;

var error = CsoFile.Open(csoPath, out var cso);
if (error != CsoError.None || cso is null)
    return;

using (cso)
using (var iso = cso.OpenStream())
{
    Console.WriteLine($"ISO length: {iso.Length:N0} bytes");

    // Seek to a sector and read it.
    const int sectorSize = 2048;
    iso.Position = 1_000L * sectorSize;

    var sector = new byte[sectorSize];
    var read = iso.Read(sector, 0, sector.Length);
    Console.WriteLine($"Read {read} bytes at sector 1000.");
}

On .NET, the Read(Span<byte>) override is also available:

Span<byte> header = stackalloc byte[2048];
var read = iso.Read(header);

CsoStream is read-only: Write and SetLength throw NotSupportedException. A block that cannot be decompressed surfaces as an IOException from Read.

Extract a CSO to ISO with progress and cancellation

ExtractToIso decompresses every block and writes the ISO in one call. The optional callback reports (processedBlocks, totalBlocks), and the cancellation token is checked before each block.

using CSOSharp;
using CSOSharp.Models;

var error = CsoFile.Open(csoPath, out var cso);
if (error != CsoError.None || cso is null)
    return;

using (cso)
using (var cts = new CancellationTokenSource())
{
    var outputPath = Path.ChangeExtension(csoPath, ".iso");

    try
    {
        error = cso.ExtractToIso(
            outputPath,
            (processed, total) =>
            {
                var percent = total == 0 ? 100 : (processed * 100.0) / total;
                Console.Write($"\r{percent,6:0.0}%");
            },
            cts.Token
        );
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Cancelled - delete the partial ISO if you do not need it.");
        return;
    }

    Console.WriteLine();
    Console.WriteLine(error == CsoError.None ? $"Wrote {outputPath}" : $"Failed: {error}");
}

ExtractToIso writes the ISO first. If decompression fails partway, a partial file may remain; delete it when the returned error is not CsoError.None.

Open a CSO from a stream

Both Open overloads require a readable, seekable stream. The ownsStream flag decides whether disposing the CsoFile also disposes the stream.

using CSOSharp;
using CSOSharp.Models;

using var file = File.OpenRead(csoPath);
var error = CsoFile.Open(file, ownsStream: false, out var cso);

if (error != CsoError.None || cso is null)
{
    Console.Error.WriteLine($"Could not open the CSO: {error}");
    return;
}

using (cso)
{
    Console.WriteLine($"{cso.Header.TotalBlocks} blocks");
}

// The stream is still usable here because ownsStream was false.

You can also open a CSO that is already in memory:

var bytes = await File.ReadAllBytesAsync(csoPath);
using var memory = new MemoryStream(bytes);
var error = CsoFile.Open(memory, ownsStream: false, out var cso);

Copy the ISO data to another stream

CsoStream is an ordinary Stream, so it composes with the rest of the BCL — for example CopyToAsync:

using CSOSharp;

CsoFile.Open(csoPath, out var cso);
using (cso)
using (var iso = cso.OpenStream())
using (var output = File.Create(isoPath))
{
    await iso.CopyToAsync(output);
}

Error handling

CsoFile.Open, ReadBlock and ExtractToIso return a CsoError value instead of throwing for data-related problems. Corrupt headers, unsupported versions, truncated index tables and decompression failures are all reported through the enum, which keeps batch processing straightforward.

var error = CsoFile.Open(path, out var cso);

switch (error)
{
    case CsoError.None:
        // cso is ready to use
        break;
    case CsoError.FileNotFound:
        Console.Error.WriteLine("The file does not exist.");
        break;
    case CsoError.InvalidHeader:
        Console.Error.WriteLine("Not a CSO/CISO file (bad or missing magic).");
        break;
    case CsoError.UnsupportedVersion:
        Console.Error.WriteLine("Only CSO v1 and CSO v2/ZSO are supported.");
        break;
    default:
        Console.Error.WriteLine($"Could not open the CSO: {error}");
        break;
}
CsoError Value Meaning
None 0 Success.
InvalidHeader 1 Missing or invalid CISO magic/header.
UnsupportedVersion 2 The header version is not 1 or 2.
FileNotFound 3 The file does not exist or could not be opened.
IoError 4 An I/O error occurred (also returned for non-readable/non-seekable streams).
CorruptIndex 5 The index table is truncated or references invalid offsets.
DecompressionError 6 A block could not be decompressed, or the target buffer was smaller than BlockSize.
BlockOutOfRange 7 The requested block index is past Header.TotalBlocks - 1.
InvalidBlockSize 8 The header declares a zero block size.

Notes:

  • ReadBlock returns BlockOutOfRange for an index past the end and DecompressionError when the destination buffer is too small (the buffer must hold BlockSize bytes starting at offset).
  • ReadBlock and OpenStream on a disposed instance return CsoError.IoError and throw ObjectDisposedException respectively.
  • CsoStream.Read throws ObjectDisposedException after disposal, IOException when a block fails to decompress, and ArgumentOutOfRangeException for an invalid buffer offset/count.
  • ExtractToIso propagates OperationCanceledException when the token is cancelled, returns InvalidHeader for a header that declares zero uncompressed bytes (instead of leaving an empty ISO behind) and never writes past the declared uncompressed size.
  • Open returns CorruptIndex for an index table larger than the file itself before allocating it, so a header claiming a huge block count cannot exhaust memory.

API reference

All types live in the CSOSharp namespace; the model types live in CSOSharp.Models.

CsoFile

The entry point. Represents an opened CSO container and owns the underlying stream when it was opened from a path.

Member Description
static CsoError Open(string path, out CsoFile? cso) Opens a CSO from disk. The returned instance owns the file stream.
static CsoError Open(Stream stream, bool ownsStream, out CsoFile? cso) Opens a CSO from a readable, seekable stream. ownsStream controls whether the stream is disposed with the instance.
CsoHeader Header Parsed CSO header: magic, header size, uncompressed size, block size, version, index shift and total block count.
bool IsLz4 true when the file uses CSO v2/ZSO (LZ4) compression.
bool IsDeflate true when the file uses CSO v1 (deflate/zlib) compression.
CsoError ReadBlock(uint blockIndex, byte[] buffer, out int bytesRead) Reads and decompresses one block. The buffer must be at least Header.BlockSize bytes.
CsoError ReadBlock(uint blockIndex, byte[] buffer, int offset, out int bytesRead) Reads one block into buffer at offset.
CsoStream OpenStream() Creates a read-only, seekable stream over the decompressed ISO data.
CsoError ExtractToIso(string outputPath, Action<uint, uint>? progress = null, CancellationToken cancellationToken = default) Decompresses the whole CSO and writes the ISO to outputPath, reporting (processedBlocks, totalBlocks).
void Dispose() Releases the stream when the instance owns it. Safe to call multiple times.

CsoStream

A read-only Stream over the decompressed ISO inside a CSO file.

Member Description
bool CanRead / bool CanSeek Always true.
bool CanWrite Always false.
long Length Total uncompressed ISO size in bytes (Header.UncompressedSize).
long Position Current absolute position. Assigning a negative value throws ArgumentOutOfRangeException.
int Read(byte[] buffer, int offset, int count) Reads up to count bytes, decompressing blocks on demand.
int Read(Span<byte> buffer) Span overload (available on .NET 8+).
long Seek(long offset, SeekOrigin origin) Seeks within the decompressed image. Seeking before the start throws IOException.
void Flush() No-op (read-only stream).
void SetLength(long value) / void Write(...) Throw NotSupportedException.

Models

Type Description
CsoHeader Read-only struct with the parsed header: Magic, HeaderSize, UncompressedSize, BlockSize, Version, IndexOffsetShift, TotalBlocks, IsValid, IsV1, IsV2, plus the MagicValue and ExpectedHeaderSize constants.
CsoError Result/error enum returned by Open, ReadBlock and ExtractToIso (see the table above).

Supported CSO variants

CSO files come from several compressors, and CSOSharp reads all the common layouts:

  • CSO v1 — deflate blocks, either zlib-wrapped (recognized by the 0x78 header and skipped) or raw deflate. Bit 31 of an index entry marks a stored (uncompressed) block.
  • CSO v2 / ZSO — a block is compressed only when its stored length is smaller than a full block; bit 31 then selects LZ4 (set) or deflate (clear). Full-size blocks are stored verbatim. LZ4 is decoded with K4os.Compression.LZ4.
  • Stored blocks — copied verbatim, zero-padded when a short final block is stored.
  • Shifted index tables — entries are left-shifted by the header's IndexOffsetShift, which is common in large images to address offsets beyond 4 GB.
  • Truncated files — a short header or index table is reported as InvalidHeader or CorruptIndex rather than crashing.

How it works

A CSO file starts with a 24-byte header:

Offset Size Field
0 4 Magic CISO (0x4F534943, little-endian)
4 4 Header size (24)
8 8 Uncompressed ISO size in bytes
16 4 Block size (typically 2,048)
20 1 Version (1 = deflate, 2 = LZ4)
21 1 Index offset shift
22 2 Reserved

The header is followed by a TotalBlocks + 1 entry index table of 32-bit little-endian values. For a block at index i:

  1. The file offset is (entry[i] & 0x7FFFFFFF) << IndexOffsetShift.
  2. The stored length is the difference between the offsets of entries i + 1 and i.
  3. How the block is read depends on the version:
    • v1: bit 31 set means the block is stored and copied verbatim; otherwise it is deflate.
    • v2: a stored length smaller than BlockSize means the block is compressed — bit 31 set means LZ4, bit 31 clear means deflate; a length of at least BlockSize means the block is stored.
  4. An empty span (next == current) represents a block of zeroes.

CSOSharp validates the magic, block size and version, reads the whole index table up front, then decompresses blocks on demand by seeking to the computed offset.

Building from source

The library lives in the CHD Studio repository under CSOSharp/.

git clone https://github.com/purelogiccode/CHDStudio.git
cd CHDStudio
dotnet build CSOSharp/CSOSharp.csproj -c Release
dotnet pack CSOSharp/CSOSharp.csproj -c Release -o artifacts

The test suite for the library lives in CHDStudio.Tests/:

dotnet test CHDStudio.Tests/CHDStudio.Tests.csproj -c Release --filter "FullyQualifiedName~Cso"

Version history

1.0.1

  • CSO v2/ZSO blocks are classified the way the format defines them: compressed only when smaller than a full block, with bit 31 selecting LZ4 over deflate. Previously v1 semantics were applied to v2, so LZ4 blocks were returned as raw compressed bytes and stored or deflate blocks were pushed through the LZ4 decoder.
  • A short final stored block is zero-padded instead of failing, and a stored block is read exactly up to the block size.
  • A header or index table that does not fit in the file is rejected (InvalidHeader / CorruptIndex) before any allocation.
  • The zlib wrapper of a v1 deflate block is detected from the real CMF/FLG header (compression method, window size, checksum) instead of the first byte alone.
  • ExtractToIso reports a header with zero uncompressed bytes as InvalidHeader and truncates output at the declared size.

1.0.0

  • First published release: CSO v1 (deflate/zlib) and CSO v2/ZSO (LZ4) reading, block-level access, a seekable decompressing stream and one-call ISO extraction, targeting net8.0, net9.0 and net10.0.

License

CSOSharp is released under the MIT license.


Back | FazBrowse Home | New Git URL