| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
parent directory.. | ||||
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.
| 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.
dotnet add package CSOSharpor with the Package Manager console:
Install-Package CSOSharpusing 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}");
}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.
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;
}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.
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.
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);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);
}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:
All types live in the CSOSharp namespace; the model types live in CSOSharp.Models.
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. |
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. |
| 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). |
CSO files come from several compressors, and CSOSharp reads all the common layouts:
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:
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.
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 artifactsThe test suite for the library lives in CHDStudio.Tests/:
dotnet test CHDStudio.Tests/CHDStudio.Tests.csproj -c Release --filter "FullyQualifiedName~Cso"CSOSharp is released under the MIT license.
| Back | FazBrowse Home | New Git URL |