mirror of
https://github.com/Cesarus85/ringfall-mjolnir-vr.git
synced 2026-10-29 01:00:36 +01:00
597 lines
17 KiB
C
597 lines
17 KiB
C
/*
|
|
MAP_VALIDATE.C
|
|
|
|
The tag validator (port/linux/game/tag_validate.c) run alone on map files,
|
|
as the game runs it when a map loads: each map's tag data is read (and
|
|
inflated) to where the game reads it, its tags checked, then each of its
|
|
structure bsps where it loads. Prints every correction and refusal, then a
|
|
line for each map:
|
|
<map>: <corrections> corrections
|
|
<map>: refused
|
|
and exits 1 if any map was refused (or, with --strict, corrected: the retail
|
|
maps need no correction, which tools/test_linux_port.py checks).
|
|
|
|
Built by ninja linux as build/linux/map_validate, with the platform layer's
|
|
POSIX flags (it calls the validator's few functions, whose arguments are
|
|
pointers and longs alike in both ABIs).
|
|
|
|
With --fuzz, each map is instead checked iterations times with a few of its
|
|
words changed at random (from --seed on): the validator must never crash or
|
|
hang, and a map it lets through must be clean when checked again.
|
|
|
|
With --log, each run that was corrected is written to a file as the map's
|
|
name, the seed and the changed words (offset:value in the inflated file).
|
|
|
|
usage: map_validate [--strict] [--quiet] [--fuzz iterations [--seed n] [--log file]] map.map...
|
|
*/
|
|
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <signal.h>
|
|
#include <sys/mman.h>
|
|
#include <unistd.h>
|
|
|
|
#include "zlib_prefixed.h"
|
|
|
|
/* ---------- constants */
|
|
|
|
enum
|
|
{
|
|
/* where the game reads a map's tags, and how many bytes there are room
|
|
for (physical_memory_map.c, physical_memory_map.h) */
|
|
TAG_CACHE_BASE_ADDRESS = 0x803A6000,
|
|
TAG_CACHE_SIZE = 0x1600000,
|
|
|
|
CACHE_FILE_HEADER_SIZE = 0x800,
|
|
CACHE_FILE_HEADER_SIGNATURE = 'head',
|
|
CACHE_FILE_FOOTER_SIGNATURE = 'foot',
|
|
SCENARIO_GROUP_TAG = 'scnr',
|
|
/* what the game rounds a read up to (cache_files.c) */
|
|
CACHE_FILE_SECTOR_SIZE = 512,
|
|
/* the scenario's structure bsps block (scenario_definitions.h) */
|
|
SCENARIO_STRUCTURE_BSPS_OFFSET = 0x5A4,
|
|
MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO = 32,
|
|
};
|
|
|
|
/* ---------- structures */
|
|
|
|
struct cache_file_header
|
|
{
|
|
unsigned long header_signature;
|
|
long version;
|
|
long file_length;
|
|
unsigned long reserved0C;
|
|
long tag_data_offset;
|
|
long tag_data_size;
|
|
unsigned char reserved18[8];
|
|
char name[0x20];
|
|
char build[0x20];
|
|
unsigned char reserved60[0x79C];
|
|
unsigned long footer_signature;
|
|
};
|
|
|
|
struct tag_instance
|
|
{
|
|
unsigned long group_tag;
|
|
unsigned long parent_group_tags[2];
|
|
long tag_index;
|
|
char *name;
|
|
void *base_address;
|
|
unsigned long unused[2];
|
|
};
|
|
|
|
struct tag_header
|
|
{
|
|
struct tag_instance *instances;
|
|
long scenario_tag_index;
|
|
unsigned long checksum;
|
|
long tag_count;
|
|
};
|
|
|
|
struct tag_block
|
|
{
|
|
long count;
|
|
void *address;
|
|
void *definition;
|
|
};
|
|
|
|
struct scenario_structure_bsp_reference
|
|
{
|
|
long file_offset;
|
|
long file_size;
|
|
void *base_address;
|
|
unsigned char unused[4];
|
|
unsigned long group_tag;
|
|
char *name;
|
|
long name_length;
|
|
long tag_index;
|
|
};
|
|
|
|
typedef char verify_cache_file_header_size[sizeof(struct cache_file_header) == CACHE_FILE_HEADER_SIZE ? 1 : -1];
|
|
typedef char verify_scenario_structure_bsp_reference_size[
|
|
sizeof(struct scenario_structure_bsp_reference) == 0x20 ? 1 : -1];
|
|
|
|
/* ---------- the validator's (port/linux/game/tag_validate.c) */
|
|
|
|
unsigned char tag_validate_tags(void *tag_header, long tag_data_size, long file_length, char const *map_name);
|
|
unsigned char tag_validate_structure_bsp(long tag_index, void *base, long size);
|
|
long tag_validate_corrections(void);
|
|
unsigned char tag_validate_claimed(void const *address);
|
|
|
|
static int quiet;
|
|
/* (fuzzing: each bsp is checked twice, and whether a second check of one
|
|
found more to correct) */
|
|
static int recheck;
|
|
static int recheck_failed;
|
|
static unsigned long fuzz_seed;
|
|
|
|
void tag_validate_report(char const *message)
|
|
{
|
|
if (!quiet)
|
|
printf("%s\n", message);
|
|
}
|
|
|
|
/* (the game's printf family, which the validator calls by these names:
|
|
port/linux/include/stdio.h) */
|
|
int halo_linux_vsnprintf(char *buffer, size_t count, char const *format, va_list arguments)
|
|
{
|
|
return vsnprintf(buffer, count, format, arguments);
|
|
}
|
|
|
|
int halo_linux_snprintf(char *buffer, size_t count, char const *format, ...)
|
|
{
|
|
va_list arguments;
|
|
int result;
|
|
|
|
va_start(arguments, format);
|
|
result = vsnprintf(buffer, count, format, arguments);
|
|
va_end(arguments);
|
|
return result;
|
|
}
|
|
|
|
/* (the game's string functions, which its units call by these names:
|
|
cseries.h) */
|
|
void *csmemset(void *buffer, long c, unsigned long size)
|
|
{
|
|
return memset(buffer, (int)c, size);
|
|
}
|
|
|
|
void *csmemcpy(void *destination, void const *source, unsigned long size)
|
|
{
|
|
return memcpy(destination, source, size);
|
|
}
|
|
|
|
unsigned long csstrlen(char const *s)
|
|
{
|
|
return strlen(s);
|
|
}
|
|
|
|
char *csstrcat(char *s1, char const *s2)
|
|
{
|
|
return strcat(s1, s2);
|
|
}
|
|
|
|
long csstrcmp(char const *s1, char const *s2)
|
|
{
|
|
return strcmp(s1, s2);
|
|
}
|
|
|
|
/* ---------- code */
|
|
|
|
/* the map's bytes as the game sees them (its tag data and bsps at their file
|
|
offsets): the file, inflated after its header if it is compressed */
|
|
static unsigned char *map_read(char const *path, struct cache_file_header *header)
|
|
{
|
|
FILE *file = fopen(path, "rb");
|
|
unsigned char *bytes = NULL;
|
|
unsigned char *compressed = NULL;
|
|
long size;
|
|
|
|
if (!file)
|
|
return NULL;
|
|
if (fseek(file, 0, SEEK_END) || (size = ftell(file)) < CACHE_FILE_HEADER_SIZE || fseek(file, 0, SEEK_SET) ||
|
|
fread(header, CACHE_FILE_HEADER_SIZE, 1, file) != 1 ||
|
|
header->header_signature != CACHE_FILE_HEADER_SIGNATURE ||
|
|
header->footer_signature != CACHE_FILE_FOOTER_SIGNATURE ||
|
|
header->file_length < CACHE_FILE_HEADER_SIZE)
|
|
{
|
|
fclose(file);
|
|
return NULL;
|
|
}
|
|
bytes = malloc(header->file_length);
|
|
compressed = malloc(size - CACHE_FILE_HEADER_SIZE);
|
|
if (bytes && compressed && fread(compressed, size - CACHE_FILE_HEADER_SIZE, 1, file) == 1)
|
|
{
|
|
memcpy(bytes, header, CACHE_FILE_HEADER_SIZE);
|
|
if (size == header->file_length)
|
|
{
|
|
memcpy(bytes + CACHE_FILE_HEADER_SIZE, compressed, size - CACHE_FILE_HEADER_SIZE);
|
|
}
|
|
else
|
|
{
|
|
uLongf inflated = header->file_length - CACHE_FILE_HEADER_SIZE;
|
|
|
|
if (uncompress(bytes + CACHE_FILE_HEADER_SIZE, &inflated, compressed, size - CACHE_FILE_HEADER_SIZE) != Z_OK ||
|
|
inflated != (uLongf)(header->file_length - CACHE_FILE_HEADER_SIZE))
|
|
{
|
|
free(bytes);
|
|
bytes = NULL;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
free(bytes);
|
|
bytes = NULL;
|
|
}
|
|
free(compressed);
|
|
fclose(file);
|
|
|
|
return bytes;
|
|
}
|
|
|
|
/* the map's tags (bytes, as map_read gives them) checked where the game
|
|
reads them, then each of its structure bsps where it loads: 0 if the map
|
|
plays (its corrections counted), 1 if it is refused */
|
|
static int map_check(struct cache_file_header const *header, unsigned char const *bytes, unsigned char *tag_cache,
|
|
char const *name, long *corrections)
|
|
{
|
|
struct tag_header *tags = (struct tag_header *)tag_cache;
|
|
struct tag_block *bsps;
|
|
long bsp_count;
|
|
long index;
|
|
int refused = 0;
|
|
|
|
*corrections = 0;
|
|
memset(tag_cache, 0xCD, TAG_CACHE_SIZE);
|
|
memcpy(tag_cache, bytes + header->tag_data_offset, header->tag_data_size);
|
|
if (!tag_validate_tags(tag_cache, header->tag_data_size, header->file_length, name))
|
|
return 1;
|
|
*corrections = tag_validate_corrections();
|
|
|
|
/* each bsp, where it loads (scenario_structure_bsp_load): the reference
|
|
is the validated scenario's, which is a scenario (as
|
|
cache_file_tag_header_verify checks) */
|
|
{
|
|
short scenario_index = (short)tags->scenario_tag_index;
|
|
struct tag_instance *scenario;
|
|
|
|
if (scenario_index < 0 || scenario_index >= tags->tag_count ||
|
|
tags->instances[scenario_index].tag_index != tags->scenario_tag_index ||
|
|
tags->instances[scenario_index].group_tag != SCENARIO_GROUP_TAG)
|
|
{
|
|
if (!quiet)
|
|
printf("%s: its tag header names no scenario\n", name);
|
|
return 1;
|
|
}
|
|
scenario = &tags->instances[scenario_index];
|
|
|
|
bsps = (struct tag_block *)((unsigned char *)scenario->base_address + SCENARIO_STRUCTURE_BSPS_OFFSET);
|
|
bsp_count = bsps->count < MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO ? bsps->count : MAXIMUM_STRUCTURE_BSPS_PER_SCENARIO;
|
|
}
|
|
for (index = 0; index < bsp_count && !refused; index++)
|
|
{
|
|
struct scenario_structure_bsp_reference *reference =
|
|
(struct scenario_structure_bsp_reference *)bsps->address + index;
|
|
unsigned long load = (unsigned long)reference->base_address - TAG_CACHE_BASE_ADDRESS;
|
|
/* (the game reads whole sectors) */
|
|
unsigned long read_size = ((unsigned long)reference->file_size + CACHE_FILE_SECTOR_SIZE - 1) &
|
|
~(unsigned long)(CACHE_FILE_SECTOR_SIZE - 1);
|
|
|
|
/* (what cache_file_structure_bsp_reference_verify checks) */
|
|
if (reference->file_offset < 0 || reference->file_size <= 0 ||
|
|
reference->file_offset > header->file_length - reference->file_size ||
|
|
load < (unsigned long)header->tag_data_size || load > TAG_CACHE_SIZE ||
|
|
read_size > TAG_CACHE_SIZE - load)
|
|
{
|
|
if (!quiet)
|
|
printf("%s: its structure bsp %ld is outside it\n", name, index);
|
|
refused = 1;
|
|
break;
|
|
}
|
|
memset(tag_cache + header->tag_data_size, 0xCD, TAG_CACHE_SIZE - header->tag_data_size);
|
|
memcpy(reference->base_address, bytes + reference->file_offset, reference->file_size);
|
|
if (!tag_validate_structure_bsp(reference->tag_index, reference->base_address, reference->file_size))
|
|
{
|
|
refused = 1;
|
|
}
|
|
/* (fuzzing: the bsp as corrected, checked again, needs no more) */
|
|
else if (recheck)
|
|
{
|
|
long before = tag_validate_corrections();
|
|
|
|
if (!tag_validate_structure_bsp(reference->tag_index, reference->base_address, reference->file_size) ||
|
|
tag_validate_corrections() != before)
|
|
{
|
|
printf("%s: fuzz seed %lu: structure bsp %ld as corrected is not clean when checked again\n", name,
|
|
fuzz_seed, index);
|
|
recheck_failed = 1;
|
|
}
|
|
}
|
|
}
|
|
*corrections = tag_validate_corrections();
|
|
|
|
return refused;
|
|
}
|
|
|
|
static int map_validate(char const *path, unsigned char *tag_cache, long *corrections)
|
|
{
|
|
struct cache_file_header header;
|
|
unsigned char *bytes = map_read(path, &header);
|
|
char const *name = strrchr(path, '/') ? strrchr(path, '/') + 1 : path;
|
|
int refused;
|
|
|
|
*corrections = 0;
|
|
if (!bytes)
|
|
{
|
|
printf("%s: cannot be read\n", name);
|
|
return 1;
|
|
}
|
|
if (header.tag_data_offset < 0 || header.tag_data_size <= 0 || header.tag_data_size > TAG_CACHE_SIZE ||
|
|
header.tag_data_offset > header.file_length - header.tag_data_size)
|
|
{
|
|
printf("%s: its tag data is outside it\n", name);
|
|
free(bytes);
|
|
return 1;
|
|
}
|
|
refused = map_check(&header, bytes, tag_cache, name, corrections);
|
|
free(bytes);
|
|
|
|
return refused;
|
|
}
|
|
|
|
/* ---------- fuzzing */
|
|
|
|
/* the run being fuzzed, for the signal handlers' message */
|
|
static char const *fuzz_name = "";
|
|
|
|
static unsigned long fuzz_state;
|
|
static FILE *fuzz_log;
|
|
|
|
static unsigned long fuzz_random(void)
|
|
{
|
|
/* (xorshift32) */
|
|
fuzz_state ^= fuzz_state << 13;
|
|
fuzz_state ^= fuzz_state >> 17;
|
|
fuzz_state ^= fuzz_state << 5;
|
|
return fuzz_state;
|
|
}
|
|
|
|
static void fuzz_signal(int signal_number)
|
|
{
|
|
char message[160];
|
|
int length = snprintf(message, sizeof(message), "map_validate: %s, fuzz seed %lu: %s\n", fuzz_name, fuzz_seed,
|
|
signal_number == SIGALRM ? "hangs" : "crashes");
|
|
|
|
if (write(2, message, length) < 0)
|
|
_exit(3);
|
|
_exit(3);
|
|
}
|
|
|
|
/* a value to put in a field: one of those that break a reader of tags most */
|
|
static unsigned long fuzz_value(struct cache_file_header const *header)
|
|
{
|
|
switch (fuzz_random() % 9)
|
|
{
|
|
case 0: return fuzz_random();
|
|
case 1: return fuzz_random() % 8;
|
|
case 2: return fuzz_random() % 300;
|
|
case 3: return 0xFFFFFFFFUL;
|
|
case 4: return 0x80000000UL;
|
|
case 5: return 0x7FFFFFFFUL;
|
|
case 6: return 0xFFFF;
|
|
case 7: return TAG_CACHE_BASE_ADDRESS + fuzz_random() % (unsigned long)header->tag_data_size;
|
|
default: return TAG_CACHE_BASE_ADDRESS + fuzz_random() % TAG_CACHE_SIZE;
|
|
}
|
|
}
|
|
|
|
/* iterations runs of the map, each with a few of its tags' and bsps' words
|
|
changed: the validator must never crash or hang, and the tags it lets
|
|
through, checked again, must need no more corrections */
|
|
static int map_fuzz(char const *path, unsigned char *tag_cache, unsigned long seed, long iterations)
|
|
{
|
|
struct cache_file_header header;
|
|
unsigned char *bytes = map_read(path, &header);
|
|
char const *name = strrchr(path, '/') ? strrchr(path, '/') + 1 : path;
|
|
long iteration;
|
|
long refusals = 0;
|
|
long corrected = 0;
|
|
int failed = 0;
|
|
unsigned long *claimed = NULL;
|
|
long claimed_count = 0;
|
|
|
|
if (!bytes)
|
|
{
|
|
printf("%s: cannot be read\n", name);
|
|
return 1;
|
|
}
|
|
if (header.tag_data_offset < 0 || header.tag_data_size <= 0 || header.tag_data_size > TAG_CACHE_SIZE ||
|
|
header.tag_data_offset > header.file_length - header.tag_data_size)
|
|
{
|
|
printf("%s: its tag data is outside it\n", name);
|
|
free(bytes);
|
|
return 1;
|
|
}
|
|
fuzz_name = name;
|
|
recheck = 1;
|
|
/* (the words of the tags' roots, blocks and data, where nearly every
|
|
field the game trusts is: most changes go there) */
|
|
{
|
|
long corrections;
|
|
long offset;
|
|
|
|
map_check(&header, bytes, tag_cache, name, &corrections);
|
|
claimed = malloc(sizeof(*claimed) * (header.tag_data_size / 4 + 1));
|
|
for (offset = 0; claimed && offset + 4 <= header.tag_data_size; offset += 4)
|
|
{
|
|
if (tag_validate_claimed(tag_cache + offset))
|
|
claimed[claimed_count++] = header.tag_data_offset + offset;
|
|
}
|
|
}
|
|
for (iteration = 0; iteration < iterations; iteration++)
|
|
{
|
|
enum { MAXIMUM_CHANGES = 8 };
|
|
unsigned long offsets[MAXIMUM_CHANGES];
|
|
unsigned long values[MAXIMUM_CHANGES];
|
|
long changes = 1 + fuzz_random() % MAXIMUM_CHANGES;
|
|
long corrections;
|
|
long change;
|
|
|
|
fuzz_seed = seed + iteration;
|
|
fuzz_state = fuzz_seed * 2654435761UL + 1;
|
|
changes = 1 + fuzz_random() % MAXIMUM_CHANGES;
|
|
for (change = 0; change < changes; change++)
|
|
{
|
|
/* (most in the tags' roots, blocks and data, some anywhere in the
|
|
tags, some anywhere in the file: its bsps) */
|
|
unsigned long choice = fuzz_random() % 8;
|
|
unsigned long offset = choice < 5 && claimed_count ? claimed[fuzz_random() % claimed_count] :
|
|
choice < 7 ? header.tag_data_offset + (fuzz_random() % (unsigned long)header.tag_data_size) :
|
|
fuzz_random() % (unsigned long)header.file_length;
|
|
|
|
offset &= ~3UL;
|
|
if (offset + 4 > (unsigned long)header.file_length)
|
|
offset = header.tag_data_offset;
|
|
offsets[change] = offset;
|
|
memcpy(&values[change], bytes + offset, 4);
|
|
{
|
|
unsigned long value = fuzz_value(&header);
|
|
|
|
memcpy(bytes + offset, &value, 4);
|
|
}
|
|
}
|
|
|
|
alarm(60);
|
|
if (map_check(&header, bytes, tag_cache, name, &corrections))
|
|
{
|
|
refusals++;
|
|
}
|
|
else
|
|
{
|
|
long again;
|
|
|
|
if (corrections)
|
|
{
|
|
corrected++;
|
|
/* (with --log, the changes of a run that was corrected, for
|
|
playing it in the game: "offset value" in the file) */
|
|
if (fuzz_log)
|
|
{
|
|
fprintf(fuzz_log, "%s %lu", name, fuzz_seed);
|
|
for (change = 0; change < changes; change++)
|
|
{
|
|
unsigned long value;
|
|
|
|
memcpy(&value, bytes + offsets[change], 4);
|
|
fprintf(fuzz_log, " %lx:%lx", offsets[change], value);
|
|
}
|
|
fprintf(fuzz_log, "\n");
|
|
}
|
|
}
|
|
/* (the tags as corrected, checked again: nothing more to correct) */
|
|
if (!tag_validate_tags(tag_cache, header.tag_data_size, header.file_length, name) ||
|
|
(again = tag_validate_corrections()) != 0)
|
|
{
|
|
printf("%s: fuzz seed %lu: the corrected tags are not clean when checked again:\n", name, fuzz_seed);
|
|
failed = 1;
|
|
/* (what was corrected, then what is left) */
|
|
quiet = 0;
|
|
map_check(&header, bytes, tag_cache, name, &corrections);
|
|
printf("checked again:\n");
|
|
tag_validate_tags(tag_cache, header.tag_data_size, header.file_length, name);
|
|
quiet = 1;
|
|
}
|
|
}
|
|
alarm(0);
|
|
|
|
/* (undone, last first) */
|
|
for (change = changes - 1; change >= 0; change--)
|
|
memcpy(bytes + offsets[change], &values[change], 4);
|
|
}
|
|
recheck = 0;
|
|
if (recheck_failed)
|
|
failed = 1;
|
|
printf("%s: %ld runs: %ld refused, %ld corrected, %ld unchanged\n", name, iterations, refusals, corrected,
|
|
iterations - refusals - corrected);
|
|
free(claimed);
|
|
free(bytes);
|
|
|
|
return failed;
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
unsigned char *tag_cache;
|
|
int strict = 0;
|
|
int failed = 0;
|
|
long fuzz_iterations = 0;
|
|
unsigned long seed = 1;
|
|
int argument;
|
|
|
|
tag_cache = mmap((void *)TAG_CACHE_BASE_ADDRESS, TAG_CACHE_SIZE, PROT_READ | PROT_WRITE,
|
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
|
if (tag_cache != (unsigned char *)TAG_CACHE_BASE_ADDRESS)
|
|
{
|
|
fprintf(stderr, "map_validate: the tag cache's address (%08x) is taken\n", TAG_CACHE_BASE_ADDRESS);
|
|
return 2;
|
|
}
|
|
for (argument = 1; argument < argc; argument++)
|
|
{
|
|
long corrections;
|
|
|
|
if (!strcmp(argv[argument], "--strict"))
|
|
{
|
|
strict = 1;
|
|
continue;
|
|
}
|
|
if (!strcmp(argv[argument], "--quiet"))
|
|
{
|
|
quiet = 1;
|
|
continue;
|
|
}
|
|
if (!strcmp(argv[argument], "--fuzz") && argument + 1 < argc)
|
|
{
|
|
fuzz_iterations = strtol(argv[++argument], NULL, 10);
|
|
quiet = 1;
|
|
signal(SIGSEGV, fuzz_signal);
|
|
signal(SIGBUS, fuzz_signal);
|
|
signal(SIGFPE, fuzz_signal);
|
|
signal(SIGALRM, fuzz_signal);
|
|
continue;
|
|
}
|
|
if (!strcmp(argv[argument], "--log") && argument + 1 < argc)
|
|
{
|
|
fuzz_log = fopen(argv[++argument], "w");
|
|
continue;
|
|
}
|
|
if (!strcmp(argv[argument], "--seed") && argument + 1 < argc)
|
|
{
|
|
seed = strtoul(argv[++argument], NULL, 10);
|
|
continue;
|
|
}
|
|
if (fuzz_iterations)
|
|
{
|
|
if (map_fuzz(argv[argument], tag_cache, seed, fuzz_iterations))
|
|
failed = 1;
|
|
fflush(stdout);
|
|
continue;
|
|
}
|
|
if (map_validate(argv[argument], tag_cache, &corrections))
|
|
{
|
|
printf("%s: refused\n", argv[argument]);
|
|
failed = 1;
|
|
}
|
|
else
|
|
{
|
|
printf("%s: %ld corrections\n", argv[argument], corrections);
|
|
if (strict && corrections)
|
|
failed = 1;
|
|
}
|
|
fflush(stdout);
|
|
}
|
|
|
|
return failed;
|
|
}
|