I lied when I said I intended to release a blog post along my releases. Let me make it right.
I have a lot of PSP UMD games. When I back them up on my computer, I back them up as CSO images which are registered as "application/x-compressed-iso" in XDG's MIME database. They don't compress as much as CHDs but they run on real hardware so I decided to use them.
Most Linux desktop environments (except KDE which has its own thing) use GNOME's thumbnailer standard. I decided I wanted some flair to my disc backups so I ended up writing a CSO thumbnailer. According to the timestamps in the release of cso-thumbnailer, I wrote it in May of 2022 and released it in February of 2024. It took about two years to release it simply because I code dirty so it took me a lot to get around to clean the source code of it up. This blog post was written in April of 2025. That's a lot of procrastination. I need to do better.
Let's start by running some sanity checks on the input file. We'll check whether the file is not too small, whether it starts with the format's magic bytes and whether its header is corrupt.
#define CISO_MAGIC 0x4F534943 // "CISO"
#define ZISO_MAGIC 0x4F53495A // "ZISO"
unsigned char header_pvd[6] = {0x01, 0x43, 0x44, 0x30, 0x30, 0x31}; // 0x01 + "CD001"
ptr_input = fopen(argv[1], "r");
if (ptr_input == NULL) error_and_exit("Error opening input file\n");
fseek(ptr_input, 0L, SEEK_END);
uint64_t size_file = ftell(ptr_input);
rewind(ptr_input);
if(size_file < 32) error_and_exit("Input file too small to be valid\n");
unsigned char header_cso[0x18];
fread(header_cso, 1, sizeof(header_cso), ptr_input);
magic = header_cso[0x0] + (header_cso[0x1] << 8) + (header_cso[0x2] << 16) + (header_cso[0x3] << 24);
if(magic != CISO_MAGIC && magic != ZISO_MAGIC) error_and_exit("File couldn't be identified as CISO or ZISO\n");
uint64_t size_uncompressed = 0;
for(int i = 7; i >= 0; --i) {
size_uncompressed <<= 8;
size_uncompressed |= (uint64_t)header_cso[0x8 + i];
}
size_block = header_cso[0x10] + (header_cso[0x11] << 8) + (header_cso[0x12] << 16) + (header_cso[0x13] << 24);
int version = header_cso[0x14];
alignment_index = header_cso[0x15];
if((magic == ZISO_MAGIC && version != 1) || !size_uncompressed || !size_block) error_and_exit("Corrupt header in specified ZISO file\n");
The file we want to extract is named "ICON0.PNG". While every official release I backed up have the image file at the exact same location on the disc, I also have some games I applied translation patches to which have the image file at different locations. To avoid decompressing the entire disc image to extract a single image file, we can locate the image file's location from the disc image's table of contents section.
#define MB64_IN_BYTES 64 * 1024 * 1024
unsigned char record_icon0[10] = {0x09, 0x49, 0x43, 0x4F, 0x4E, 0x30, 0x2E, 0x50, 0x4E, 0x47}; // 0x09 + "ICON0.PNG"
int total_block = floor(size_uncompressed / size_block);
int limit64mb_block = floor(MB64_IN_BYTES / size_block);
if(total_block > limit64mb_block) total_block = limit64mb_block; // First 64MB should be enough to get the location of the file, we can decrypt more if needed later
int size_index = (total_block + 1) * sizeof(uint32_t);
buffer_index = calloc(1, size_index);
buffer_output = calloc(1, size_block);
buffer_input = calloc(1, size_block * 2);
file_iso = (int*) calloc(1, size_uncompressed * sizeof(int));
if(!buffer_index || !buffer_output || !buffer_input || !file_iso) error_and_exit("Couldn't allocate enough memory\n");
fread(buffer_index, 1, size_index, ptr_input);
decompress(total_block);
uint64_t *location_pvd = memmem(file_iso, size_uncompressed * sizeof(int), header_pvd, sizeof(header_pvd));
unsigned char pvd[2048] = {0};
unsigned char magic_iso[8] = {0};
unsigned char first18bytes[18] = {0};
memcpy(&pvd, location_pvd, 2048);
uint64_t *location_record_icon0_name = memmem(file_iso, size_uncompressed * sizeof(int), record_icon0, sizeof(record_icon0));
uint64_t *location_record_icon0 = location_record_icon0_name - 4;
memcpy(&first18bytes, location_record_icon0, 18);
int location = first18bytes[2] + (first18bytes[3] << 8) + (first18bytes[4] << 16) + (first18bytes[5] << 24);
location = location * size_block;
int size = first18bytes[10] + (first18bytes[11] << 8) + (first18bytes[12] << 16) + (first18bytes[13] << 24);
While we're at it, let's check whether we can confirm that the disc image is really a PSP UMD disc backup somewhere around here.
unsigned char magic_psp[8] = {0x50, 0x53, 0x50, 0x20, 0x47, 0x41, 0x4D, 0x45}; // "PSP GAME"
for(int i = 0; i < 8; i++) magic_iso[i] = pvd[8 + i];
for(int i = 0; i < 8; i++) if(magic_iso[i] != magic_psp[i]) error_and_exit("File couldn't be identified as PSP ISO\n");
Now that we know for sure that we're dealing with a PSP UMD disc backup and where its "ICON0.PNG" file is located, we can selectively decompress its location and extract it without decompressing the entire image.
if((location + size + (2048 * 8)) > MB64_IN_BYTES) {
free(buffer_index);
free(file_iso);
total_block = floor((location + size + (2048 * 8)) / size_block);
size_index = (total_block + 1) * sizeof(uint32_t);
memset(buffer_output, 0, size_block);
memset(buffer_input, 0, size_block * 2);
buffer_index = calloc(1, size_index);
file_iso = (int*) calloc(1, (location + size + (2048 * 8)) * sizeof(int));
if(!buffer_index || !buffer_output || !buffer_input || !file_iso) error_and_exit("Couldn't allocate enough memory\n");
fread(header_cso, 1, sizeof(header_cso), ptr_input);
fread(buffer_index, 1, size_index, ptr_input);
decompress(total_block);
}
memcpy(png, (file_iso + (location / sizeof(int))), size);
Congratulations to us as we have just extracted a file from another file. Write the boilerplate for the thumbnailer standard and you have a thumbnailer in your hands.

Writing a thumbnailer is easy and fun. Why don't we have a lot more of them?