cheevos: hash CISO GameCube/Wii images

rc_hash reads a .ciso file through formats/ciso as the disc image it
stands for, the way RVZ and WIA files are read, so a CISO identifies
as its ISO does. The file reader lives in cheevos/cheevos_ciso.c: it
reads on the descriptor path, straight into the buffer rc_hash gives,
and seeks only when the file is not already where a read starts. The
reader is now built with cheevos as well as with the database.

ciso_hash_test hashes a GameCube disc as an ISO and as a CISO with
blocks left out, and checks the two hashes match.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-11-07 04:57:34 +01:00
1 parent aa3228069f
commit f3774dba7c
8 files changed
+482 -3

No files matched your search

+19
View File
@@ -3020,6 +3020,25 @@ jobs:
make clean TARGET=progress_bounds_test.exe
echo "[pass] progress_bounds_test (ASan + UBSan, and a mingw-w64 build)"
- name: Build and run ciso_hash_test (ASan + UBSan)
shell: bash
working-directory: samples/cheevos/ciso_hash
run: |
set -eu
# rc_hash reads a CISO through cheevos/cheevos_ciso.c as the
# disc image it stands for: a GameCube disc with blocks left
# out of the CISO must hash the same as its ISO, and the
# console check must tell GameCube, Wii and not-CISO apart.
make clean && make SANITIZER=address,undefined
ciso_dir="$(mktemp -d)"
ASAN_OPTIONS=detect_leaks=1 timeout 60 ./ciso_hash_test "$ciso_dir"
rm -rf "${ciso_dir:?}"
make clean
make CC=x86_64-w64-mingw32-gcc TARGET=ciso_hash_test.exe
test -f ciso_hash_test.exe
make clean TARGET=ciso_hash_test.exe
echo "[pass] ciso_hash_test (ASan + UBSan, and a mingw-w64 build)"
- name: Build and run psp1_gfx_contracts_test (ASan + UBSan)
shell: bash
working-directory: samples/gfx/psp1_gfx_contracts
+7
View File
@@ -2862,6 +2862,13 @@ ifeq ($(HAVE_NETWORKING), 1)
endif
endif
# CISO disc images for RetroAchievements; the scanner builds the
# reader with the database
OBJ += cheevos/cheevos_ciso.o
ifneq ($(HAVE_LIBRETRODB), 1)
OBJ += $(LIBRETRO_COMM_DIR)/formats/ciso/ciso.o
endif
# The hashes' AES (3DS titles) is the crypto library's wherever
# crypto.o is built, rcheevos' own elsewhere.
ifneq ($(filter 1,$(HAVE_CRYPTO) $(HAVE_CHEEVOS_RVZ)),)
+9 -3
View File
@@ -59,6 +59,7 @@
#endif
#include "cheevos.h"
#include "cheevos_ciso.h"
#include "cheevos_client.h"
#include "cheevos_menu.h"
#include "cheevos_locals.h"
@@ -2147,8 +2148,8 @@ bool rcheevos_load(const void *data)
#ifdef CHEEVOS_RVZ_READER
/* The file reader is kept from one load to the next, so it is
* set for every load: the RVZ one for a file it reads, the
* default for anything else. */
* set for every load: the RVZ or CISO one for a file it reads,
* the default for anything else. */
const char* ext = path_get_extension(info->path);
if ( (string_is_equal_noncase(ext, "rvz") || string_is_equal_noncase(ext, "wia"))
&& (console_id = rcheevos_rvz_get_console_id(info->path))
@@ -2164,8 +2165,13 @@ bool rcheevos_load(const void *data)
rc_hash_init_custom_filereader(&filereader);
}
else
rc_hash_init_custom_filereader(NULL);
#endif
if ( string_is_equal_noncase(path_get_extension(info->path), "ciso")
&& (console_id = rcheevos_ciso_get_console_id(info->path))
!= RC_CONSOLE_UNKNOWN)
rcheevos_ciso_filereader_init();
else
rc_hash_init_custom_filereader(NULL);
{
#ifdef HAVE_THREADS
+173
View File
@@ -0,0 +1,173 @@
/* RetroArch - A frontend for libretro.
* Copyright (C) 2026 - The RetroArch team
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
/* rc_hash reads a CISO file through these as the disc image it stands
* for. formats/ciso does no file I/O: what it asks for is read here,
* on the descriptor path, straight to where it asks - which is always
* the buffer rc_hash gave. */
#include <stdio.h>
#include <stdlib.h>
#include <boolean.h>
#include <formats/ciso.h>
#include <streams/file_stream.h>
#include "../deps/rcheevos/include/rc_consoles.h"
#include "../deps/rcheevos/include/rc_hash.h"
#include "cheevos_ciso.h"
typedef struct
{
int64_t pos; /* in the disc image */
uint64_t file_pos; /* where the file stands; seeks to it are skipped */
ciso_t *ciso;
RFILE *file;
} rcheevos_ciso_t;
/* Runs an operation's step until it is done, reading what it asks */
static int rcheevos_ciso_pump(rcheevos_ciso_t *handle, bool opening)
{
for (;;)
{
ciso_request_t req;
int64_t got = 0;
int err = opening
? ciso_open_step(handle->ciso, &req)
: ciso_read_step(handle->ciso, &req);
if (err != CISO_PENDING)
return err;
if ( handle->file_pos == req.offset
|| filestream_seek(handle->file, (int64_t)req.offset,
RETRO_VFS_SEEK_POSITION_START) == 0)
got = filestream_read(handle->file, req.dst, (int64_t)req.length);
/* unknown after a failed read: seek next time */
handle->file_pos = got > 0 ? req.offset + (uint64_t)got : ~(uint64_t)0;
ciso_feed(handle->ciso, got > 0 ? (size_t)got : 0);
}
}
static void RC_CCONV rcheevos_ciso_close(void *file_handle)
{
rcheevos_ciso_t *handle = (rcheevos_ciso_t*)file_handle;
if (handle)
{
ciso_free(handle->ciso);
if (handle->file)
filestream_close(handle->file);
free(handle);
}
}
static void* RC_CCONV rcheevos_ciso_open(const char *path)
{
rcheevos_ciso_t *handle = (rcheevos_ciso_t*)malloc(sizeof(*handle));
if (!handle)
return NULL;
handle->pos = 0;
handle->file_pos = 0;
handle->ciso = NULL;
/* the descriptor path: no stdio buffer between the file and the
* buffers rc_hash reads into */
if ( !(handle->file = filestream_open(path, RETRO_VFS_FILE_ACCESS_READ,
RETRO_VFS_FILE_ACCESS_HINT_SEQUENTIAL_BULK))
|| !(handle->ciso = ciso_new())
|| rcheevos_ciso_pump(handle, true) != CISO_OK)
{
rcheevos_ciso_close(handle);
return NULL;
}
return handle;
}
static void RC_CCONV rcheevos_ciso_seek(void *file_handle, int64_t offset,
int origin)
{
rcheevos_ciso_t *handle = (rcheevos_ciso_t*)file_handle;
switch (origin)
{
case SEEK_SET:
handle->pos = offset;
break;
case SEEK_CUR:
handle->pos += offset;
break;
case SEEK_END:
handle->pos = (int64_t)ciso_disc_size(handle->ciso) + offset;
break;
}
if (handle->pos < 0)
handle->pos = 0;
}
static int64_t RC_CCONV rcheevos_ciso_tell(void *file_handle)
{
return ((rcheevos_ciso_t*)file_handle)->pos;
}
static size_t RC_CCONV rcheevos_ciso_read(void *file_handle, void *buffer,
size_t requested_bytes)
{
rcheevos_ciso_t *handle = (rcheevos_ciso_t*)file_handle;
size_t got;
if (ciso_read_begin(handle->ciso, (uint64_t)handle->pos,
buffer, requested_bytes) != CISO_OK)
return 0;
rcheevos_ciso_pump(handle, false);
/* what came before a failure is still read */
got = ciso_read_progress(handle->ciso);
handle->pos += (int64_t)got;
return got;
}
uint32_t rcheevos_ciso_get_console_id(const char *path)
{
uint8_t magic[8];
uint32_t console_id = RC_CONSOLE_UNKNOWN;
rcheevos_ciso_t *handle = (rcheevos_ciso_t*)rcheevos_ciso_open(path);
if (!handle)
return console_id;
rcheevos_ciso_seek(handle, 0x18, SEEK_SET);
if (rcheevos_ciso_read(handle, magic, sizeof(magic)) == sizeof(magic))
{
if ( magic[0] == 0x5D && magic[1] == 0x1C
&& magic[2] == 0x9E && magic[3] == 0xA3)
console_id = RC_CONSOLE_WII;
else if (magic[4] == 0xC2 && magic[5] == 0x33
&& magic[6] == 0x9F && magic[7] == 0x3D)
console_id = RC_CONSOLE_GAMECUBE;
}
rcheevos_ciso_close(handle);
return console_id;
}
void rcheevos_ciso_filereader_init(void)
{
struct rc_hash_filereader filereader;
filereader.open = rcheevos_ciso_open;
filereader.seek = rcheevos_ciso_seek;
filereader.tell = rcheevos_ciso_tell;
filereader.read = rcheevos_ciso_read;
filereader.close = rcheevos_ciso_close;
rc_hash_init_custom_filereader(&filereader);
}
+34
View File
@@ -0,0 +1,34 @@
/* RetroArch - A frontend for libretro.
* Copyright (C) 2026 - The RetroArch team
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef __RARCH_CHEEVOS_CISO_H
#define __RARCH_CHEEVOS_CISO_H
#include <stdint.h>
#include <retro_common_api.h>
RETRO_BEGIN_DECLS
/* RC_CONSOLE_GAMECUBE or RC_CONSOLE_WII for a CISO file of either's
* disc, RC_CONSOLE_UNKNOWN for anything else */
uint32_t rcheevos_ciso_get_console_id(const char *path);
/* Has rc_hash read files as CISO disc images */
void rcheevos_ciso_filereader_init(void);
RETRO_END_DECLS
#endif
+3
View File
@@ -234,6 +234,7 @@ ACHIEVEMENTS
#include "../cheevos/cheevos_client.c"
#include "../cheevos/cheevos_menu.c"
#include "../cheevos/cheevos_badge.c"
#include "../cheevos/cheevos_ciso.c"
#if defined(HAVE_CHEEVOS_RVZ) && defined(HAVE_RZSTD)
#include "../libretro-common/formats/rvz/rvz.c"
@@ -1850,6 +1851,8 @@ DATA RUNLOOP
#include "../tasks/task_database.c"
#ifdef HAVE_LIBRETRODB
#include "../tasks/task_database_cue.c"
#endif
#if defined(HAVE_LIBRETRODB) || defined(HAVE_CHEEVOS)
#include "../libretro-common/formats/ciso/ciso.c"
#endif
#if defined(HAVE_NETWORKING) && defined(HAVE_MENU)
+59
View File
@@ -0,0 +1,59 @@
TARGET := ciso_hash_test
# rc_hash over a CISO through cheevos/cheevos_ciso.c, against the same
# disc as an ISO.
# make && make check
REPO_ROOT := ../../..
RC := $(REPO_ROOT)/deps/rcheevos
LRC := $(REPO_ROOT)/libretro-common
CFLAGS += -Wall -std=gnu99 -g1 -O1 -I$(RC)/include -I$(LRC)/include \
-DRC_CLIENT_SUPPORTS_HASH -DRC_NO_THREADS
ifneq ($(SANITIZER),)
CFLAGS += -fsanitize=$(SANITIZER) -fno-omit-frame-pointer
LDFLAGS += -fsanitize=$(SANITIZER)
endif
ifneq ($(findstring undefined,$(SANITIZER)),)
CFLAGS += -fno-sanitize-recover=undefined
LDFLAGS += -fno-sanitize-recover=undefined
endif
SOURCES := ciso_hash_test.c \
$(REPO_ROOT)/cheevos/cheevos_ciso.c \
$(LRC)/formats/ciso/ciso.c \
$(RC)/src/rhash/hash.c \
$(RC)/src/rhash/hash_disc.c \
$(RC)/src/rhash/hash_encrypted.c \
$(RC)/src/rhash/hash_rom.c \
$(RC)/src/rhash/hash_zip.c \
$(RC)/src/rhash/cdreader.c \
$(RC)/src/rhash/aes.c \
$(RC)/src/rc_util.c \
$(RC)/src/rc_compat.c \
$(LRC)/utils/md5.c \
$(LRC)/streams/file_stream.c \
$(LRC)/vfs/vfs_implementation.c \
$(LRC)/file/file_path.c \
$(LRC)/file/file_path_io.c \
$(LRC)/string/stdstring.c \
$(LRC)/string/rstrtod.c \
$(LRC)/encodings/encoding_utf.c \
$(LRC)/compat/compat_strl.c \
$(LRC)/compat/compat_posix_string.c \
$(LRC)/compat/fopen_utf8.c \
$(LRC)/features/features_cpu.c \
$(LRC)/time/rtime.c
all: $(TARGET)
$(TARGET): $(SOURCES)
$(CC) $(CFLAGS) -o $@ $(SOURCES) $(LDFLAGS) -lm
check: $(TARGET)
./$(TARGET)
clean:
rm -f $(TARGET) $(TARGET).exe
.PHONY: all check clean
+178
View File
@@ -0,0 +1,178 @@
/* ciso_hash_test: rc_hash reads a CISO file through
* cheevos/cheevos_ciso.c as the disc image it stands for, so a
* GameCube disc hashes the same as a CISO and as an ISO.
*
* The disc made up here has its headers, apploader and main.dol
* where rc_hash looks for them, segments that cross block edges, and
* runs of blocks left out of the CISO between them. A Wii header and
* a file that is not CISO are told apart by the console check.
*
* Usage: ciso_hash_test [dir], the files written there (default .). */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <boolean.h>
#include <file/file_path.h>
#include <streams/file_stream.h>
#include "../../../deps/rcheevos/include/rc_consoles.h"
#include "../../../deps/rcheevos/include/rc_hash.h"
#include "../../../cheevos/cheevos_ciso.h"
#define BLOCK 0x8000
#define BLOCKS 16
#define HEAD 0x8000
static int failures = 0;
static uint32_t rng_state = 0x9E3779B9u;
static uint32_t rnd(void)
{
rng_state ^= rng_state << 13;
rng_state ^= rng_state >> 17;
rng_state ^= rng_state << 5;
return rng_state;
}
static void check(int ok, const char *what)
{
printf("%s %s\n", ok ? "ok " : "FAIL", what);
if (!ok)
failures++;
}
static void put32(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v >> 24);
p[1] = (uint8_t)(v >> 16);
p[2] = (uint8_t)(v >> 8);
p[3] = (uint8_t)v;
}
static int write_file(const char *path, const uint8_t *d, size_t n)
{
FILE *f = fopen(path, "wb");
int ok;
if (!f)
return 0;
ok = fwrite(d, 1, n, f) == n;
return fclose(f) == 0 && ok;
}
/* The disc as an ISO and as a CISO of the blocks in @used */
static int write_disc(const char *iso, const char *ciso,
const uint8_t *used, const uint8_t *id, const uint8_t *magic,
unsigned magic_at)
{
uint8_t *disc = (uint8_t*)calloc(1, (size_t)BLOCKS * BLOCK);
uint8_t *file = (uint8_t*)calloc(1, HEAD + (size_t)BLOCKS * BLOCK);
size_t at = HEAD;
size_t i;
int ok;
if (!disc || !file)
{
free(disc);
free(file);
return 0;
}
for (i = 0; i < (size_t)BLOCKS * BLOCK; i++)
if (used[i / BLOCK])
disc[i] = (uint8_t)rnd();
memcpy(disc, id, 6);
memcpy(disc + magic_at, magic, 4);
/* apploader: 0x1000 octets of body, no trailer */
put32(disc + 0x2440 + 0x14, 0x1000);
put32(disc + 0x2440 + 0x18, 0);
/* main.dol at block 8: a code segment over blocks 9 and 10, a data
* segment from block 14 into 15 */
put32(disc + 0x420, 0x40000);
memset(disc + 0x40000, 0, 0xD8);
put32(disc + 0x40000 + 0x00, 0x48000);
put32(disc + 0x40000 + 0x90, 0x9000);
put32(disc + 0x40000 + 7 * 4, 0x70000);
put32(disc + 0x40000 + 0x90 + 7 * 4, 0x8100);
memcpy(file, "CISO", 4);
file[5] = (uint8_t)(BLOCK >> 8);
for (i = 0; i < BLOCKS; i++)
{
file[8 + i] = used[i];
if (!used[i])
continue;
memcpy(file + at, disc + i * BLOCK, BLOCK);
at += BLOCK;
}
ok = write_file(iso, disc, (size_t)BLOCKS * BLOCK)
&& write_file(ciso, file, at);
free(disc);
free(file);
return ok;
}
int main(int argc, char *argv[])
{
static const uint8_t used[BLOCKS] =
{ 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1 };
static const uint8_t gc_magic[4] = { 0xC2, 0x33, 0x9F, 0x3D };
static const uint8_t wii_magic[4] = { 0x5D, 0x1C, 0x9E, 0xA3 };
const char *dir = argc > 1 ? argv[1] : ".";
char iso[1024], ciso[1024], wii_iso[1024], wii_ciso[1024];
char h_iso[33], h_ciso[33], h_raw[33];
int ok_iso, ok_ciso, ok_raw;
fill_pathname_join_special(iso, dir, "ciso_hash_gc.iso", sizeof(iso));
fill_pathname_join_special(ciso, dir, "ciso_hash_gc.ciso", sizeof(ciso));
fill_pathname_join_special(wii_iso, dir, "ciso_hash_wii.iso", sizeof(wii_iso));
fill_pathname_join_special(wii_ciso, dir, "ciso_hash_wii.ciso", sizeof(wii_ciso));
if ( !write_disc(iso, ciso, used, (const uint8_t*)"GALE01",
gc_magic, 0x1c)
|| !write_disc(wii_iso, wii_ciso, used, (const uint8_t*)"RMCE01",
wii_magic, 0x18))
{
printf("FAIL could not write the images in %s\n", dir);
return 1;
}
check(rcheevos_ciso_get_console_id(ciso) == RC_CONSOLE_GAMECUBE,
"GameCube CISO identified");
check(rcheevos_ciso_get_console_id(wii_ciso) == RC_CONSOLE_WII,
"Wii CISO identified");
check(rcheevos_ciso_get_console_id(iso) == RC_CONSOLE_UNKNOWN,
"an ISO is not taken as CISO");
memset(h_iso, 0, sizeof(h_iso));
memset(h_ciso, 0, sizeof(h_ciso));
memset(h_raw, 0, sizeof(h_raw));
rc_hash_init_custom_filereader(NULL);
ok_iso = rc_hash_generate_from_file(h_iso, RC_CONSOLE_GAMECUBE, iso);
ok_raw = rc_hash_generate_from_file(h_raw, RC_CONSOLE_GAMECUBE, ciso);
rcheevos_ciso_filereader_init();
ok_ciso = rc_hash_generate_from_file(h_ciso, RC_CONSOLE_GAMECUBE, ciso);
rc_hash_init_custom_filereader(NULL);
printf(" iso %s\n ciso %s\n", h_iso, h_ciso);
check(ok_iso, "ISO hashes");
check(!ok_raw, "CISO read as it is stored does not hash");
check(ok_ciso && !strcmp(h_iso, h_ciso),
"CISO hashes as its disc image");
filestream_delete(iso);
filestream_delete(ciso);
filestream_delete(wii_iso);
filestream_delete(wii_ciso);
if (failures)
{
printf("%d failure(s)\n", failures);
return 1;
}
printf("ciso_hash_test: all passed\n");
return 0;
}