From 1773db1326cfbd665ec10fe0c975de4a6e0b7f21 Mon Sep 17 00:00:00 2001 From: DCurrent Date: Thu, 18 Jun 2026 10:25:21 -0400 Subject: [PATCH] Enforce strict archive paths during pack creation Reject new archive entries that use characters outside the supported ASCII subset. This catches common filename mistakes early instead of allowing non-portable paths into newly built packs. Limit stored archive paths to 511 characters so they fit the engine's 512-byte path buffer including the null terminator. Keep source path handling separate with a larger 1024-byte buffer, since host filesystem paths may be longer than the stored archive path. Clean up archive path copying and validation so path errors hard fail during pack creation while legacy packs can still be listed or extracted. --- tools/borpak/source/borpak.c | 128 ++++++++++++++++++++++++++++------- tools/borpak/source/borpak.h | 27 +++++++- 2 files changed, 130 insertions(+), 25 deletions(-) diff --git a/tools/borpak/source/borpak.c b/tools/borpak/source/borpak.c index 97072546f..e362233f2 100644 --- a/tools/borpak/source/borpak.c +++ b/tools/borpak/source/borpak.c @@ -242,20 +242,78 @@ void validate_pak32_budget(const char *file_path, pak_offset_t data_offset, pak_ * Caskey, Damon V. * 2026-06-10 * -* Copies a path into the archive table using -* legacy OpenBOR pack naming: forward slashes -* become backslashes, and a leading "./" is -* ignored. Source paths and archive paths are -* handled separately so absolute -d paths do not -* leak host-specific prefixes such as /home/user/project -* into the pack table. +* Validates that an archive path contains +* only allowed characters. Returns 1 on pass +* and 0 on failure. */ -void copy_archive_path(char *destination, size_t destination_size, const char *source) { +static int is_archive_path_character_allowed(unsigned char character) { + + /* + * Check if the character is an uppercase letter. + */ + if(character >= 'A' && character <= 'Z') { + return 1; + } + + /* + * Check if the character is a lowercase letter. + */ + if(character >= 'a' && character <= 'z') { + return 1; + } + + /* + * Check if the character is a digit. + */ + if(character >= '0' && character <= '9') { + return 1; + } + + /* + * Check if the character is an allowed punctuation mark. + */ + switch(character) { + case '/': + case '\\': + case '.': + case '_': + case '-': + return 1; + default: + return 0; + } +} + +int validate_archive_path(const char *archive_path, const char *source_file_path) { + size_t character_index; + unsigned char character; + + if(!archive_path || !archive_path[0]) { + printf("\nError: empty archive path for %s\n", source_file_path ? source_file_path : "(unknown source)"); + return 0; + } + + for(character_index = 0; archive_path[character_index]; character_index++) { + character = (unsigned char)archive_path[character_index]; + + if(!is_archive_path_character_allowed(character)) { + printf("\nError: invalid archive path character in %s\n", archive_path); + printf("Source file: %s\n", source_file_path ? source_file_path : "(unknown source)"); + printf("Invalid byte: 0x%02x at position %zu\n", (unsigned int)character, character_index); + printf("Archive paths may only use A-Z, a-z, 0-9, slash, backslash, dot, underscore, and hyphen.\n"); + return 0; + } + } + + return 1; +} + +int copy_archive_path(char *destination, size_t destination_size, const char *source) { size_t source_index = 0; size_t destination_index = 0; if(destination_size == 0) { - return; + return 0; } while(source[source_index] == '.' && @@ -263,13 +321,19 @@ void copy_archive_path(char *destination, size_t destination_size, const char *s source_index += 2; } - while(source[source_index] && destination_index < destination_size - 1) { + while(source[source_index]) { + if(destination_index >= destination_size - 1) { + destination[destination_index] = '\0'; + return 0; + } + destination[destination_index] = source[source_index] == '/' ? '\\' : source[source_index]; source_index++; destination_index++; } destination[destination_index] = '\0'; + return 1; } /* @@ -289,7 +353,7 @@ void copy_archive_root_from_input_directory(char *destination, size_t destinatio const char *component_end; const char *component_start; size_t component_length; - char component[FILE_NAME_MAX_LEN]; + char component[ARCHIVE_PATH_MAX_LEN]; if(destination_size == 0) { return; @@ -334,7 +398,10 @@ void copy_archive_root_from_input_directory(char *destination, size_t destinatio memcpy(component, component_start, component_length); component[component_length] = '\0'; - copy_archive_path(destination, destination_size, component); + if(!copy_archive_path(destination, destination_size, component)) { + printf("\nError: archive root path too long: %s\n", input_directory); + exit(1); + } } /* @@ -421,7 +488,7 @@ int main(int argc, char *argv[]) { pak_entry_node_t *entry_node_free; pak_entry_t entry; - FILE *pak_file_object; + FILE *pak_file_object = NULL; pak_offset_t table_offset; pak_offset_t table_end_offset; pak_offset_t table_start_offset; @@ -435,11 +502,12 @@ int main(int argc, char *argv[]) { pak_u32 pack_format_id = 0; size_t file_count = 0; int fgetc_result = 0; + int exit_status = 0; pak_format_t format = PAK_FORMAT_PAK32; unsigned char pack_magic_buffer[PACK_MAGIC_BYTE_COUNT]; char curdir[512]; - char archive_root[FILE_NAME_MAX_LEN]; + char archive_root[ARCHIVE_PATH_MAX_LEN]; char *input_dir = NULL; char *pattern = NULL; char *pak_filename; @@ -605,6 +673,7 @@ int main(int argc, char *argv[]) { if(pack_directory(pak_file_object, input_dir, archive_root, format) < 0) { printf("\nError: an error occurred during the directory scanning\n"); + exit_status = 1; goto quit; } @@ -682,11 +751,13 @@ int main(int argc, char *argv[]) { } if(fread(pack_magic_buffer, 1, pack_magic_len, pak_file_object) != pack_magic_len) { + exit_status = 1; goto quit; } if(memcmp(pack_magic_buffer, pack_magic_bytes, pack_magic_len) != 0) { printf("\nError: unsupported pack format\n"); + exit_status = 1; goto quit; } @@ -701,6 +772,7 @@ int main(int argc, char *argv[]) { entry_base_size = PAK64_ENTRY_BASE_SIZE; } else { printf("\nError: unsupported PACK format id %u\n", (unsigned int)pack_format_id); + exit_status = 1; goto quit; } @@ -719,6 +791,7 @@ int main(int argc, char *argv[]) { if(table_offset < header_size || table_offset > table_end_offset) { printf("\nError: malformed %s file table offset\n", format_name(format)); + exit_status = 1; goto quit; } @@ -731,6 +804,7 @@ int main(int argc, char *argv[]) { if(remaining_table_bytes < entry_base_size) { printf("\nError: malformed %s file table\n", format_name(format)); + exit_status = 1; goto quit; } @@ -746,11 +820,13 @@ int main(int argc, char *argv[]) { if(entry.record_size <= entry_base_size) { printf("\nError: malformed %s file table entry\n", format_name(format)); + exit_status = 1; goto quit; } if(entry.record_size > remaining_table_bytes) { printf("\nError: malformed %s file table\n", format_name(format)); + exit_status = 1; goto quit; } @@ -791,6 +867,7 @@ int main(int argc, char *argv[]) { entry.data_size > table_start_offset - entry.data_offset) { printf("\nError: malformed %s file data range: %s\n", format_name(format), entry.name); free(entry.name); + exit_status = 1; goto quit; } @@ -807,11 +884,13 @@ int main(int argc, char *argv[]) { } quit: - fclose(pak_file_object); + if(pak_file_object) { + fclose(pak_file_object); + } printf("- finished: %zu files\n", file_count); printf("- current directory: %s\n", getcwd(curdir, sizeof(curdir))); - return 0; + return exit_status; } int pack_file(FILE *pak_file_object, const char *source_file_path, const char *archive_file_path, pak_format_t format) { @@ -827,12 +906,15 @@ int pack_file(FILE *pak_file_object, const char *source_file_path, const char *a pak_u32 record_size; unsigned char copy_buffer[8192]; - char archive_name[FILE_NAME_MAX_LEN]; + char archive_name[ARCHIVE_PATH_MAX_LEN]; - copy_archive_path(archive_name, sizeof(archive_name), archive_file_path); - if(!archive_name[0]) { - printf("\nError: empty archive path for %s\n", source_file_path); - exit(1); + if(!copy_archive_path(archive_name, sizeof(archive_name), archive_file_path)) { + printf("\nError: archive path too long: %s\n", archive_file_path); + return -1; + } + + if(!validate_archive_path(archive_name, source_file_path)) { + return -1; } // printf(" %s\r", archive_name); @@ -1024,8 +1106,8 @@ void write_le_u64(FILE *pak_file_object, uint64_t value) { } int pack_directory(FILE *pak_file_object, const char *source_directory_path, const char *archive_directory_path, pak_format_t format) { - char child_source_path[FILE_NAME_MAX_LEN]; - char child_archive_path[FILE_NAME_MAX_LEN]; + char child_source_path[SOURCE_PATH_MAX_LEN]; + char child_archive_path[ARCHIVE_PATH_MAX_LEN]; struct stat path_status; struct dirent **namelist; diff --git a/tools/borpak/source/borpak.h b/tools/borpak/source/borpak.h index 22a94ea7b..ad88522cd 100644 --- a/tools/borpak/source/borpak.h +++ b/tools/borpak/source/borpak.h @@ -18,7 +18,29 @@ #include #include -#define FILE_NAME_MAX_LEN 1024U +/* +* Maximum source path and maximum archive path lengths. +* +* Source is the path on disk to the file being packed. +* Archive is the path within the PAK archive. We keep +* separate limits because the source path may be and +* probably is much longer than the archive path, and +* we need to read it in full to get the file data, but +* only the archive path length is relevant for the PAK +* format. +* +* In practice it usually doesn't matter - Windows paths +* are limited to 260 chars by default. +* +* ex: source path: sysdrive/openbor_projects/mygame/data/chars/johndoe/idle_0.png +* archive path: data/chars/johndoe/idle_0.png +* +* Note the engine requires a buffer size at least equal +* to the maximum archive path length. +* +*/ +#define ARCHIVE_PATH_MAX_LEN 512U +#define SOURCE_PATH_MAX_LEN 1024U #define PACK_HEADER_SIZE 8U #define PACK_MAGIC_BYTE_COUNT 4U @@ -104,7 +126,8 @@ pak_size_t pak_file_size(FILE *file_object); void pak_seek_relative(FILE *pak_file_object, int64_t offset, int origin); void pak_seek_to_offset(FILE *pak_file_object, pak_offset_t offset); void validate_pak32_budget(const char *file_path, pak_offset_t data_offset, pak_size_t data_size, pak_u32 record_size); -void copy_archive_path(char *destination, size_t destination_size, const char *source); +int copy_archive_path(char *destination, size_t destination_size, const char *source); +int validate_archive_path(const char *archive_path, const char *source_file_path); void copy_archive_root_from_input_directory(char *destination, size_t destination_size, const char *input_directory); void print_format_details(pak_format_t format); int is_unsafe_path(const char *name);