mirror of
https://github.com/Elpisdev/unsegaREBORN.git
synced 2026-10-02 03:38:10 +03:00
412 lines
15 KiB
C
412 lines
15 KiB
C
#include "exfat.h"
|
|
|
|
#define EXFAT_IO_SIZE (1024 * 1024)
|
|
|
|
static uint64_t get_cluster_offset(ExfatContext* ctx, uint32_t cluster) {
|
|
if (cluster < 2) return 0;
|
|
return ctx->cluster_heap_offset_bytes + ((uint64_t)(cluster - 2) * ctx->bytes_per_cluster);
|
|
}
|
|
|
|
static bool exfat_read(ExfatContext* ctx, void* buffer, uint64_t offset, size_t size) {
|
|
if (ctx->stream) {
|
|
return stream_read(ctx->stream, buffer, offset, size);
|
|
}
|
|
if (ctx->raw_file_pos != offset) {
|
|
if (FSEEKO(ctx->fp, offset, SEEK_SET) != 0) {
|
|
return false;
|
|
}
|
|
}
|
|
if (fread(buffer, 1, size, ctx->fp) != size) {
|
|
return false;
|
|
}
|
|
ctx->raw_file_pos = offset + size;
|
|
return true;
|
|
}
|
|
|
|
static bool read_cluster(ExfatContext* ctx, uint32_t cluster, void* buffer) {
|
|
uint64_t offset = get_cluster_offset(ctx, cluster);
|
|
return exfat_read(ctx, buffer, offset, ctx->bytes_per_cluster);
|
|
}
|
|
|
|
static uint32_t get_next_cluster(ExfatContext* ctx, uint32_t cluster) {
|
|
uint32_t max_cluster = ctx->fat_length_bytes / sizeof(uint32_t);
|
|
if (cluster >= max_cluster) {
|
|
return 0;
|
|
}
|
|
uint32_t next = ctx->fat[cluster];
|
|
if (next >= 0xFFFFFFF8) {
|
|
return 0;
|
|
}
|
|
if (next == 0) return 0;
|
|
if (next >= max_cluster) {
|
|
return 0;
|
|
}
|
|
return next;
|
|
}
|
|
|
|
static bool combine_path(char* dest, size_t dest_size, const char* dir, const char* name) {
|
|
if (!dest || dest_size == 0 || !dir || !name) {
|
|
return false;
|
|
}
|
|
|
|
if (!is_safe_path(name)) {
|
|
return false;
|
|
}
|
|
|
|
size_t dir_len = strlen(dir);
|
|
size_t name_len = strlen(name);
|
|
size_t sep_len = (dir_len > 0 && dir[dir_len - 1] != '/' && dir[dir_len - 1] != '\\') ? 1 : 0;
|
|
|
|
if (dir_len + sep_len + name_len + 1 > dest_size) {
|
|
return false;
|
|
}
|
|
|
|
STRCPY_S(dest, dest_size, dir);
|
|
if (sep_len) {
|
|
STRCAT_S(dest, dest_size, PATH_SEPARATOR);
|
|
}
|
|
STRCAT_S(dest, dest_size, name);
|
|
return true;
|
|
}
|
|
|
|
static void update_cached_dir(ExfatContext* ctx, const char* output_path) {
|
|
char parent[MAX_PATH_LENGTH];
|
|
strncpy(parent, output_path, sizeof(parent) - 1);
|
|
parent[sizeof(parent) - 1] = '\0';
|
|
char* sep = strrchr(parent, PATH_SEP_CHAR);
|
|
if (!sep) return;
|
|
*sep = '\0';
|
|
if (strcmp(parent, ctx->last_dir) == 0) return;
|
|
close_output_dir(ctx->cached_dir);
|
|
ctx->cached_dir = open_output_dir(parent);
|
|
strncpy(ctx->last_dir, parent, MAX_PATH_LENGTH - 1);
|
|
ctx->last_dir[MAX_PATH_LENGTH - 1] = '\0';
|
|
}
|
|
|
|
static bool extract_file(ExfatContext* ctx, ExfatFileInfo* file, const char* output_path) {
|
|
update_cached_dir(ctx, output_path);
|
|
|
|
FILE* out = NULL;
|
|
if (ctx->cached_dir != INVALID_DIR_HANDLE) {
|
|
out = fopen_in_dir(ctx->cached_dir, file->name, file->data_length >= 65536 ? file->data_length : 0);
|
|
}
|
|
if (!out) {
|
|
if (file->data_length >= 65536) {
|
|
out = FOPEN_PREALLOC(output_path, file->data_length);
|
|
} else {
|
|
out = FOPEN(output_path, "wb");
|
|
}
|
|
}
|
|
if (!out) {
|
|
return false;
|
|
}
|
|
|
|
uint64_t remaining = file->data_length;
|
|
bool success = true;
|
|
|
|
if (file->no_fat_chain) {
|
|
uint64_t offset = get_cluster_offset(ctx, file->first_cluster);
|
|
while (remaining > 0 && success) {
|
|
size_t chunk = (remaining > EXFAT_IO_SIZE) ? EXFAT_IO_SIZE : (size_t)remaining;
|
|
if (!exfat_read(ctx, ctx->io_buf, offset, chunk)) {
|
|
success = false;
|
|
break;
|
|
}
|
|
if (FWRITE_DIRECT(out, ctx->io_buf, chunk) != chunk) {
|
|
success = false;
|
|
break;
|
|
}
|
|
offset += chunk;
|
|
remaining -= chunk;
|
|
ctx->extracted_bytes += chunk;
|
|
}
|
|
} else {
|
|
uint32_t max_clusters = (uint32_t)((file->data_length + ctx->bytes_per_cluster - 1) / ctx->bytes_per_cluster);
|
|
uint32_t cluster_count = 0;
|
|
uint32_t current_cluster = file->first_cluster;
|
|
while (remaining > 0 && current_cluster != 0 && success) {
|
|
if (++cluster_count > max_clusters) break;
|
|
size_t chunk = (remaining > ctx->bytes_per_cluster) ? ctx->bytes_per_cluster : (size_t)remaining;
|
|
uint64_t offset = get_cluster_offset(ctx, current_cluster);
|
|
if (!exfat_read(ctx, ctx->io_buf, offset, chunk)) {
|
|
success = false;
|
|
break;
|
|
}
|
|
if (FWRITE_DIRECT(out, ctx->io_buf, chunk) != chunk) {
|
|
success = false;
|
|
break;
|
|
}
|
|
remaining -= chunk;
|
|
current_cluster = get_next_cluster(ctx, current_cluster);
|
|
ctx->extracted_bytes += chunk;
|
|
}
|
|
}
|
|
|
|
if (success && file->modify_timestamp != 0) {
|
|
uint64_t mtime = exfat_timestamp_to_ntfs(file->modify_timestamp, file->modify_10ms, file->modify_utc_offset);
|
|
uint64_t atime = exfat_timestamp_to_ntfs(file->access_timestamp, 0, file->access_utc_offset);
|
|
#ifdef _WIN32
|
|
set_file_times_handle(out, mtime, atime);
|
|
#endif
|
|
}
|
|
|
|
fclose(out);
|
|
|
|
if (success) {
|
|
ctx->files_extracted++;
|
|
#ifndef _WIN32
|
|
if (file->modify_timestamp != 0) {
|
|
uint64_t mtime = exfat_timestamp_to_ntfs(file->modify_timestamp, file->modify_10ms, file->modify_utc_offset);
|
|
uint64_t atime = exfat_timestamp_to_ntfs(file->access_timestamp, 0, file->access_utc_offset);
|
|
set_file_times(output_path, mtime, atime);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
#define EXFAT_MAX_RECURSION_DEPTH 128
|
|
|
|
static bool process_directory_recursive(ExfatContext* ctx, uint32_t start_cluster, const char* output_dir, int depth) {
|
|
if (depth > EXFAT_MAX_RECURSION_DEPTH) {
|
|
fprintf(stderr, "depth\n");
|
|
return true;
|
|
}
|
|
|
|
uint32_t current_cluster = start_cluster;
|
|
uint32_t cluster_count = 0;
|
|
bool finished = false;
|
|
|
|
while (!finished && current_cluster != 0) {
|
|
if (++cluster_count > ctx->boot_sector.cluster_count) break;
|
|
if (!read_cluster(ctx, current_cluster, ctx->cluster_buf)) {
|
|
return false;
|
|
}
|
|
|
|
uint32_t entries_per_cluster = ctx->bytes_per_cluster / EXFAT_ENTRY_SIZE;
|
|
uint32_t entry_offset = 0;
|
|
for (uint32_t i = 0; i < entries_per_cluster; ) {
|
|
uint8_t* entry_ptr = ctx->cluster_buf + entry_offset;
|
|
uint8_t entry_type = *entry_ptr;
|
|
if (entry_type == EXFAT_ENTRY_EOD) {
|
|
finished = true;
|
|
break;
|
|
}
|
|
|
|
if (entry_type == EXFAT_ENTRY_FILE) {
|
|
if (entry_offset + EXFAT_ENTRY_SIZE * 2 > ctx->bytes_per_cluster) {
|
|
i++; entry_offset += EXFAT_ENTRY_SIZE; continue;
|
|
}
|
|
ExfatFileEntry* file_entry = (ExfatFileEntry*)entry_ptr;
|
|
ExfatStreamEntry* stream_entry = (ExfatStreamEntry*)(entry_ptr + EXFAT_ENTRY_SIZE);
|
|
if (stream_entry->entry_type != EXFAT_ENTRY_STREAM) {
|
|
i++;
|
|
entry_offset += EXFAT_ENTRY_SIZE;
|
|
continue;
|
|
}
|
|
int total_name_chars = stream_entry->name_length;
|
|
int num_name_entries = (total_name_chars + 14) / 15;
|
|
|
|
if (entry_offset + EXFAT_ENTRY_SIZE * (2 + num_name_entries) > ctx->bytes_per_cluster) {
|
|
i++; entry_offset += EXFAT_ENTRY_SIZE; continue;
|
|
}
|
|
|
|
char full_name[MAX_FILENAME_LENGTH];
|
|
uint16_t full_name_unicode[MAX_FILENAME_LENGTH];
|
|
int pos = 0;
|
|
uint8_t* name_entry_ptr = entry_ptr + EXFAT_ENTRY_SIZE * 2;
|
|
for (int k = 0; k < num_name_entries; k++) {
|
|
ExfatFileNameEntry* name_entry = (ExfatFileNameEntry*)(name_entry_ptr + k * EXFAT_ENTRY_SIZE);
|
|
int chars_in_this_entry = (total_name_chars - k * 15 < 15) ? (total_name_chars - k * 15) : 15;
|
|
for (int j = 0; j < chars_in_this_entry; j++) {
|
|
if (pos < MAX_FILENAME_LENGTH - 1) {
|
|
full_name_unicode[pos++] = name_entry->file_name[j];
|
|
}
|
|
}
|
|
}
|
|
full_name_unicode[pos] = 0;
|
|
|
|
fs_name_to_utf8(full_name_unicode, pos, full_name, sizeof(full_name));
|
|
|
|
ExfatFileInfo file_info;
|
|
memset(&file_info, 0, sizeof(file_info));
|
|
strncpy(file_info.name, full_name, MAX_PATH_LENGTH - 1);
|
|
file_info.name[MAX_PATH_LENGTH - 1] = '\0';
|
|
if (file_entry->file_attributes & 0x04) {
|
|
int total_entries = 2 + num_name_entries;
|
|
i += total_entries;
|
|
entry_offset += EXFAT_ENTRY_SIZE * total_entries;
|
|
continue;
|
|
}
|
|
|
|
file_info.first_cluster = stream_entry->first_cluster;
|
|
file_info.data_length = stream_entry->data_length;
|
|
file_info.is_directory = ((file_entry->file_attributes & 0x10) != 0);
|
|
file_info.no_fat_chain = ((stream_entry->flags & 0x02) != 0);
|
|
file_info.modify_timestamp = file_entry->last_modified_timestamp;
|
|
file_info.access_timestamp = file_entry->last_access_timestamp;
|
|
file_info.modify_10ms = file_entry->last_modified_10ms;
|
|
file_info.modify_utc_offset = (int8_t)file_entry->last_modified_utc_offset;
|
|
file_info.access_utc_offset = (int8_t)file_entry->last_access_utc_offset;
|
|
|
|
char full_path[MAX_PATH_LENGTH];
|
|
if (!combine_path(full_path, sizeof(full_path), output_dir, file_info.name)) {
|
|
fprintf(stderr, "path:%s\n", file_info.name);
|
|
continue;
|
|
}
|
|
|
|
int total_entries = 2 + num_name_entries;
|
|
i += total_entries;
|
|
entry_offset += EXFAT_ENTRY_SIZE * total_entries;
|
|
|
|
if (file_info.is_directory) {
|
|
if (create_directories(full_path)) {
|
|
process_directory_recursive(ctx, file_info.first_cluster, full_path, depth + 1);
|
|
if (file_info.modify_timestamp != 0) {
|
|
if (ctx->deferred_count >= ctx->deferred_capacity)
|
|
grow_deferred_dirs(&ctx->deferred_dirs, &ctx->deferred_capacity);
|
|
if (ctx->deferred_count < ctx->deferred_capacity) {
|
|
DeferredDirTime* d = &ctx->deferred_dirs[ctx->deferred_count++];
|
|
STRCPY_S(d->path, sizeof(d->path), full_path);
|
|
d->mtime = exfat_timestamp_to_ntfs(file_info.modify_timestamp, file_info.modify_10ms, file_info.modify_utc_offset);
|
|
d->atime = exfat_timestamp_to_ntfs(file_info.access_timestamp, 0, file_info.access_utc_offset);
|
|
}
|
|
}
|
|
if (!read_cluster(ctx, current_cluster, ctx->cluster_buf)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
extract_file(ctx, &file_info, full_path);
|
|
}
|
|
continue;
|
|
}
|
|
else {
|
|
i++;
|
|
entry_offset += EXFAT_ENTRY_SIZE;
|
|
}
|
|
}
|
|
|
|
if (!finished) {
|
|
current_cluster = get_next_cluster(ctx, current_cluster);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool exfat_setup_fields(ExfatContext* ctx) {
|
|
ctx->cached_dir = INVALID_DIR_HANDLE;
|
|
|
|
if (ctx->boot_sector.bytes_per_sector_shift < 9 || ctx->boot_sector.bytes_per_sector_shift > 12) return false;
|
|
if (ctx->boot_sector.sectors_per_cluster_shift > 25 - ctx->boot_sector.bytes_per_sector_shift) return false;
|
|
|
|
ctx->bytes_per_sector = (1 << ctx->boot_sector.bytes_per_sector_shift);
|
|
ctx->bytes_per_cluster = ctx->bytes_per_sector * (1 << ctx->boot_sector.sectors_per_cluster_shift);
|
|
if (ctx->bytes_per_cluster == 0) return false;
|
|
|
|
if (ctx->boot_sector.fat_offset > (0xFFFFFFFFU / ctx->bytes_per_sector)) return false;
|
|
if (ctx->boot_sector.cluster_heap_offset > (0xFFFFFFFFU / ctx->bytes_per_sector)) return false;
|
|
if (ctx->boot_sector.fat_length > (0xFFFFFFFFU / ctx->bytes_per_sector)) return false;
|
|
|
|
ctx->cluster_heap_offset_bytes = ctx->boot_sector.cluster_heap_offset * ctx->bytes_per_sector;
|
|
ctx->fat_offset_bytes = ctx->boot_sector.fat_offset * ctx->bytes_per_sector;
|
|
ctx->fat_length_bytes = ctx->boot_sector.fat_length * ctx->bytes_per_sector;
|
|
|
|
if (ctx->fat_length_bytes == 0 || ctx->fat_length_bytes > (1U << 30)) return false;
|
|
|
|
ctx->fat = malloc(ctx->fat_length_bytes);
|
|
ctx->cluster_buf = malloc(ctx->bytes_per_cluster);
|
|
ctx->io_buf = malloc(EXFAT_IO_SIZE);
|
|
if (!ctx->fat || !ctx->cluster_buf || !ctx->io_buf) {
|
|
free(ctx->fat);
|
|
free(ctx->cluster_buf);
|
|
free(ctx->io_buf);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool exfat_init(ExfatContext* ctx, const char* filename) {
|
|
memset(ctx, 0, sizeof(ExfatContext));
|
|
|
|
ctx->fp = FOPEN(filename, "rb");
|
|
if (!ctx->fp) return false;
|
|
|
|
if (fread(&ctx->boot_sector, sizeof(ExfatBootSector), 1, ctx->fp) != 1) {
|
|
fclose(ctx->fp);
|
|
return false;
|
|
}
|
|
|
|
if (!exfat_setup_fields(ctx)) {
|
|
fclose(ctx->fp);
|
|
return false;
|
|
}
|
|
|
|
if (FSEEKO(ctx->fp, ctx->fat_offset_bytes, SEEK_SET) != 0 ||
|
|
fread(ctx->fat, 1, ctx->fat_length_bytes, ctx->fp) != ctx->fat_length_bytes) {
|
|
exfat_close(ctx);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool exfat_init_stream(ExfatContext* ctx, DecryptStream* stream) {
|
|
memset(ctx, 0, sizeof(ExfatContext));
|
|
ctx->stream = stream;
|
|
|
|
if (!exfat_read(ctx, &ctx->boot_sector, 0, sizeof(ExfatBootSector))) {
|
|
return false;
|
|
}
|
|
|
|
if (!exfat_setup_fields(ctx)) return false;
|
|
|
|
if (!exfat_read(ctx, ctx->fat, ctx->fat_offset_bytes, ctx->fat_length_bytes)) {
|
|
exfat_close(ctx);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool exfat_extract_all(ExfatContext* ctx, const char* output_dir) {
|
|
if (!create_directories(output_dir)) {
|
|
return false;
|
|
}
|
|
|
|
ctx->extracted_bytes = 0;
|
|
|
|
bool result = process_directory_recursive(ctx, ctx->boot_sector.first_cluster_of_root_dir, output_dir, 0);
|
|
|
|
for (uint32_t i = ctx->deferred_count; i > 0; i--) {
|
|
DeferredDirTime* d = &ctx->deferred_dirs[i - 1];
|
|
set_dir_times(d->path, d->mtime, d->atime);
|
|
}
|
|
free(ctx->deferred_dirs);
|
|
ctx->deferred_dirs = NULL;
|
|
ctx->deferred_count = 0;
|
|
ctx->deferred_capacity = 0;
|
|
|
|
return result;
|
|
}
|
|
|
|
void exfat_close(ExfatContext* ctx) {
|
|
close_output_dir(ctx->cached_dir);
|
|
ctx->cached_dir = INVALID_DIR_HANDLE;
|
|
if (!ctx->stream && ctx->fp) {
|
|
fclose(ctx->fp);
|
|
ctx->fp = NULL;
|
|
}
|
|
if (ctx->fat) {
|
|
free(ctx->fat);
|
|
ctx->fat = NULL;
|
|
}
|
|
free(ctx->cluster_buf);
|
|
ctx->cluster_buf = NULL;
|
|
free(ctx->io_buf);
|
|
ctx->io_buf = NULL;
|
|
}
|