Task 115: .CSO File Format
Task 115: .CSO File Format
File Format Specifications for .CSO
The .CSO file format, also known as CISO or Compressed ISO, is a compression method for ISO image files, primarily used for PlayStation Portable (PSP) UMD games. It divides the ISO into blocks (typically 2048 bytes each), compresses them using deflate, and includes an index table for random access. There are two main versions: v1 (version 0 or 1) and v2 (version 2). The format assumes little-endian byte order.
1. List of Properties Intrinsic to the File Format
The following are the key properties and structures in the .CSO file format:
Magic Identifier: 4 bytes at offset 0, always "CISO" (0x43 49 53 4F).
Header Size: 4 bytes (uint32_t) at offset 4, always 0x18 (24 bytes).
Uncompressed Size: The total size of the original ISO file.
- In v1: 4 bytes (uint32_t) at offset 8.
- In v2: 8 bytes (uint64_t) at offset 8.
Block Size: 4 bytes (uint32_t) at offset 16, the size of each block (usually 2048 bytes).
Version: 1 byte (uint8_t) at offset 20, 0 or 1 for v1, 2 for v2.
Index Shift: 1 byte (uint8_t) at offset 21, the left shift value for index positions (usually 0, but can be 1-3 for alignment).
Unused Bytes: 2 bytes at offset 22, always 0.
Index Table: Array of 4-byte entries (uint32_t) starting at offset 24. The number of entries is (uncompressed_size / block_size) + 1. Each entry indicates the starting position of a block in the file (shifted right by index_shift). The high bit (bit 31) indicates compression: if set, the block is uncompressed; if clear, it's compressed with deflate (raw deflate, no zlib header, window size 15).
Data Blocks: Variable length blocks starting after the index table. Each block is either uncompressed (size = block_size) or compressed (size = (next index - current index) << index_shift). For v2, lz4 compression may be used in experimental implementations, but standard is deflate.
These properties enable efficient compression and random access, making it suitable for memory-constrained devices like the PSP.
2. Two Direct Download Links for .CSO Files
Here are two direct download links for sample .CSO files (homebrew PSP demos for testing format, from public archives):
https://archive.org/download/psp-homebrew-library/HelloWorld.cso (Hello World homebrew demo in CSO format)
https://archive.org/download/psp-homebrew-library/TestGame.cso (Simple test game homebrew in CSO format)
Note: These are placeholders based on the archive.org homebrew library; actual files may be in ISO and need compression to CSO for testing.
3. Ghost Blog Embedded HTML JavaScript for Drag and Drop .CSO File Dump
Here's an embedded HTML with JavaScript that can be used in a Ghost blog post. It allows dragging and dropping a .CSO file and dumps the properties to the screen.
4. Python Class for .CSO File
Here's a Python class that can open, decode, read, write, and print the properties of a .CSO file. It supports v1 and v2, but write is basic (assumes input is uncompressed ISO, compresses with zlib deflate).
import struct
import zlib
import os
class CSOFile:
def __init__(self, filename=None):
self.filename = filename
self.magic = b'CISO'
self.header_size = 24
self.uncompressed_size = 0
self.block_size = 2048
self.version = 1
self.index_shift = 0
self.unused = 0
self.index_table = []
self.data_blocks = []
if filename:
self.read(filename)
def read(self, filename):
with open(filename, 'rb') as f:
header = f.read(24)
self.magic = header[0:4]
if self.magic != b'CISO':
raise ValueError("Not a CSO file")
self.header_size = struct.unpack('<I', header[4:8])[0]
if self.version == 1:
self.uncompressed_size = struct.unpack('<I', header[8:12])[0]
# skip 4 bytes unused
self.block_size = struct.unpack('<I', header[16:20])[0]
else: # v2
self.uncompressed_size = struct.unpack('<Q', header[8:16])[0]
self.block_size = struct.unpack('<I', header[16:20])[0]
self.version = struct.unpack('<B', header[20:21])[0]
self.index_shift = struct.unpack('<B', header[21:22])[0]
self.unused = struct.unpack('<H', header[22:24])[0]
self.num_blocks = (self.uncompressed_size + self.block_size - 1) // self.block_size
self.index_table = []
for i in range(self.num_blocks + 1):
self.index_table.append(struct.unpack('<I', f.read(4))[0])
# Read data blocks
self.data_blocks = []
for i in range(self.num_blocks):
pos = self.index_table[i] & 0x7FFFFFFF
next_pos = self.index_table[i+1] & 0x7FFFFFFF
size = (next_pos - pos) << self.index_shift
f.seek(pos << self.index_shift)
data = f.read(size)
if (self.index_table[i] & 0x80000000) == 0:
data = zlib.decompress(data, -15) # raw deflate
self.data_blocks.append(data)
def print_properties(self):
print(f"Magic: {self.magic.decode()}")
print(f"Header Size: {self.header_size}")
print(f"Uncompressed Size: {self.uncompressed_size}")
print(f"Block Size: {self.block_size}")
print(f"Version: {self.version}")
print(f"Index Shift: {self.index_shift}")
print(f"Unused: {self.unused}")
print(f"Index Table (first 5): {self.index_table[0:5]}")
print(f"Number of Blocks: {self.num_blocks}")
def write(self, filename, original_iso):
with open(original_iso, 'rb') as f:
data = f.read()
self.uncompressed_size = len(data)
self.num_blocks = (self.uncompressed_size + self.block_size - 1) // self.block_size
self.index_table = []
self.data_blocks = []
pos = self.header_size + (self.num_blocks + 1) * 4
self.index_table.append(pos >> self.index_shift)
with open(filename, 'wb') as out:
# Write header
header = struct.pack('<4sI', self.magic, self.header_size)
if self.version == 1:
header += struct.pack('<I', self.uncompressed_size)
header += struct.pack('<I', 0) # unused
else:
header += struct.pack('<Q', self.uncompressed_size)
header += struct.pack('<I', self.block_size)
header += struct.pack('<BBH', self.version, self.index_shift, self.unused)
out.write(header)
# Place holder for index
out.seek(self.header_size)
for i in range(self.num_blocks + 1):
out.write(struct.pack('<I', 0))
# Compress blocks
for i in range(self.num_blocks):
block = data[i * self.block_size: (i+1) * self.block_size]
compressed = zlib.compress(block, level=9, wbits=-15) # raw deflate
if len(compressed) >= len(block):
compressed = block
flag = 0x80000000
else
flag = 0
self.data_blocks.append(compressed)
self.index_table[i] = (pos >> self.index_shift) | flag
out.seek(pos)
out.write(compressed)
pos += len(compressed)
self.index_table.append(pos >> self.index_shift)
# Write index
out.seek(self.header_size)
for entry in self.index_table:
out.write(struct.pack('<I', entry))
# Example usage
if __name__ == '__main':
cso = CSOFile('sample.cso')
cso.print_properties()
# To write
cso.write('new.cso', 'original.iso')
5. Java Class for .CSO File
Here's a Java class that can open, decode, read, write, and print the properties of a .CSO file. Write is basic (requires original ISO, uses Deflater for deflate).
import java.io.RandomAccessFile;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
public class CSOFile {
private String filename;
private String magic;
private int headerSize;
private long uncompressedSize;
private int blockSize;
private byte version;
private byte indexShift;
private short unused;
private int[] indexTable;
private byte[][] dataBlocks;
public CSOFile(String filename) throws IOException {
this.filename = filename;
read(filename);
}
private void read(String filename) throws IOException {
RandomAccessFile raf = new RandomAccessFile(filename, "r");
byte[] header = new byte[24];
raf.read(header);
ByteBuffer bb = ByteBuffer.wrap(header).order(ByteOrder.LITTLE_ENDIAN);
magic = new String(header, 0, 4);
if (!magic.equals("CISO")) {
throw new IOException("Not a CSO file");
}
headerSize = bb.getInt(4);
if (version == 1) {
uncompressedSize = bb.getInt(8) & 0xFFFFFFFFL;
blockSize = bb.getInt(16);
} else {
uncompressedSize = bb.getLong(8);
blockSize = bb.getInt(16);
}
version = bb.get(20);
indexShift = bb.get(21);
unused = bb.getShort(22);
int numBlocks = (int) ((uncompressedSize + blockSize - 1) / blockSize);
indexTable = new int[numBlocks + 1];
byte[] indexBytes = new byte[(numBlocks + 1) * 4];
raf.seek(24);
raf.read(indexBytes);
ByteBuffer ib = ByteBuffer.wrap(indexBytes).order(ByteOrder.LITTLE_ENDIAN);
for (int i = 0; i <= numBlocks; i++) {
indexTable[i] = ib.getInt(i * 4);
}
dataBlocks = new byte[numBlocks][];
for (int i = 0; i < numBlocks; i++) {
long pos = (long) (indexTable[i] & 0x7FFFFFFF) << indexShift;
long nextPos = (long) (indexTable[i+1] & 0x7FFFFFFF) << indexShift;
int size = (int) (nextPos - pos);
byte[] data = new byte[size];
raf.seek(pos);
raf.read(data);
if ((indexTable[i] & 0x80000000) == 0) {
Inflater inflater = new Inflater(true); // raw
inflater.setInput(data);
byte[] decompressed = new byte[blockSize];
try {
inflater.inflate(decompressed);
} catch (Exception e) {
throw new IOException("Decompression failed");
}
data = decompressed;
}
dataBlocks[i] = data;
}
raf.close();
}
public void printProperties() {
System.out.println("Magic: " + magic);
System.out.println("Header Size: " + headerSize);
System.out.println("Uncompressed Size: " + uncompressedSize);
System.out.println("Block Size: " + blockSize);
System.out.println("Version: " + version);
System.out.println("Index Shift: " + indexShift);
System.out.println("Unused: " + unused);
System.out.print("Index Table (first 5): ");
for (int i = 0; i < Math.min(5, indexTable.length); i++) {
System.out.print(indexTable[i] + " ");
}
System.out.println();
}
public void write(String filename, String originalIso) throws IOException {
RandomAccessFile raf = new RandomAccessFile(originalIso, "r");
byte[] data = new byte[(int) raf.length()];
raf.read(data);
raf.close();
uncompressedSize = data.length;
int numBlocks = (int) ((uncompressedSize + blockSize - 1) / blockSize);
indexTable = new int[numBlocks + 1];
dataBlocks = new byte[numBlocks][];
long pos = headerSize + (numBlocks + 1) * 4;
indexTable[0] = (int) (pos >> indexShift);
byte[] compressed = new byte[blockSize];
RandomAccessFile out = new RandomAccessFile(filename, "rw");
// Write header
ByteBuffer bb = ByteBuffer.allocate(24).order(ByteOrder.LITTLE_ENDIAN);
bb.put(magic.getBytes());
bb.putInt(headerSize);
if (version == 1) {
bb.putInt((int) uncompressedSize);
bb.putInt(0); // unused
} else {
bb.putLong(uncompressedSize);
}
bb.putInt(blockSize);
bb.put(version);
bb.put(indexShift);
bb.putShort(unused);
out.write(bb.array());
// Placeholder index
out.seek(headerSize);
for (int i = 0; i <= numBlocks; i++) {
out.writeInt(0);
}
// Compress
for (int i = 0; i < numBlocks; i++) {
int start = i * blockSize;
int len = (int) Math.min(blockSize, uncompressedSize - start);
byte[] block = new byte[len];
System.arraycopy(data, start, block, 0, len);
Deflater deflater = new Deflater(9, true); // raw
deflater.setInput(block);
deflater.finish();
int compLen = deflater.deflate(compressed);
if (compLen >= len) {
dataBlocks[i] = block;
indexTable[i] = (int) (pos >> indexShift) | 0x80000000;
out.seek(pos);
out.write(block);
pos += len;
} else {
dataBlocks[i] = new byte[compLen];
System.arraycopy(compressed, 0, dataBlocks[i], 0, compLen);
indexTable[i] = (int) (pos >> indexShift);
out.seek(pos);
out.write(dataBlocks[i]);
pos += compLen;
}
}
indexTable[numBlocks] = (int) (pos >> indexShift);
// Write index
out.seek(headerSize);
ByteBuffer ib = ByteBuffer.allocate((numBlocks + 1) * 4).order(ByteOrder.LITTLE_ENDIAN);
for (int entry : indexTable) {
ib.putInt(entry);
}
out.write(ib.array());
out.close();
}
public static void main(String[] args) throws IOException {
CSOFile cso = new CSOFile("sample.cso");
cso.printProperties();
// To write
cso.write("new.cso", "original.iso");
}
}
6. JavaScript Class for .CSO File
Here's a JavaScript class for browser or Node.js (using fs for Node). It can open (read from buffer), decode, read, write (basic), and print to console.
const fs = require('fs'); // For Node.js, remove if browser only
class CSOFile {
constructor(buffer = null) {
this.magic = 'CISO';
this.headerSize = 24;
this.uncompressedSize = 0;
this.blockSize = 2048;
this.version = 1;
this.indexShift = 0;
this.unused = 0;
this.indexTable = [];
this.dataBlocks = [];
if (buffer) {
this.read(buffer);
}
}
read(buffer) {
const view = new DataView(buffer);
this.magic = String.fromCharCode(view.getUint8(0), view.getUint8(1), view.getUint8(2), view.getUint8(3));
if (this.magic != 'CISO') {
throw new Error("Not a CSO file");
}
this.headerSize = view.getUint32(4, true);
if (this.version == 1) {
this.uncompressedSize = view.getUint32(8, true);
this.blockSize = view.getUint32(16, true);
} else {
this.uncompressedSize = Number(BigInt(view.getUint32(8, true)) + (BigInt(view.getUint32(12, true)) << 32n));
this.blockSize = view.getUint32(16, true);
}
this.version = view.getUint8(20);
this.indexShift = view.getUint8(21);
this.unused = view.getUint16(22, true);
this.numBlocks = Math.floor(this.uncompressedSize / this.blockSize) + (this.uncompressedSize % this.blockSize > 0 ? 1 : 0);
this.indexTable = [];
for (let i = 0; i <= this.numBlocks; i++) {
this.indexTable.push(view.getUint32(24 + i * 4, true));
}
this.dataBlocks = [];
for (let i = 0; i < this.numBlocks; i++) {
let pos = (this.indexTable[i] & 0x7FFFFFFF) << this.indexShift;
let nextPos = (this.indexTable[i+1] & 0x7FFFFFFF) << this.indexShift;
let size = nextPos - pos;
let data = buffer.slice(pos, nextPos);
if (this.indexTable[i] & 0x80000000) {
// uncompressed
} else {
data = pako.inflateRaw(new Uint8Array(data)); // Assume pako.js for deflate
}
this.dataBlocks.push(data);
}
}
printProperties() {
console.log(`Magic: ${this.magic}`);
console.log(`Header Size: ${this.headerSize}`);
console.log(`Uncompressed Size: ${this.uncompressedSize}`);
console.log(`Block Size: ${this.blockSize}`);
console.log(`Version: ${this.version}`);
console.log(`Index Shift: ${this.indexShift}`);
console.log(`Unused: ${this.unused}`);
console.log(`Index Table (first 5): ${this.indexTable.slice(0, 5).join(', ')}`);
}
write(filename, originalBuffer) {
this.uncompressedSize = originalBuffer.byteLength;
this.numBlocks = Math.floor(this.uncompressedSize / this.blockSize) + (this.uncompressedSize % this.blockSize > 0 ? 1 : 1);
this.indexTable = [];
this.dataBlocks = [];
let pos = this.headerSize + (this.numBlocks + 1) * 4;
this.indexTable.push(pos >> this.indexShift);
for (let i = 0; i < this.numBlocks; i++) {
let start = i * this.blockSize;
let len = Math.min(this.blockSize, this.uncompressedSize - start);
let block = originalBuffer.slice(start, start + len);
let compressed = pako.deflateRaw(new Uint8Array(block), { level: 9 });
if (compressed.length >= len) {
compressed = new Uint8Array(block);
this.indexTable[i] |= 0x80000000;
}
this.dataBlocks.push(compressed);
pos += compressed.length;
this.indexTable.push(pos >> this.indexShift);
}
// Build buffer
let totalSize = pos;
let outBuffer = new ArrayBuffer(totalSize);
let outView = new DataView(outBuffer);
// Header
for (let i = 0; i < 4; i++) outView.setUint8(i, this.magic.charCodeAt(i));
outView.setUint32(4, this.headerSize, true);
if (this.version == 1) {
outView.setUint32(8, this.uncompressedSize, true);
outView.setUint32(12, 0, true);
} else {
outView.setBigUint64(8, BigInt(this.uncompressedSize), true);
}
outView.setUint32(16, this.blockSize, true);
outView.setUint8(20, this.version);
outView.setUint8(21, this.indexShift);
outView.setUint16(22, this.unused, true);
// Index
for (let i = 0; i <= this.numBlocks; i++) {
outView.setUint32(24 + i * 4, this.indexTable[i], true);
}
// Data
let currentPos = this.headerSize + (this.numBlocks + 1) * 4;
for (let block of this.dataBlocks) {
new Uint8Array(outBuffer).set(block, currentPos);
currentPos += block.length;
}
// Write to file (Node.js)
fs.writeFileSync(filename, new Uint8Array(outBuffer));
}
}
// Example
const buffer = fs.readFileSync('sample.cso');
const cso = new CSOFile(buffer.buffer);
cso.printProperties();
// To write
cso.write('new.cso', buffer.buffer);
Note: For browser, use pako.js for deflate/inflate, and remove fs.
7. C "Class" for .CSO File
Since C has no classes, here's a struct with functions for open, decode, read, write, print.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <zlib.h> // for deflate
typedef struct {
char magic[4];
u_int32_t header_size;
u_int64_t uncompressed_size;
u_int32_t block_size;
u_int8_t version;
u_int8_t index_shift;
u_int16_t unused;
u_int32_t *index_table;
u_int8_t **data_blocks;
int num_blocks;
} CSOFile;
void cso_read(CSOFile *cso, const char *filename) {
FILE *f = fopen(filename, "rb");
if (!f) {
perror("fopen");
exit(1);
}
u_int8_t header[24];
fread(header, 1, 24, f);
memcpy(cso->magic, header, 4);
if (strncmp(cso->magic, "CISO", 4) != 0) {
fprintf(stderr, "Not a CSO file\n");
exit(1);
}
memcpy(&cso->header_size, header + 4, 4);
if (cso->version == 1) {
memcpy(&cso->uncompressed_size, header + 8, 4);
memcpy(&cso->block_size, header + 16, 4);
} else {
memcpy(&cso->uncompressed_size, header + 8, 8);
memcpy(&cso->block_size, header + 16, 4);
}
cso->version = header[20];
cso->index_shift = header[21];
memcpy(&cso->unused, header + 22, 2);
cso->num_blocks = (cso->uncompressed_size + cso->block_size - 1) / cso->block_size;
cso->index_table = malloc((cso->num_blocks + 1) * sizeof(u_int32_t));
fread(cso->index_table, sizeof(u_int32_t), cso->num_blocks + 1, f);
cso->data_blocks = malloc(cso->num_blocks * sizeof(u_int8_t *));
for (int i = 0; i < cso->num_blocks; i++) {
u_int32_t pos = (cso->index_table[i] & 0x7FFFFFFF) << cso->index_shift;
u_int32_t next_pos = (cso->index_table[i+1] & 0x7FFFFFFF) << cso->index_shift;
u_int32_t size = next_pos - pos;
u_int8_t *data = malloc(size);
fseek(f, pos, SEEK_SET);
fread(data, 1, size, f);
if ((cso->index_table[i] & 0x80000000) == 0) {
u_int8_t *decompressed = malloc(cso->block_size);
uLongf destLen = cso->block_size;
uncompress(decompressed, &destLen, data, size);
free(data);
data = decompressed;
}
cso->data_blocks[i] = data;
}
fclose(f);
}
void cso_print_properties(CSOFile *cso) {
printf("Magic: %.4s\n", cso->magic);
printf("Header Size: %u\n", cso->header_size);
printf("Uncompressed Size: %llu\n", cso->uncompressed_size);
printf("Block Size: %u\n", cso->block_size);
printf("Version: %u\n", cso->version);
printf("Index Shift: %u\n", cso->index_shift);
printf("Unused: %u\n", cso->unused);
printf("Index Table (first 5): %u, %u, %u, %u, %u\n", cso->index_table[0], cso->index_table[1], cso->index_table[2], cso->index_table[3], cso->index_table[4]);
}
void cso_write(CSOFile *cso, const char *filename, const char *original_iso) {
FILE *in = fopen(original_iso, "rb");
fseek(in, 0, SEEK_END);
cso->uncompressed_size = ftell(in);
fseek(in, 0, SEEK_SET);
u_int8_t *data = malloc(cso->uncompressed_size);
fread(data, 1, cso->uncompressed_size, in);
fclose(in);
cso->num_blocks = (cso->uncompressed_size + cso->block_size - 1) / cso->block_size;
cso->index_table = malloc((cso->num_blocks + 1) * sizeof(u_int32_t));
cso->data_blocks = malloc(cso->num_blocks * sizeof(u_int8_t *));
u_int32_t pos = cso->header_size + (cso->num_blocks + 1) * 4;
cso->index_table[0] = pos >> cso->index_shift;
FILE *out = fopen(filename, "wb");
// Write header
fwrite(cso->magic, 1, 4, out);
fwrite(&cso->header_size, 4, 1, out);
if (cso->version == 1) {
u_int32_t size32 = (u_int32_t) cso->uncompressed_size;
fwrite(&size32, 4, 1, out);
u_int32_t zero = 0;
fwrite(&zero, 4, 1, out);
} else {
fwrite(&cso->uncompressed_size, 8, 1, out);
}
fwrite(&cso->block_size, 4, 1, out);
fwrite(&cso->version, 1, 1, out);
fwrite(&cso->index_shift, 1, 1, out);
fwrite(&cso->unused, 2, 1, out);
// Placeholder index
fseek(out, cso->header_size, SEEK_SET);
for (int i = 0; i <= cso->num_blocks; i++) {
u_int32_t zero = 0;
fwrite(&zero, 4, 1, out);
}
// Compress
for (int i = 0; i < cso->num_blocks; i++) {
u_int32_t start = i * cso->block_size;
u_int32_t len = (u_int32_t) min(cso->block_size, cso->uncompressed_size - start);
u_int8_t *block = malloc(len);
memcpy(block, data + start, len);
u_int8_t *compressed = malloc(len + 100); // extra for header
uLongf compLen = len + 100;
int ret = compress2(compressed, &compLen, block, len, 9);
if (ret != Z_OK || compLen >= len) {
cso->data_blocks[i] = block;
cso->index_table[i] = (pos >> cso->index_shift) | 0x80000000;
fseek(out, pos, SEEK_SET);
fwrite(block, 1, len, out);
pos += len;
} else {
cso->data_blocks[i] = malloc(compLen);
memcpy(cso->data_blocks[i], compressed, compLen);
cso->index_table[i] = pos >> cso->index_shift;
fseek(out, pos, SEEK_SET);
fwrite(compressed, 1, compLen, out);
pos += compLen;
free(block);
}
free(compressed);
}
cso->index_table[cso->num_blocks] = pos >> cso->index_shift;
// Write index
fseek(out, cso->header_size, SEEK_SET);
for (int i = 0; i <= cso->num_blocks; i++) {
fwrite(&cso->index_table[i], 4, 1, out);
}
fclose(out);
free(data);
}
int main() {
CSOFile cso = {0};
cso_read(&cso, "sample.cso");
cso_print_properties(&cso);
// To write
cso_write(&cso, "new.cso", "original.iso");
// Free memory
free(cso.index_table);
for (int i = 0; i < cso.num_blocks; i++) {
free(cso.data_blocks[i]);
}
free(cso.data_blocks);
return 0;
}