commit 7099589b02c3546d83ef8dec83c29d5de858875c Author: Elpisdev Date: Fri Feb 6 00:50:03 2026 +0300 major rework diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f6a525d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +* text=auto eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..b006481 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,66 @@ +name: build + +on: + push: + tags: ['[0-9]*'] + +jobs: + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + install: mingw-w64-x86_64-clang mingw-w64-x86_64-lld + - name: inject keys + shell: bash + env: + KEYS: ${{ secrets.KEYS_INC }} + run: printf '%s\n' "$KEYS" > include/keys.inc + - name: build + shell: msys2 {0} + run: sh build.cmd + - uses: actions/upload-artifact@v4 + with: + name: win-x64 + path: build/unsegareborn-win-x64.exe + + linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: deps + run: | + sudo apt-get update + sudo apt-get install -y clang lld + - name: inject keys + env: + KEYS: ${{ secrets.KEYS_INC }} + run: printf '%s\n' "$KEYS" > include/keys.inc + - run: sed 's/\r$//' build.cmd | sh + - uses: actions/upload-artifact@v4 + with: + name: linux-x64 + path: build/unsegareborn-linux-x64 + + release: + needs: [windows, linux] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + permissions: + contents: write + steps: + - uses: actions/download-artifact@v4 + with: + name: win-x64 + path: win + - uses: actions/download-artifact@v4 + with: + name: linux-x64 + path: linux + - uses: softprops/action-gh-release@v2 + with: + files: | + win/unsegareborn-win-x64.exe + linux/unsegareborn-linux-x64 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d181f0e --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +# build outputs +build/ +*.o +*.obj + +# keys (use keys.inc.example as template) +include/keys.inc diff --git a/README.md b/README.md new file mode 100644 index 0000000..05265ce --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# unsegaREBORN + +SEGA arcade image toolkit + +## features + +- APP/OPT/APM3 decryption +- NTFS/exFAT support +- VHD support (fixed, dynamic, differencing) +- stream directly from encrypted image (no temp files) +- preserved timestamps +- AES-NI accelerated with software fallback + +## build + +``` +build.cmd # windows +sh build.cmd # linux +``` + +output: `build/unsegareborn-{platform}-x64[.exe]` + +## usage + +``` +unsegareborn [flags] +``` + +flags: +- `-o dir` output directory +- `-n` decrypt only, skip extraction +- `-w` write intermediate .ntfs/.exfat files +- `-p file` parent for differencing VHD +- `-s` silent +- `-v` verbose +- `-vn` version + +drag and drop works on windows + +## keys + +prebuilt releases include keys. source does not. + +to build from source: +1. copy `include/keys.inc.example` to `include/keys.inc` +2. add your keys in the format shown + +format: +```c +{"SDEZ", {0xd1,0x36,...}, {0xc4,0x84,...}, true}, +``` + +## platforms + +| platform | method | +|-------------|-------------------------| +| win x64 | native (ntdll only) | +| win arm64 | x64 emulation | +| linux x64 | native (static no libc) | +| linux arm64 | box64/qemu | +| macos | wine (untested) | + +## release + +push version tag: +``` +git tag 2026020501 +git push origin 2026020501 +``` + +ci builds both platforms and creates a github release with binaries + +## license + +UNLICENSE \ No newline at end of file diff --git a/UNLICENSE b/UNLICENSE new file mode 100644 index 0000000..c32dd18 --- /dev/null +++ b/UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..15dfa66 --- /dev/null +++ b/build.cmd @@ -0,0 +1,76 @@ +:<<"::BATCH_SECTION" +@echo off +goto :WINDOWS +::BATCH_SECTION +#!/bin/sh +set -e +mkdir -p build + +SRC="src/lib.c src/main.c src/crypto.c src/keys.c src/exfat.c src/ntfs.c src/stream.c src/aes.c" + +# Check for clang +command -v clang >/dev/null 2>&1 || { echo "error: clang not found"; exit 1; } + +# Detect OS +case "$(uname -s)" in + MINGW*|MSYS*|CYGWIN*) + OUT="build/unsegareborn-win-x64.exe" + CFLAGS="-target x86_64-pc-windows-gnu -Oz -maes -msse4.1 -I include -fno-asynchronous-unwind-tables -fno-ident -ffunction-sections -fdata-sections -flto -ffreestanding -fno-builtin -fno-stack-protector -nostdlib -fno-unwind-tables -fno-exceptions -fmerge-all-constants -fno-addrsig" + LDFLAGS="-fuse-ld=lld -Wl,--gc-sections -Wl,--icf=all -Wl,-e,_start -Wl,--subsystem,console -Wl,-s -Wl,--lto-Oz -L/mingw64/lib" + LIBS="-lntdll" + ;; + *) + OUT="build/unsegareborn-linux-x64" + CFLAGS="-Oz -maes -msse4.1 -I include -fno-asynchronous-unwind-tables -fno-ident -ffunction-sections -fdata-sections -flto -ffreestanding -fno-builtin -fno-stack-protector -nostdlib -fno-unwind-tables -fno-exceptions -fmerge-all-constants -fno-addrsig" + LDFLAGS="-fuse-ld=lld -Wl,--gc-sections -Wl,--icf=all -Wl,-e,_start -Wl,-s -Wl,--lto-O2 -static" + LIBS="" + ;; +esac + +echo "building..." +clang $CFLAGS $LDFLAGS -o $OUT $SRC $LIBS +echo "done: $OUT ($(stat -c%s "$OUT" 2>/dev/null || stat -f%z "$OUT") bytes)" +exit 0 + +:WINDOWS +setlocal +if not exist build mkdir build + +set SRC=src\lib.c src\main.c src\crypto.c src\keys.c src\exfat.c src\ntfs.c src\stream.c src\aes.c + +:: Find Clang +where clang >nul 2>&1 || ( + for %%P in ( + "C:\Program Files\LLVM\bin" + "C:\LLVM\bin" + "%LOCALAPPDATA%\LLVM\bin" + "C:\msys64\clang64\bin" + "C:\msys64\mingw64\bin" + ) do if exist "%%~P\clang.exe" set "PATH=%%~P;%PATH%"& goto :clang_found + echo error: clang not found - install LLVM or add to PATH + exit /b 1 +) +:clang_found + +:: Find libraries +set LIBPATH= +for %%L in ( + "C:\msys64\mingw64\lib" + "C:\msys64\clang64\lib" + "C:\msys64\ucrt64\lib" +) do if exist "%%~L\libntdll.a" set "LIBPATH=-L%%~L"& goto :lib_found +:lib_found + +set CFLAGS=-target x86_64-pc-windows-gnu -Oz -maes -msse4.1 -I include -fno-asynchronous-unwind-tables -fno-ident -ffunction-sections -fdata-sections -flto -ffreestanding -fno-builtin -fno-stack-protector -nostdlib -fno-unwind-tables -fno-exceptions -fmerge-all-constants -fno-addrsig +set LDFLAGS=-fuse-ld=lld -Wl,--gc-sections -Wl,--icf=all -Wl,-e,_start -Wl,--subsystem,console -Wl,-s -Wl,--lto-Oz %LIBPATH% + +echo building... +clang %CFLAGS% %LDFLAGS% -o build\unsegareborn-win-x64.exe %SRC% -lntdll + +if exist build\unsegareborn-win-x64.exe ( + for %%F in (build\unsegareborn-win-x64.exe) do echo done: %%~nxF ^(%%~zF bytes^) +) else ( + echo build failed + exit /b 1 +) +exit /b 0 \ No newline at end of file diff --git a/include/aes.h b/include/aes.h new file mode 100644 index 0000000..4d63d9d --- /dev/null +++ b/include/aes.h @@ -0,0 +1,20 @@ +#ifndef AES_H +#define AES_H + +#include "lib.h" + +#define AES_BLOCKLEN 16 + +typedef struct { + uint8_t round_keys[176] __attribute__((aligned(16))); + uint8_t dec_keys[160] __attribute__((aligned(16))); + uint8_t iv[16] __attribute__((aligned(16))); +} AES_ctx; + +void AES_init_ctx_iv(AES_ctx* ctx, const uint8_t* key, const uint8_t* iv); +void AES_ctx_set_iv(AES_ctx* ctx, const uint8_t* iv); +void AES_CBC_decrypt_buffer(AES_ctx* ctx, uint8_t* buf, size_t len); +void AES_CBC_encrypt_buffer(AES_ctx* ctx, uint8_t* buf, size_t len); +int aes_hw_supported(void); + +#endif diff --git a/include/bootid.h b/include/bootid.h new file mode 100644 index 0000000..d24b411 --- /dev/null +++ b/include/bootid.h @@ -0,0 +1,71 @@ +#ifndef BOOTID_H +#define BOOTID_H + +#include "lib.h" + +extern const uint8_t BOOTID_KEY[16]; +extern const uint8_t BOOTID_IV[16]; + +enum ContainerType { + CONTAINER_TYPE_OS = 0x00, + CONTAINER_TYPE_APP = 0x01, + CONTAINER_TYPE_OPTION = 0x02 +}; + +#define IS_APM3_OPTION(game_id) (memcmp(game_id, "SDEM", 4) == 0) + +#pragma pack(push, 1) + +typedef struct { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + uint8_t unk1; +} Timestamp; + +typedef struct { + uint8_t release; + uint8_t minor; + uint16_t major; +} Version; + +typedef union { + Version version; + uint8_t option[4]; +} GameVersion; + +typedef struct { + uint32_t crc32; + uint32_t length; + uint8_t signature[4]; + uint8_t unk1; + uint8_t container_type; + uint8_t sequence_number; + bool use_custom_iv; + uint8_t game_id[4]; + Timestamp target_timestamp; + GameVersion target_version; + uint64_t block_count; + uint64_t block_size; + uint64_t header_block_count; + uint64_t unk2; + uint8_t os_id[3]; + uint8_t os_generation; + Timestamp source_timestamp; + Version source_version; + Version os_version; + uint8_t padding[8]; + uint8_t extra_padding[4]; +} BootId; + +#pragma pack(pop) + +static inline void format_timestamp(const Timestamp* ts, char* buffer, size_t buffer_size) { + snprintf(buffer, buffer_size, "%04d%02d%02d%02d%02d%02d", + ts->year, ts->month, ts->day, ts->hour, ts->minute, ts->second); +} + +#endif \ No newline at end of file diff --git a/include/common.h b/include/common.h new file mode 100644 index 0000000..138598f --- /dev/null +++ b/include/common.h @@ -0,0 +1,254 @@ +#ifndef COMMON_H +#define COMMON_H + +#include "lib.h" + +#define FSEEKO fseeko +#define FTELLO ftello + +#ifdef PLATFORM_WINDOWS + +static inline int utf8_to_wide(const char* utf8, WCHAR* wide, int wide_len) { + if (!utf8 || !wide || wide_len <= 0) return 0; + int out = 0; + const unsigned char* s = (const unsigned char*)utf8; + while (*s && out < wide_len - 1) { + uint32_t cp; + if (s[0] < 0x80) { cp = s[0]; s += 1; } + else if ((s[0] & 0xE0) == 0xC0 && (s[1] & 0xC0) == 0x80) { cp = ((s[0] & 0x1F) << 6) | (s[1] & 0x3F); s += 2; } + else if ((s[0] & 0xF0) == 0xE0 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) { cp = ((s[0] & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F); s += 3; } + else if ((s[0] & 0xF8) == 0xF0 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80 && (s[3] & 0xC0) == 0x80) { cp = ((s[0] & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F); s += 4; } + else { cp = '?'; s += 1; } + if (cp <= 0xFFFF) wide[out++] = (WCHAR)cp; + else if (cp <= 0x10FFFF && out < wide_len - 2) { cp -= 0x10000; wide[out++] = (WCHAR)(0xD800 | (cp >> 10)); wide[out++] = (WCHAR)(0xDC00 | (cp & 0x3FF)); } + } + wide[out] = 0; + return out; +} + +static inline FILE* fopen_utf8(const char* path, const char* mode) { + return fopen(path, mode); +} + +static inline FILE* fopen_prealloc_utf8(const char* path, uint64_t size) { + WCHAR wpath[1024]; + if (!utf8_to_wide(path, wpath, 1024)) return NULL; + return lib_wfopen_prealloc(wpath, size); +} + +static inline int mkdir_utf8(const char* path) { + WCHAR wpath[1024]; + if (!utf8_to_wide(path, wpath, 1024)) return -1; + return _wmkdir(wpath); +} + +static inline int remove_utf8(const char* path) { + WCHAR wpath[1024]; + if (!utf8_to_wide(path, wpath, 1024)) return -1; + return _wremove(wpath); +} + +#define FOPEN fopen_utf8 +#define FOPEN_PREALLOC fopen_prealloc_utf8 +#define FWRITE_DIRECT lib_fwrite_direct +#define MKDIR(path) mkdir_utf8(path) +#define REMOVE remove_utf8 +#define RMDIR(path) _rmdir(path) + +static inline bool set_file_times(const char* path, uint64_t modified_time, uint64_t access_time) { + WCHAR wpath[1024]; + if (!utf8_to_wide(path, wpath, 1024)) return false; + + struct { int64_t actime; int64_t modtime; } times; + times.modtime = (int64_t)((modified_time / 10000000ULL) - 11644473600ULL); + times.actime = (int64_t)((access_time / 10000000ULL) - 11644473600ULL); + return _wutime(wpath, ×) == 0; +} + +static inline bool set_dir_times(const char* path, uint64_t modified_time, uint64_t access_time) { + WCHAR wpath[1024]; + if (!utf8_to_wide(path, wpath, 1024)) return false; + + struct { int64_t actime; int64_t modtime; } times; + times.modtime = (int64_t)((modified_time / 10000000ULL) - 11644473600ULL); + times.actime = (int64_t)((access_time / 10000000ULL) - 11644473600ULL); + return lib_wutime_dir(wpath, ×) == 0; +} + +static inline bool set_file_times_handle(FILE* f, uint64_t modified_time, uint64_t access_time) { + return lib_set_file_times_ntfs(f, (int64_t)modified_time, (int64_t)access_time); +} + +#else + +#define FOPEN fopen +#define FOPEN_PREALLOC(path, size) fopen(path, "wb") +#define FWRITE_DIRECT(f, buf, size) fwrite(buf, 1, size, f) +#define MKDIR(path) mkdir(path) +#define REMOVE remove +#define RMDIR(path) rmdir(path) + +static inline bool set_file_times(const char* path, uint64_t modified_time, uint64_t access_time) { + struct linux_timespec times[2]; + int64_t unix_mtime = (int64_t)((modified_time / 10000000ULL) - 11644473600ULL); + int64_t unix_atime = (int64_t)((access_time / 10000000ULL) - 11644473600ULL); + times[0].tv_sec = unix_atime; + times[0].tv_nsec = (access_time % 10000000ULL) * 100; + times[1].tv_sec = unix_mtime; + times[1].tv_nsec = (modified_time % 10000000ULL) * 100; + return syscall4(SYS_utimensat, AT_FDCWD, (long)path, (long)times, 0) == 0; +} + +static inline bool set_dir_times(const char* path, uint64_t modified_time, uint64_t access_time) { + return set_file_times(path, modified_time, access_time); +} + +static inline bool set_file_times_handle(FILE* f, uint64_t modified_time, uint64_t access_time) { + (void)f; (void)modified_time; (void)access_time; + return true; +} + +#endif + +static inline uint64_t exfat_timestamp_to_ntfs(uint32_t exfat_ts, uint8_t centiseconds, int8_t utc_offset) { + uint32_t second = (exfat_ts & 0x1F) * 2; + uint32_t minute = (exfat_ts >> 5) & 0x3F; + uint32_t hour = (exfat_ts >> 11) & 0x1F; + uint32_t day = (exfat_ts >> 16) & 0x1F; + uint32_t month = (exfat_ts >> 21) & 0x0F; + uint32_t year = ((exfat_ts >> 25) & 0x7F) + 1980; + + uint64_t days = 0; + for (uint32_t y = 1601; y < year; y++) + days += (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)) ? 366 : 365; + static const uint16_t month_days[] = {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; + if (month >= 1 && month <= 12) { + days += month_days[month - 1]; + if (month > 2 && (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0))) days += 1; + } + days += day - 1; + + uint64_t intervals = days * 24ULL * 60 * 60 * 10000000ULL; + intervals += hour * 60ULL * 60 * 10000000ULL; + intervals += minute * 60ULL * 10000000ULL; + intervals += second * 10000000ULL; + intervals += centiseconds * 100000ULL; + + int64_t offset_seconds = (int64_t)utc_offset * 15 * 60; + intervals -= offset_seconds * 10000000LL; + + return intervals; +} + +#define MAX_PATH_LENGTH 256 +#define MAX_FILENAME_LENGTH 256 + +#ifndef min + #define min(a,b) ((a) < (b) ? (a) : (b)) +#endif + +static inline size_t utf16_to_utf8_common(const uint16_t* utf16, int utf16_len, char* utf8, size_t utf8_size) { + size_t out_pos = 0; + for (int i = 0; i < utf16_len && out_pos < utf8_size - 1; i++) { + uint16_t c = utf16[i]; + if (c < 0x80) utf8[out_pos++] = (char)c; + else if (c < 0x800) { + if (out_pos + 2 > utf8_size - 1) break; + utf8[out_pos++] = (char)(0xC0 | (c >> 6)); + utf8[out_pos++] = (char)(0x80 | (c & 0x3F)); + } else { + if (out_pos + 3 > utf8_size - 1) break; + utf8[out_pos++] = (char)(0xE0 | (c >> 12)); + utf8[out_pos++] = (char)(0x80 | ((c >> 6) & 0x3F)); + utf8[out_pos++] = (char)(0x80 | (c & 0x3F)); + } + } + utf8[out_pos] = '\0'; + return out_pos; +} + +#define utf16_to_utf8 utf16_to_utf8_common + +static inline void sanitize_filename(char* name) { + for (char* p = name; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c < 0x20 || c == '<' || c == '>' || c == ':' || c == '"' || c == '|' || c == '?' || c == '*' || c == '\\' || c == '/') + *p = '_'; + } +} + +static inline void fs_name_to_utf8(const uint16_t* utf16, int len, char* utf8, size_t utf8_size) { + if (!utf8 || utf8_size == 0) return; + utf16_to_utf8(utf16, min(len, MAX_FILENAME_LENGTH - 1), utf8, utf8_size); + sanitize_filename(utf8); +} + +#define STRCPY_S(dst, size, src) do { strncpy(dst, src, (size)-1); (dst)[(size)-1] = '\0'; } while(0) +#define STRCAT_S(dst, size, src) strncat(dst, src, (size) - strlen(dst) - 1) + +static inline bool is_safe_path(const char* name) { + if (!name || name[0] == '\0') return false; + if (name[0] == '\\' || name[0] == '/') return false; + if (name[0] == '.' && name[1] == '.') return false; + if (strstr(name, "/..") || strstr(name, "\\..")) return false; + if (strchr(name, ':') != NULL) return false; + return true; +} + +static inline uint32_t dir_hash(const char* s, size_t len) { + uint32_t h = 5381; + for (size_t i = 0; i < len; i++) h = ((h << 5) + h) ^ s[i]; + return h; +} + +#define DIR_CACHE_BITS 10 +#define DIR_CACHE_SIZE (1 << DIR_CACHE_BITS) + +extern uint32_t g_dir_cache[DIR_CACHE_SIZE]; +extern bool g_dir_cache_init; + +static inline bool create_directories(const char* path) { + if (!path || path[0] == '\0' || strstr(path, "..") != NULL) return false; + + if (!g_dir_cache_init) { memset(g_dir_cache, 0, sizeof(g_dir_cache)); g_dir_cache_init = true; } + + size_t path_len = strlen(path); + uint32_t h = dir_hash(path, path_len); + uint32_t idx = h & (DIR_CACHE_SIZE - 1); + + if (g_dir_cache[idx] == h) return true; + if (path_len >= MAX_PATH_LENGTH) return false; + + char tmp[MAX_PATH_LENGTH]; + memcpy(tmp, path, path_len + 1); + + bool success = true; + char* p = tmp; + +#ifdef PLATFORM_WINDOWS + if (path_len > 2 && p[1] == ':') { + p += 2; + if (*p == '\\' || *p == '/') p++; + } +#else + if (*p == '/') p++; +#endif + + while ((p = strchr(p, PATH_SEP_CHAR)) != NULL) { + *p = '\0'; + int result = MKDIR(tmp); + if (result != 0 && errno != EEXIST) { success = false; break; } + *p = PATH_SEP_CHAR; + p++; + } + + if (success && tmp[0] != '\0') { + int result = MKDIR(tmp); + if (result != 0 && errno != EEXIST) success = false; + } + + if (success) g_dir_cache[idx] = h; + return success; +} + +#endif diff --git a/include/crypto.h b/include/crypto.h new file mode 100644 index 0000000..05dbe7a --- /dev/null +++ b/include/crypto.h @@ -0,0 +1,35 @@ +#ifndef CRYPTO_H +#define CRYPTO_H + +#include "lib.h" + +extern const uint8_t NTFS_HEADER[16]; +extern const uint8_t EXFAT_HEADER[16]; +extern const uint8_t OPTION_KEY[16]; +extern const uint8_t OPTION_IV[16]; +extern const uint8_t APM3_SEED[96]; +extern const uint8_t APM3_KEY[16]; +extern const uint8_t APM3_IV[16]; + +typedef struct { + uint8_t key[16]; + uint8_t iv[16]; + bool has_iv; + bool external; +} GameKeys; + +typedef struct { + const char* game_id; + uint8_t key[16]; + uint8_t iv[16]; + bool has_iv; +} GameKeyEntry; + +bool key_lookup(const char* id, uint8_t key[16], uint8_t iv[16], bool* from_external); +bool key_any(void); +void iv_page(uint64_t off, const uint8_t* base, uint8_t* out); +bool iv_file(const uint8_t key[16], const uint8_t* hdr, const uint8_t* page, uint8_t out[16]); +bool key_game(const char* id, GameKeys* out); +bool key_derive(const char* id, uint8_t key[16], uint8_t iv[16]); + +#endif \ No newline at end of file diff --git a/include/error.h b/include/error.h new file mode 100644 index 0000000..3c6ccc6 --- /dev/null +++ b/include/error.h @@ -0,0 +1,44 @@ +#ifndef ERROR_H +#define ERROR_H + +#include "lib.h" + +typedef enum { + ERR_OK = 0, + ERR_MEMORY, + ERR_FILE_OPEN, + ERR_FILE_READ, + ERR_FILE_WRITE, + ERR_FILE_SEEK, + ERR_INVALID_BOOTID, + ERR_UNKNOWN_CONTAINER, + ERR_KEY_NOT_FOUND, + ERR_INVALID_KEY_FILE, + ERR_AES_ALIGNMENT, + ERR_IV_CALCULATION, + ERR_INVALID_NTFS, + ERR_INVALID_EXFAT, + ERR_INVALID_VHD, + ERR_MFT_CORRUPT, + ERR_PATH_UNSAFE, + ERR_DIR_CREATE, + ERR_EXTRACTION_FAILED +} ErrorCode; + +static inline const char* error_string(ErrorCode code) { + static const char* const errs[] = { + "ok", "mem", "open", "read", "write", "seek", "bootid", "container", + "key", "keyfile", "align", "iv", "ntfs", "exfat", "vhd", "mft", "path", "mkdir", "extract" + }; + return (code < sizeof(errs)/sizeof(errs[0])) ? errs[code] : "?"; +} + +#define FAIL(code) do { \ + fprintf(stderr, "error: %s\n", error_string(code)); \ +} while(0) + +#define FAIL_MSG(code, msg) do { \ + fprintf(stderr, "error: %s - %s\n", error_string(code), (msg)); \ +} while(0) + +#endif diff --git a/include/exfat.h b/include/exfat.h new file mode 100644 index 0000000..65b9ff2 --- /dev/null +++ b/include/exfat.h @@ -0,0 +1,128 @@ +#ifndef EXFAT_H +#define EXFAT_H + +#include "lib.h" +#include "common.h" +#include "stream.h" + +#define EXFAT_ENTRY_SIZE 32 + +#define EXFAT_ENTRY_EOD 0x00 +#define EXFAT_ENTRY_BITMAP 0x81 +#define EXFAT_ENTRY_FILE 0x85 +#define EXFAT_ENTRY_STREAM 0xC0 +#define EXFAT_ENTRY_FILENAME 0xC1 + +#pragma pack(push, 1) + +typedef struct { + uint8_t jump_boot[3]; + uint8_t fs_name[8]; + uint8_t must_be_zero[53]; + uint64_t partition_offset; + uint64_t volume_length; + uint32_t fat_offset; + uint32_t fat_length; + uint32_t cluster_heap_offset; + uint32_t cluster_count; + uint32_t first_cluster_of_root_dir; + uint32_t volume_serial_number; + uint16_t fs_revision; + uint16_t volume_flags; + uint8_t bytes_per_sector_shift; + uint8_t sectors_per_cluster_shift; + uint8_t number_of_fats; + uint8_t drive_select; + uint8_t percent_in_use; + uint8_t reserved[7]; + uint8_t boot_code[390]; + uint16_t boot_signature; +} ExfatBootSector; + +typedef struct { + uint8_t entry_type; + uint8_t secondary_count; + uint16_t set_checksum; + uint16_t file_attributes; + uint16_t reserved1; + uint32_t create_timestamp; + uint32_t last_modified_timestamp; + uint32_t last_access_timestamp; + uint8_t create_10ms; + uint8_t last_modified_10ms; + uint8_t create_utc_offset; + uint8_t last_modified_utc_offset; + uint8_t last_access_utc_offset; + uint8_t reserved2[7]; +} ExfatFileEntry; + +typedef struct { + uint8_t entry_type; + uint8_t flags; + uint8_t reserved1; + uint8_t name_length; + uint16_t name_hash; + uint16_t reserved2; + uint64_t valid_data_length; + uint32_t reserved3; + uint32_t first_cluster; + uint64_t data_length; +} ExfatStreamEntry; + +typedef struct { + uint8_t entry_type; + uint8_t flags; + uint16_t file_name[15]; +} ExfatFileNameEntry; + +#pragma pack(pop) + +typedef struct { + char name[MAX_PATH_LENGTH]; + uint32_t first_cluster; + uint64_t data_length; + bool is_directory; + bool no_fat_chain; + uint32_t modify_timestamp; + uint32_t access_timestamp; + uint8_t modify_10ms; + int8_t modify_utc_offset; + int8_t access_utc_offset; +} ExfatFileInfo; + +typedef struct { + char path[MAX_PATH_LENGTH]; + uint64_t mtime; + uint64_t atime; +} DeferredDirTime; + +typedef struct { + FILE* fp; + ExfatBootSector boot_sector; + uint32_t bytes_per_sector; + uint32_t bytes_per_cluster; + uint32_t cluster_heap_offset_bytes; + uint32_t fat_offset_bytes; + uint32_t fat_length_bytes; + uint32_t* fat; + uint8_t* cluster_buf; + uint8_t* io_buf; + uint64_t total_bytes; + uint64_t extracted_bytes; + uint64_t files_extracted; + void* progress; + bool silent; + bool verbose; + DecryptStream* stream; + char last_dir[MAX_PATH_LENGTH]; + DeferredDirTime* deferred_dirs; + uint32_t deferred_count; + uint32_t deferred_capacity; +} ExfatContext; + +bool exfat_init(ExfatContext* ctx, const char* filename); +bool exfat_init_stream(ExfatContext* ctx, DecryptStream* stream); +bool exfat_extract_all(ExfatContext* ctx, const char* output_dir); +void exfat_close(ExfatContext* ctx); + +#endif diff --git a/include/keys.h b/include/keys.h new file mode 100644 index 0000000..5acfb8b --- /dev/null +++ b/include/keys.h @@ -0,0 +1,6 @@ +static const GameKeyEntry embedded_keys[] = { +#if __has_include("keys.inc") +#include "keys.inc" +#endif +{NULL, {0}, {0}, false} +}; diff --git a/include/keys.inc.example b/include/keys.inc.example new file mode 100644 index 0000000..7b3a49c --- /dev/null +++ b/include/keys.inc.example @@ -0,0 +1,3 @@ +// copy to keys.inc and add your keys +// format: {"SXXX", {key[16]}, {iv[16]}, has_iv}, +// {"XXXX", {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00}, true}, diff --git a/include/lib.h b/include/lib.h new file mode 100644 index 0000000..ec68f35 --- /dev/null +++ b/include/lib.h @@ -0,0 +1,638 @@ +#ifndef LIB_H +#define LIB_H + +#if defined(_WIN32) || defined(_WIN64) + #define PLATFORM_WINDOWS 1 +#elif defined(__linux__) && defined(__x86_64__) + #define PLATFORM_LINUX 1 +#else + #error "unsupported platform" +#endif + +typedef signed char int8_t; +typedef unsigned char uint8_t; +typedef signed short int16_t; +typedef unsigned short uint16_t; +typedef signed int int32_t; +typedef unsigned int uint32_t; +typedef signed long long int64_t; +typedef unsigned long long uint64_t; + +#ifdef PLATFORM_WINDOWS +typedef uint64_t size_t; +typedef int64_t ssize_t; +typedef uint64_t uintptr_t; +typedef int64_t intptr_t; +#else +typedef unsigned long size_t; +typedef long ssize_t; +typedef unsigned long uintptr_t; +typedef long intptr_t; +#endif + +typedef int64_t off_t; +typedef int64_t time_t; +typedef int64_t __time64_t; + +#ifndef __bool_true_false_are_defined + #if __STDC_VERSION__ < 202311L + typedef _Bool bool; + #define true 1 + #define false 0 + #endif + #define __bool_true_false_are_defined 1 +#endif + +#define NULL ((void*)0) + +typedef __builtin_va_list va_list; +#define va_start(ap, last) __builtin_va_start(ap, last) +#define va_end(ap) __builtin_va_end(ap) +#define va_arg(ap, type) __builtin_va_arg(ap, type) + +#ifdef __cplusplus +extern "C" { +#endif + +#define EPERM 1 +#define ENOENT 2 +#define ESRCH 3 +#define EINTR 4 +#define EIO 5 +#define ENXIO 6 +#define EBADF 9 +#define ENOMEM 12 +#define EACCES 13 +#define EFAULT 14 +#define EEXIST 17 +#define ENOTDIR 20 +#define EISDIR 21 +#define EINVAL 22 +#define EMFILE 24 +#define ENOSPC 28 +#define EROFS 30 +#define ERANGE 34 + +#ifdef PLATFORM_WINDOWS + +typedef long NTSTATUS; +typedef void* HANDLE; +typedef uint16_t WCHAR; +typedef uint32_t ULONG; +typedef int32_t LONG; +typedef uint64_t ULONGLONG; +typedef int64_t LONGLONG; + +#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0) +#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) +#define STATUS_END_OF_FILE ((NTSTATUS)0xC0000011L) +#define STATUS_NO_MORE_FILES ((NTSTATUS)0x80000006L) +#define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) + +#define INVALID_HANDLE_VALUE ((HANDLE)(intptr_t)-1) + +#define FILE_READ_DATA 0x0001 +#define FILE_WRITE_DATA 0x0002 +#define FILE_APPEND_DATA 0x0004 +#define FILE_READ_ATTRIBUTES 0x0080 +#define FILE_WRITE_ATTRIBUTES 0x0100 +#define FILE_LIST_DIRECTORY 0x0001 +#define DELETE 0x00010000L +#define SYNCHRONIZE 0x00100000L + +#define FILE_SHARE_READ 0x00000001 +#define FILE_SHARE_WRITE 0x00000002 +#define FILE_SHARE_DELETE 0x00000004 + +#define FILE_OPEN 0x00000001 +#define FILE_CREATE 0x00000002 +#define FILE_OVERWRITE_IF 0x00000005 +#define FILE_OPEN_IF 0x00000003 + +#define FILE_DIRECTORY_FILE 0x00000001 +#define FILE_SEQUENTIAL_ONLY 0x00000004 +#define FILE_NON_DIRECTORY_FILE 0x00000040 +#define FILE_SYNCHRONOUS_IO_NONALERT 0x00000020 +#define FILE_DELETE_ON_CLOSE 0x00001000 + +#define FILE_ATTRIBUTE_NORMAL 0x00000080 +#define FILE_ATTRIBUTE_DIRECTORY 0x00000010 + +#define OBJ_CASE_INSENSITIVE 0x00000040 + +typedef struct _UNICODE_STRING { + uint16_t Length; + uint16_t MaximumLength; + WCHAR* Buffer; +} UNICODE_STRING; + +typedef struct _OBJECT_ATTRIBUTES { + ULONG Length; + HANDLE RootDirectory; + UNICODE_STRING* ObjectName; + ULONG Attributes; + void* SecurityDescriptor; + void* SecurityQualityOfService; +} OBJECT_ATTRIBUTES; + +typedef struct _IO_STATUS_BLOCK { + union { NTSTATUS Status; void* Pointer; }; + uintptr_t Information; +} IO_STATUS_BLOCK; + +typedef struct _LARGE_INTEGER { + LONGLONG QuadPart; +} LARGE_INTEGER; + +typedef struct _FILE_POSITION_INFORMATION { + LARGE_INTEGER CurrentByteOffset; +} FILE_POSITION_INFORMATION; + +typedef struct _FILE_STANDARD_INFORMATION { + LARGE_INTEGER AllocationSize; + LARGE_INTEGER EndOfFile; + ULONG NumberOfLinks; + uint8_t DeletePending; + uint8_t Directory; +} FILE_STANDARD_INFORMATION; + +typedef struct _FILE_BASIC_INFORMATION { + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + ULONG FileAttributes; +} FILE_BASIC_INFORMATION; + +typedef struct _FILE_BOTH_DIR_INFORMATION { + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + uint8_t ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_BOTH_DIR_INFORMATION; + +typedef enum _FILE_INFORMATION_CLASS { + FileBasicInformation = 4, + FileStandardInformation = 5, + FilePositionInformation = 14, + FileBothDirectoryInformation = 3, +} FILE_INFORMATION_CLASS; + +typedef struct _RTL_USER_PROCESS_PARAMETERS { + uint8_t _Reserved0[0x20]; + HANDLE StandardInput; + HANDLE StandardOutput; + HANDLE StandardError; + uint8_t _Reserved1[0x38]; + UNICODE_STRING CommandLine; + WCHAR* Environment; +} RTL_USER_PROCESS_PARAMETERS; + +typedef struct _PEB { + uint8_t _Reserved0[0x20]; + RTL_USER_PROCESS_PARAMETERS* ProcessParameters; + void* SubSystemData; + HANDLE ProcessHeap; +} PEB; + +typedef struct _PROCESS_BASIC_INFORMATION { + NTSTATUS ExitStatus; + void* PebBaseAddress; + uintptr_t AffinityMask; + int32_t BasePriority; + uintptr_t UniqueProcessId; + uintptr_t InheritedFromUniqueProcessId; +} PROCESS_BASIC_INFORMATION; + +typedef struct _SYSTEM_PROCESS_INFORMATION { + ULONG NextEntryOffset; + ULONG NumberOfThreads; + uint8_t Reserved1[48]; + UNICODE_STRING ImageName; + int32_t BasePriority; + uintptr_t UniqueProcessId; +} SYSTEM_PROCESS_INFORMATION; + +#define ProcessBasicInformation 0 +#define SystemProcessInformation 5 + +#define CRT_OFFSETOF(type, member) ((size_t)&(((type*)0)->member)) + +__declspec(dllimport) NTSTATUS __stdcall NtCreateFile(HANDLE*, ULONG, OBJECT_ATTRIBUTES*, IO_STATUS_BLOCK*, LARGE_INTEGER*, ULONG, ULONG, ULONG, ULONG, void*, ULONG); +__declspec(dllimport) NTSTATUS __stdcall NtReadFile(HANDLE, HANDLE, void*, void*, IO_STATUS_BLOCK*, void*, ULONG, LARGE_INTEGER*, ULONG*); +__declspec(dllimport) NTSTATUS __stdcall NtWriteFile(HANDLE, HANDLE, void*, void*, IO_STATUS_BLOCK*, const void*, ULONG, LARGE_INTEGER*, ULONG*); +__declspec(dllimport) NTSTATUS __stdcall NtClose(HANDLE); +__declspec(dllimport) NTSTATUS __stdcall NtQueryInformationFile(HANDLE, IO_STATUS_BLOCK*, void*, ULONG, FILE_INFORMATION_CLASS); +__declspec(dllimport) NTSTATUS __stdcall NtSetInformationFile(HANDLE, IO_STATUS_BLOCK*, void*, ULONG, FILE_INFORMATION_CLASS); +__declspec(dllimport) NTSTATUS __stdcall NtQueryDirectoryFile(HANDLE, HANDLE, void*, void*, IO_STATUS_BLOCK*, void*, ULONG, FILE_INFORMATION_CLASS, uint8_t, UNICODE_STRING*, uint8_t); +__declspec(dllimport) NTSTATUS __stdcall NtQuerySystemTime(LARGE_INTEGER*); +__declspec(dllimport) NTSTATUS __stdcall NtQueryInformationProcess(HANDLE, ULONG, void*, ULONG, ULONG*); +__declspec(dllimport) NTSTATUS __stdcall NtQuerySystemInformation(ULONG, void*, ULONG, ULONG*); +__declspec(dllimport) NTSTATUS __stdcall NtDelayExecution(uint8_t, LARGE_INTEGER*); +__declspec(dllimport) void* __stdcall RtlAllocateHeap(HANDLE, ULONG, size_t); +__declspec(dllimport) void* __stdcall RtlReAllocateHeap(HANDLE, ULONG, void*, size_t); +__declspec(dllimport) uint8_t __stdcall RtlFreeHeap(HANDLE, ULONG, void*); +__declspec(dllimport) void __stdcall RtlExitUserProcess(NTSTATUS); +__declspec(dllimport) NTSTATUS __stdcall RtlDosPathNameToNtPathName_U_WithStatus(const WCHAR*, UNICODE_STRING*, WCHAR**, void*); +__declspec(dllimport) void __stdcall RtlFreeUnicodeString(UNICODE_STRING*); + +static inline PEB* lib_get_peb(void) { + PEB* peb; + __asm__ volatile ("mov %%gs:0x60, %0" : "=r"(peb)); + return peb; +} + +#else + +#define SYS_read 0 +#define SYS_write 1 +#define SYS_open 2 +#define SYS_close 3 +#define SYS_fstat 5 +#define SYS_lseek 8 +#define SYS_mmap 9 +#define SYS_mprotect 10 +#define SYS_munmap 11 +#define SYS_brk 12 +#define SYS_ioctl 16 +#define SYS_access 21 +#define SYS_dup2 33 +#define SYS_getpid 39 +#define SYS_exit 60 +#define SYS_uname 63 +#define SYS_fcntl 72 +#define SYS_fsync 74 +#define SYS_ftruncate 77 +#define SYS_getdents64 217 +#define SYS_fadvise64 221 +#define SYS_exit_group 231 +#define SYS_openat 257 +#define SYS_mkdirat 258 +#define SYS_newfstatat 262 +#define SYS_unlinkat 263 +#define SYS_utimensat 280 + +#define O_RDONLY 0x0000 +#define O_WRONLY 0x0001 +#define O_RDWR 0x0002 +#define O_CREAT 0x0040 +#define O_EXCL 0x0080 +#define O_TRUNC 0x0200 +#define O_APPEND 0x0400 +#define O_DIRECTORY 0x10000 +#define O_CLOEXEC 0x80000 + +#define S_IRWXU 0700 +#define S_IRUSR 0400 +#define S_IWUSR 0200 +#define S_IXUSR 0100 +#define S_IRWXG 0070 +#define S_IRGRP 0040 +#define S_IXGRP 0010 +#define S_IRWXO 0007 +#define S_IROTH 0004 +#define S_IXOTH 0001 + +#define S_IFMT 0170000 +#define S_IFDIR 0040000 +#define S_IFREG 0100000 +#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) +#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) + +#define PROT_READ 0x1 +#define PROT_WRITE 0x2 +#define MAP_PRIVATE 0x02 +#define MAP_ANONYMOUS 0x20 +#define MAP_FAILED ((void*)-1) + +#define SEEK_SET 0 +#define SEEK_CUR 1 +#define SEEK_END 2 + +#define AT_FDCWD (-100) + +#define DT_DIR 4 +#define DT_REG 8 + +struct linux_dirent64 { + uint64_t d_ino; + int64_t d_off; + uint16_t d_reclen; + uint8_t d_type; + char d_name[]; +}; + +struct linux_stat { + uint64_t st_dev; + uint64_t st_ino; + uint64_t st_nlink; + uint32_t st_mode; + uint32_t st_uid; + uint32_t st_gid; + uint32_t __pad0; + uint64_t st_rdev; + int64_t st_size; + int64_t st_blksize; + int64_t st_blocks; + int64_t st_atime_sec; + int64_t st_atime_nsec; + int64_t st_mtime_sec; + int64_t st_mtime_nsec; + int64_t st_ctime_sec; + int64_t st_ctime_nsec; + int64_t __unused[3]; +}; + +struct linux_timespec { + int64_t tv_sec; + int64_t tv_nsec; +}; + +static inline long syscall0(long n) { + long ret; + __asm__ volatile ("syscall" : "=a"(ret) : "a"(n) : "rcx", "r11", "memory"); + return ret; +} + +static inline long syscall1(long n, long a1) { + long ret; + __asm__ volatile ("syscall" : "=a"(ret) : "a"(n), "D"(a1) : "rcx", "r11", "memory"); + return ret; +} + +static inline long syscall2(long n, long a1, long a2) { + long ret; + __asm__ volatile ("syscall" : "=a"(ret) : "a"(n), "D"(a1), "S"(a2) : "rcx", "r11", "memory"); + return ret; +} + +static inline long syscall3(long n, long a1, long a2, long a3) { + long ret; + __asm__ volatile ("syscall" : "=a"(ret) : "a"(n), "D"(a1), "S"(a2), "d"(a3) : "rcx", "r11", "memory"); + return ret; +} + +static inline long syscall4(long n, long a1, long a2, long a3, long a4) { + long ret; + register long r10 __asm__("r10") = a4; + __asm__ volatile ("syscall" : "=a"(ret) : "a"(n), "D"(a1), "S"(a2), "d"(a3), "r"(r10) : "rcx", "r11", "memory"); + return ret; +} + +static inline long syscall5(long n, long a1, long a2, long a3, long a4, long a5) { + long ret; + register long r10 __asm__("r10") = a4; + register long r8 __asm__("r8") = a5; + __asm__ volatile ("syscall" : "=a"(ret) : "a"(n), "D"(a1), "S"(a2), "d"(a3), "r"(r10), "r"(r8) : "rcx", "r11", "memory"); + return ret; +} + +static inline long syscall6(long n, long a1, long a2, long a3, long a4, long a5, long a6) { + long ret; + register long r10 __asm__("r10") = a4; + register long r8 __asm__("r8") = a5; + register long r9 __asm__("r9") = a6; + __asm__ volatile ("syscall" : "=a"(ret) : "a"(n), "D"(a1), "S"(a2), "d"(a3), "r"(r10), "r"(r8), "r"(r9) : "rcx", "r11", "memory"); + return ret; +} + +#endif + +#define _IOFBF 0 +#define _IOLBF 1 +#define _IONBF 2 + +#define _A_SUBDIR 0x10 + +#ifndef SEEK_SET +#define SEEK_SET 0 +#define SEEK_CUR 1 +#define SEEK_END 2 +#endif + +#define EOF (-1) + +#ifdef PLATFORM_WINDOWS +extern HANDLE lib_heap; +extern HANDLE lib_stdout_handle; +extern HANDLE lib_stderr_handle; +extern HANDLE lib_stdin_handle; +#endif + +extern int lib_errno_val; + +void* lib_malloc(size_t size); +void* lib_calloc(size_t count, size_t size); +void* lib_realloc(void* ptr, size_t size); +void lib_free(void* ptr); + +#define malloc lib_malloc +#define calloc lib_calloc +#define realloc lib_realloc +#define free lib_free + +size_t lib_strlen(const char* s); +char* lib_strcpy(char* dst, const char* src); +char* lib_strncpy(char* dst, const char* src, size_t n); +int lib_strcmp(const char* s1, const char* s2); +int lib_strncmp(const char* s1, const char* s2, size_t n); +char* lib_strchr(const char* s, int c); +char* lib_strrchr(const char* s, int c); +char* lib_strstr(const char* haystack, const char* needle); +char* lib_strncat(char* dst, const char* src, size_t n); +void* lib_memcpy(void* dst, const void* src, size_t n); +void* lib_memset(void* dst, int c, size_t n); +int lib_memcmp(const void* s1, const void* s2, size_t n); + +#define strlen lib_strlen +#define strcpy lib_strcpy +#define strncpy lib_strncpy +#define strcmp lib_strcmp +#define strncmp lib_strncmp +#define strchr lib_strchr +#define strrchr lib_strrchr +#define strstr lib_strstr +#define strncat lib_strncat + +#define memcpy lib_memcpy +#define memset lib_memset +#define memcmp lib_memcmp + +#define LIB_FILE_READ 0x01 +#define LIB_FILE_WRITE 0x02 +#define LIB_FILE_EOF 0x04 +#define LIB_FILE_ERROR 0x08 +#define LIB_FILE_APPEND 0x10 + +#define LIB_IOBUF_SIZE (64 * 1024) + +typedef struct { +#ifdef PLATFORM_WINDOWS + HANDLE handle; +#else + int fd; +#endif + uint8_t* buffer; + uint32_t buf_cap; + uint32_t buf_pos; + uint32_t buf_fill; + int64_t file_pos; + uint32_t flags; +} FILE; + +extern FILE* lib_stdout_file; +extern FILE* lib_stderr_file; +extern FILE* lib_stdin_file; + +#define stdin lib_stdin_file +#define stdout lib_stdout_file +#define stderr lib_stderr_file + +FILE* lib_fopen(const char* path, const char* mode); +size_t lib_fread(void* buf, size_t size, size_t count, FILE* f); +size_t lib_fwrite(const void* buf, size_t size, size_t count, FILE* f); +int lib_fseek(FILE* f, long offset, int whence); +int lib_fseeki64(FILE* f, int64_t offset, int whence); +long lib_ftell(FILE* f); +int64_t lib_ftelli64(FILE* f); +int lib_fclose(FILE* f); +int lib_fflush(FILE* f); +int lib_feof(FILE* f); +char* lib_fgets(char* buf, int n, FILE* f); +void lib_rewind(FILE* f); +void lib_setvbuf(FILE* f, char* buf, int mode, size_t size); + +#define fopen lib_fopen +#define fread lib_fread +#define fwrite lib_fwrite +#define fseek lib_fseek +#define _fseeki64 lib_fseeki64 +#define fseeko lib_fseeki64 +#define ftell lib_ftell +#define _ftelli64 lib_ftelli64 +#define ftello lib_ftelli64 +#define fclose lib_fclose +#define fflush lib_fflush +#define feof lib_feof +#define fgets lib_fgets +#define rewind lib_rewind +#define setvbuf lib_setvbuf + +#ifdef PLATFORM_WINDOWS +FILE* lib_wfopen(const WCHAR* path, const WCHAR* mode); +FILE* lib_wfopen_prealloc(const WCHAR* path, uint64_t size); +bool lib_set_file_times_ntfs(FILE* f, int64_t modified_time, int64_t access_time); +int lib_wutime(const WCHAR* path, const void* times); +int lib_wutime_dir(const WCHAR* path, const void* times); +#define _wfopen lib_wfopen +#define _wutime lib_wutime +#endif + +size_t lib_fwrite_direct(FILE* f, const void* buf, size_t size); + +int lib_printf(const char* fmt, ...); +int lib_fprintf(FILE* f, const char* fmt, ...); +int lib_snprintf(char* buf, size_t n, const char* fmt, ...); +int lib_vprintf(const char* fmt, va_list ap); +int lib_vfprintf(FILE* f, const char* fmt, va_list ap); +int lib_vsnprintf(char* buf, size_t n, const char* fmt, va_list ap); +int lib_puts(const char* s); + +#define printf lib_printf +#define fprintf lib_fprintf +#define snprintf lib_snprintf +#define vprintf lib_vprintf +#define vfprintf lib_vfprintf +#define vsnprintf lib_vsnprintf +#define puts lib_puts + +int lib_mkdir(const char* path); +int lib_rmdir(const char* path); +int lib_remove(const char* path); + +#define mkdir lib_mkdir +#define _mkdir lib_mkdir +#define rmdir lib_rmdir +#define _rmdir lib_rmdir +#define remove lib_remove + +#ifdef PLATFORM_WINDOWS +int lib_wmkdir(const WCHAR* path); +int lib_wremove(const WCHAR* path); +#define _wmkdir lib_wmkdir +#define _wremove lib_wremove +#endif + +typedef struct { + uint32_t attrib; + int64_t time_create; + int64_t time_access; + int64_t time_write; + int64_t size; + char name[260]; +} lib_finddata_t; + +intptr_t lib_findfirst(const char* pattern, lib_finddata_t* data); +int lib_findnext(intptr_t handle, lib_finddata_t* data); +int lib_findclose(intptr_t handle); + +#define _finddata_t lib_finddata_t +#define _finddata64_t lib_finddata_t +#define _findfirst lib_findfirst +#define _findfirst64 lib_findfirst +#define _findnext lib_findnext +#define _findnext64 lib_findnext +#define _findclose lib_findclose + +int lib_isatty(int fd); +#define isatty lib_isatty +#define _isatty lib_isatty + +time_t lib_time(time_t* t); +double lib_difftime(time_t t1, time_t t0); + +#define time lib_time +#define _time64 lib_time +#define difftime lib_difftime + +int lib_atoi(const char* s); +int lib_isxdigit(int c); + +#define atoi lib_atoi +#define isxdigit lib_isxdigit + +int* lib_errno_func(void); + +#define errno (*lib_errno_func()) + +void lib_exit(int status); + +#define exit lib_exit +#define _exit lib_exit + +#ifdef PLATFORM_WINDOWS +#define PATH_SEPARATOR "\\" +#define PATH_SEP_CHAR '\\' +#else +#define PATH_SEPARATOR "/" +#define PATH_SEP_CHAR '/' +#endif + +void lib_init(void); +int lib_main(int argc, char** argv); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/ntfs.h b/include/ntfs.h new file mode 100644 index 0000000..73fd22e --- /dev/null +++ b/include/ntfs.h @@ -0,0 +1,280 @@ +#ifndef NTFS_H +#define NTFS_H + +#include "lib.h" +#include "common.h" +#include "stream.h" + +#define VHD_FOOTER_SIZE 512 +#define VHD_SECTOR_SIZE 512 +#define VHD_BAT_ENTRY_RESERVED 0xFFFFFFFF +#define NTFS_RECORD_SIZE 1024 +#define MFT_RECORD_MAGIC "FILE" +#define VHD_COOKIE "conectix" +#define VHD_DYNAMIC_COOKIE "cxsparse" +#define VHD_TYPE_FIXED 2 +#define VHD_TYPE_DYNAMIC 3 +#define VHD_TYPE_DIFFERENCING 4 +#define VHD_MAX_CHAIN_DEPTH 8 +#define FILE_NAME_ATTR 0x30 +#define DATA_ATTR 0x80 +#define INDEX_ROOT_ATTR 0x90 +#define INDEX_ALLOCATION_ATTR 0xA0 +#define NTFS_SIGNATURE "NTFS " +#define NTFS_PARTITION_TYPE 0x07 +#define MFT_RECORD_IN_USE 0x0001 +#define MFT_RECORD_IS_DIRECTORY 0x0002 +#define DIR_CACHE_INITIAL_SIZE 2048 +#define NTFS_MAX_RECURSION_DEPTH 64 + +typedef struct { + uint64_t ref_number; + char path[MAX_PATH_LENGTH]; + bool occupied; +} DirectoryEntry; + +typedef struct { + DirectoryEntry* entries; + size_t capacity; + size_t count; +} DirectoryCache; + +#pragma pack(push, 1) + +typedef struct { + char cookie[8]; + uint32_t features; + uint32_t version; + uint64_t data_offset; + uint32_t timestamp; + uint32_t creator_app; + uint32_t creator_ver; + uint32_t creator_os; + uint64_t original_size; + uint64_t current_size; + uint16_t cylinder; + uint8_t heads; + uint8_t sectors; + uint32_t disk_type; + uint32_t checksum; + uint8_t unique_id[16]; + uint8_t saved_state; + uint8_t reserved[427]; +} VHDFooter; + +typedef struct { + uint32_t platform_code; + uint32_t platform_data_space; + uint32_t platform_data_length; + uint32_t reserved; + uint64_t platform_data_offset; +} VHDParentLocator; + +typedef struct { + char cookie[8]; + uint64_t data_offset; + uint64_t bat_offset; + uint32_t head_vers; + uint32_t max_bat_entries; + uint32_t block_size; + uint32_t checksum; + uint8_t parent_id[16]; + uint32_t parent_timestamp; + uint32_t reserved1; + uint16_t parent_name[256]; + VHDParentLocator parent_loc[8]; + uint8_t reserved2[256]; +} VHDDynamicHeader; + +typedef struct { + uint8_t jump[3]; + uint8_t signature[8]; + uint16_t bytes_per_sector; + uint8_t sectors_per_cluster; + uint16_t reserved_sectors; + uint8_t always_zero1[3]; + uint16_t not_used1; + uint8_t media_descriptor; + uint16_t always_zero2; + uint16_t sectors_per_track; + uint16_t number_of_heads; + uint32_t hidden_sectors; + uint32_t not_used2; + uint32_t not_used3; + uint64_t total_sectors; + uint64_t mft_cluster_number; + uint64_t mft_mirror_cluster_number; + int8_t clusters_per_mft_record; + uint8_t not_used4[3]; + int8_t clusters_per_index_record; + uint8_t not_used5[3]; + uint64_t volume_serial_number; + uint32_t checksum; +} NTFSBootSector; + +typedef struct { + char magic[4]; + uint16_t usa_offset; + uint16_t usa_count; + uint64_t lsn; + uint16_t sequence_number; + uint16_t link_count; + uint16_t attrs_offset; + uint16_t flags; + uint32_t bytes_used; + uint32_t bytes_allocated; + uint64_t base_ref; + uint16_t next_attr_id; + uint16_t record_number; + uint16_t usa_value; +} MFTRecordHeader; + +typedef struct { + uint32_t type; + uint32_t length; + uint8_t non_resident; + uint8_t name_length; + uint16_t name_offset; + uint16_t flags; + uint16_t attribute_id; + union { + struct { + uint32_t value_length; + uint16_t value_offset; + uint16_t flags; + } resident; + struct { + uint64_t lowest_vcn; + uint64_t highest_vcn; + uint16_t mapping_pairs_offset; + uint16_t compression_unit; + uint32_t padding; + uint64_t allocated_size; + uint64_t data_size; + uint64_t initialized_size; + uint64_t compressed_size; + } non_resident; + } data; +} AttributeHeader; + +typedef struct { + uint64_t parent_directory; + uint64_t creation_time; + uint64_t modification_time; + uint64_t mft_modification_time; + uint64_t access_time; + uint64_t allocated_size; + uint64_t real_size; + uint32_t flags; + uint32_t reparse_value; + uint8_t name_length; + uint8_t namespace; + uint16_t name[256]; +} FileNameAttribute; + +#pragma pack(pop) + +typedef struct VHDRunSource { + void* ntfs_ctx; + DataRun runs[MAX_DATA_RUNS]; + int run_count; + uint64_t file_size; + uint64_t data_start_offset; + uint32_t bytes_per_cluster; +} VHDRunSource; + +typedef struct VHDContext { + FILE* fp; + VHDRunSource* run_source; + VHDFooter footer; + VHDDynamicHeader dyn_header; + uint32_t* bat; + uint32_t sector_bitmap_size; + uint8_t* sector_bitmap; + uint8_t* block_buffer; + uint32_t cached_block_idx; + bool block_cached; + struct VHDContext* parent; + uint32_t depth; + char base_dir[MAX_PATH_LENGTH]; + uint64_t file_pos; +} VHDContext; + +typedef struct { + char name[MAX_FILENAME_LENGTH]; + uint64_t parent_ref; + bool is_directory; + bool valid; +} FileInfo; + +typedef struct { + FILE* fp; +} RawNTFSContext; + +#define MAX_PENDING_VHDS 16 +#define MAX_PENDING_OPTS 16 + +typedef struct { + DataRun runs[MAX_DATA_RUNS]; + int run_count; + uint64_t file_size; + int vhd_number; +} PendingVHD; + +typedef struct { + DataRun runs[MAX_DATA_RUNS]; + int run_count; + uint64_t file_size; + uint64_t data_offset; + char filename[MAX_FILENAME_LENGTH]; +} PendingOpt; + +typedef struct { + union { + VHDContext vhd; + RawNTFSContext raw; + }; + bool is_vhd; + NTFSBootSector boot; + uint16_t bytes_per_sector; + uint32_t bytes_per_cluster; + uint64_t mft_offset; + uint32_t mft_record_size; + uint64_t mft_data_size; + uint64_t total_mft_records; + char base_path[MAX_PATH_LENGTH]; + DirectoryCache dir_cache; + uint64_t data_start_offset; + bool silent; + bool verbose; + uint64_t total_bytes; + uint64_t extracted_bytes; + uint64_t files_extracted; + void* progress; + uint64_t raw_file_pos; + DecryptStream* stream; + PendingVHD pending_vhds[MAX_PENDING_VHDS]; + int pending_vhd_count; + PendingOpt pending_opts[MAX_PENDING_OPTS]; + int pending_opt_count; + uint8_t* lookup_buffer; + bool apm3_decrypt; + uint8_t apm3_key[16]; + uint8_t apm3_iv[16]; + uint8_t* file_buffer; + char last_dir[MAX_PATH_LENGTH]; + void* deferred_dirs; + uint32_t deferred_count; + uint32_t deferred_capacity; +} NTFSContext; + +bool ntfs_init(NTFSContext* ctx, const char* vhd_path, const char* extract_path); +bool ntfs_init_stream(NTFSContext* ctx, DecryptStream* stream, const char* extract_path); +int ntfs_detect_vhd_type(NTFSContext* ctx); +bool ntfs_extract_all(NTFSContext* ctx); +bool ntfs_extract_pending_vhds(NTFSContext* ctx, bool silent, bool verbose, bool* is_orphan); +int ntfs_get_pending_opt_count(NTFSContext* ctx); +const PendingOpt* ntfs_get_pending_opt(NTFSContext* ctx, int index); +void ntfs_close(NTFSContext* ctx); + +#endif \ No newline at end of file diff --git a/include/progress.h b/include/progress.h new file mode 100644 index 0000000..ee750f2 --- /dev/null +++ b/include/progress.h @@ -0,0 +1,12 @@ +#ifndef PROGRESS_H +#define PROGRESS_H + +#include "lib.h" + +typedef struct { uint64_t total; } Progress; + +static inline void progress_init(Progress* p, uint64_t total) { p->total = total; } +static inline void progress_update(Progress* p, uint64_t current) { (void)p; (void)current; } +static inline void progress_finish(Progress* p) { (void)p; } + +#endif diff --git a/include/stream.h b/include/stream.h new file mode 100644 index 0000000..c3305a4 --- /dev/null +++ b/include/stream.h @@ -0,0 +1,46 @@ +#ifndef STREAM_H +#define STREAM_H + +#include "lib.h" +#include "aes.h" + +#define DECRYPT_PAGE_SIZE 4096 +#define MAX_DATA_RUNS 256 + +typedef struct { + uint64_t offset; + uint64_t length; +} DataRun; + +typedef struct { + void* ntfs_ctx; + DataRun runs[MAX_DATA_RUNS]; + int run_count; + uint64_t file_size; +} RunSource; + +typedef struct DecryptStream DecryptStream; + +struct DecryptStream { + FILE* fp; + DecryptStream* parent_stream; + RunSource* run_source; + uint64_t data_offset; + uint64_t data_size; + uint8_t key[16]; + uint8_t file_iv[16]; + uint8_t page_buffer[DECRYPT_PAGE_SIZE]; + uint64_t cached_page_offset; + uint64_t file_pos; + AES_ctx aes_ctx; +}; + +bool stream_init(DecryptStream* ds, FILE* fp, uint64_t data_offset, + uint64_t data_size, const uint8_t key[16], const uint8_t iv[16]); +bool stream_init_from_runs(DecryptStream* ds, RunSource* source, + const uint8_t key[16], const uint8_t iv[16]); +bool stream_read(DecryptStream* ds, void* buffer, uint64_t offset, size_t size); +bool stream_read_raw(void* ntfs_ctx, const DataRun* runs, int run_count, + uint64_t file_size, uint64_t offset, void* buffer, size_t size); + +#endif diff --git a/src/aes.c b/src/aes.c new file mode 100644 index 0000000..cfeed44 --- /dev/null +++ b/src/aes.c @@ -0,0 +1,319 @@ +#include "aes.h" + +const uint8_t aes_sbox[256] = { + 0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76, + 0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0, + 0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15, + 0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75, + 0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84, + 0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf, + 0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8, + 0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2, + 0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73, + 0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb, + 0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79, + 0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08, + 0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a, + 0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e, + 0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf, + 0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16 +}; + +const uint8_t aes_rcon[11] = {0x8d,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36}; + +static uint8_t aes_rsbox[256]; +static int aes_rsbox_init; + +static void aes_init_rsbox(void) { + if (aes_rsbox_init) return; + for (int i = 0; i < 256; i++) aes_rsbox[aes_sbox[i]] = (uint8_t)i; + aes_rsbox_init = 1; +} + +#define xtime(x) ((((x) << 1) ^ ((((x) >> 7) & 1) * 0x1b)) & 0xff) +#define mul(a,b) ((((b)&1)*a) ^ (((b>>1)&1)*xtime(a)) ^ (((b>>2)&1)*xtime(xtime(a))) ^ (((b>>3)&1)*xtime(xtime(xtime(a))))) + +#if defined(__x86_64__) || defined(_M_X64) +#define AES_HW_AVAILABLE 1 +#else +#define AES_HW_AVAILABLE 0 +int aes_hw_supported(void) { return 0; } +#endif + +#if AES_HW_AVAILABLE +static int aes_hw_checked = 0; +static int aes_hw_available = 0; + +__attribute__((noinline)) +int aes_hw_supported(void) { + if (!aes_hw_checked) { + unsigned int eax, ecx; + __asm__ volatile ("cpuid" : "=a"(eax), "=c"(ecx) : "a"(1), "c"(0) : "ebx", "edx"); + aes_hw_available = (ecx & (1 << 25)) != 0; + aes_hw_checked = 1; + } + return aes_hw_available; +} + +__attribute__((noinline)) +static void aes_hw_key_expand(AES_ctx* ctx, const uint8_t* key) { + __asm__ volatile ( + "movdqu (%[key]), %%xmm0\n\t" + "movdqa %%xmm0, (%[rk])\n\t" +#define KEYGEN(r, rc) \ + "aeskeygenassist $" #rc ", %%xmm0, %%xmm1\n\t" \ + "pshufd $0xff, %%xmm1, %%xmm1\n\t" \ + "movdqa %%xmm0, %%xmm2\n\t" \ + "pslldq $4, %%xmm2\n\t" "pxor %%xmm2, %%xmm0\n\t" \ + "pslldq $4, %%xmm2\n\t" "pxor %%xmm2, %%xmm0\n\t" \ + "pslldq $4, %%xmm2\n\t" "pxor %%xmm2, %%xmm0\n\t" \ + "pxor %%xmm1, %%xmm0\n\t" \ + "movdqa %%xmm0, " #r "*16(%[rk])\n\t" + KEYGEN(1,0x01) KEYGEN(2,0x02) KEYGEN(3,0x04) KEYGEN(4,0x08) + KEYGEN(5,0x10) KEYGEN(6,0x20) KEYGEN(7,0x40) KEYGEN(8,0x80) + KEYGEN(9,0x1b) KEYGEN(10,0x36) +#undef KEYGEN + : : [key] "r" (key), [rk] "r" (ctx->round_keys) + : "xmm0", "xmm1", "xmm2", "memory" + ); + __asm__ volatile ( +#define INVKEY(s, d) \ + "movdqa " #s "*16(%[rk]), %%xmm0\n\t" \ + "aesimc %%xmm0, %%xmm0\n\t" \ + "movdqa %%xmm0, " #d "*16(%[dk])\n\t" + INVKEY(1,0) INVKEY(2,1) INVKEY(3,2) INVKEY(4,3) INVKEY(5,4) + INVKEY(6,5) INVKEY(7,6) INVKEY(8,7) INVKEY(9,8) +#undef INVKEY + : : [rk] "r" (ctx->round_keys), [dk] "r" (ctx->dec_keys) + : "xmm0", "memory" + ); +} + +__attribute__((noinline)) +static void aes_hw_cbc_decrypt(AES_ctx* ctx, uint8_t* buf, size_t len) { + size_t blocks = len >> 4; + if (!blocks) return; + + __asm__ volatile ( + "movdqa (%[rk]), %%xmm15\n\t" + "movdqa 160(%[rk]), %%xmm14\n\t" + "movdqa (%[dk]), %%xmm13\n\t" + "movdqu (%[iv]), %%xmm12\n\t" + "cmpq $8, %[n]\n\t" + "jb 2f\n\t" + ".p2align 4\n" + "1:\n\t" + "movdqu (%[buf]), %%xmm0\n\t" + "movdqu 16(%[buf]), %%xmm1\n\t" + "movdqu 32(%[buf]), %%xmm2\n\t" + "movdqu 48(%[buf]), %%xmm3\n\t" + "movdqu 64(%[buf]), %%xmm4\n\t" + "movdqu 80(%[buf]), %%xmm5\n\t" + "movdqu 96(%[buf]), %%xmm6\n\t" + "movdqu 112(%[buf]), %%xmm7\n\t" + "movdqa %%xmm7, %%xmm11\n\t" + "pxor %%xmm14, %%xmm0\n\t" + "pxor %%xmm14, %%xmm1\n\t" + "pxor %%xmm14, %%xmm2\n\t" + "pxor %%xmm14, %%xmm3\n\t" + "pxor %%xmm14, %%xmm4\n\t" + "pxor %%xmm14, %%xmm5\n\t" + "pxor %%xmm14, %%xmm6\n\t" + "pxor %%xmm14, %%xmm7\n\t" +#define DR(off) \ + "movdqa " #off "(%[dk]), %%xmm10\n\t" \ + "aesdec %%xmm10, %%xmm0\n\t" \ + "aesdec %%xmm10, %%xmm1\n\t" \ + "aesdec %%xmm10, %%xmm2\n\t" \ + "aesdec %%xmm10, %%xmm3\n\t" \ + "aesdec %%xmm10, %%xmm4\n\t" \ + "aesdec %%xmm10, %%xmm5\n\t" \ + "aesdec %%xmm10, %%xmm6\n\t" \ + "aesdec %%xmm10, %%xmm7\n\t" + DR(128) DR(112) DR(96) DR(80) DR(64) DR(48) DR(32) DR(16) +#undef DR + "aesdec %%xmm13, %%xmm0\n\t" + "aesdec %%xmm13, %%xmm1\n\t" + "aesdec %%xmm13, %%xmm2\n\t" + "aesdec %%xmm13, %%xmm3\n\t" + "aesdec %%xmm13, %%xmm4\n\t" + "aesdec %%xmm13, %%xmm5\n\t" + "aesdec %%xmm13, %%xmm6\n\t" + "aesdec %%xmm13, %%xmm7\n\t" + "aesdeclast %%xmm15, %%xmm0\n\t" + "aesdeclast %%xmm15, %%xmm1\n\t" + "aesdeclast %%xmm15, %%xmm2\n\t" + "aesdeclast %%xmm15, %%xmm3\n\t" + "aesdeclast %%xmm15, %%xmm4\n\t" + "aesdeclast %%xmm15, %%xmm5\n\t" + "aesdeclast %%xmm15, %%xmm6\n\t" + "aesdeclast %%xmm15, %%xmm7\n\t" + "pxor %%xmm12, %%xmm0\n\t" + "movdqu (%[buf]), %%xmm12\n\t" + "pxor %%xmm12, %%xmm1\n\t" + "movdqu 16(%[buf]), %%xmm12\n\t" + "pxor %%xmm12, %%xmm2\n\t" + "movdqu 32(%[buf]), %%xmm12\n\t" + "pxor %%xmm12, %%xmm3\n\t" + "movdqu 48(%[buf]), %%xmm12\n\t" + "pxor %%xmm12, %%xmm4\n\t" + "movdqu 64(%[buf]), %%xmm12\n\t" + "pxor %%xmm12, %%xmm5\n\t" + "movdqu 80(%[buf]), %%xmm12\n\t" + "pxor %%xmm12, %%xmm6\n\t" + "movdqu 96(%[buf]), %%xmm12\n\t" + "pxor %%xmm12, %%xmm7\n\t" + "movdqu %%xmm0, (%[buf])\n\t" + "movdqu %%xmm1, 16(%[buf])\n\t" + "movdqu %%xmm2, 32(%[buf])\n\t" + "movdqu %%xmm3, 48(%[buf])\n\t" + "movdqu %%xmm4, 64(%[buf])\n\t" + "movdqu %%xmm5, 80(%[buf])\n\t" + "movdqu %%xmm6, 96(%[buf])\n\t" + "movdqu %%xmm7, 112(%[buf])\n\t" + "movdqa %%xmm11, %%xmm12\n\t" + "addq $128, %[buf]\n\t" + "subq $8, %[n]\n\t" + "cmpq $8, %[n]\n\t" + "jae 1b\n\t" + "2:\n\t" + "testq %[n], %[n]\n\t" + "jz 4f\n\t" + "3:\n\t" + "movdqu (%[buf]), %%xmm0\n\t" + "movdqa %%xmm0, %%xmm1\n\t" + "pxor %%xmm14, %%xmm0\n\t" + "aesdec 128(%[dk]), %%xmm0\n\t" + "aesdec 112(%[dk]), %%xmm0\n\t" + "aesdec 96(%[dk]), %%xmm0\n\t" + "aesdec 80(%[dk]), %%xmm0\n\t" + "aesdec 64(%[dk]), %%xmm0\n\t" + "aesdec 48(%[dk]), %%xmm0\n\t" + "aesdec 32(%[dk]), %%xmm0\n\t" + "aesdec 16(%[dk]), %%xmm0\n\t" + "aesdec %%xmm13, %%xmm0\n\t" + "aesdeclast %%xmm15, %%xmm0\n\t" + "pxor %%xmm12, %%xmm0\n\t" + "movdqa %%xmm1, %%xmm12\n\t" + "movdqu %%xmm0, (%[buf])\n\t" + "addq $16, %[buf]\n\t" + "decq %[n]\n\t" + "jnz 3b\n\t" + "4:\n\t" + "movdqu %%xmm12, (%[iv])\n\t" + : [buf] "+r" (buf), [n] "+r" (blocks) + : [rk] "r" (ctx->round_keys), [dk] "r" (ctx->dec_keys), [iv] "r" (ctx->iv) + : "xmm0","xmm1","xmm2","xmm3","xmm4","xmm5","xmm6","xmm7", + "xmm10","xmm11","xmm12","xmm13","xmm14","xmm15","memory","cc" + ); +} +#endif + +static void aes_sw_key_expand(uint8_t* rk, const uint8_t* key) { + uint8_t t[4]; + for (int i = 0; i < 16; ++i) rk[i] = key[i]; + for (int i = 4; i < 44; ++i) { + int k = (i - 1) << 2; + t[0] = rk[k]; t[1] = rk[k+1]; t[2] = rk[k+2]; t[3] = rk[k+3]; + if ((i & 3) == 0) { + uint8_t tmp = t[0]; + t[0] = aes_sbox[t[1]] ^ aes_rcon[i >> 2]; + t[1] = aes_sbox[t[2]]; + t[2] = aes_sbox[t[3]]; + t[3] = aes_sbox[tmp]; + } + int j = i << 2; k = (i - 4) << 2; + rk[j] = rk[k] ^ t[0]; rk[j+1] = rk[k+1] ^ t[1]; + rk[j+2] = rk[k+2] ^ t[2]; rk[j+3] = rk[k+3] ^ t[3]; + } +} + +static void aes_sw_decrypt_block(uint8_t* s, const uint8_t* rk) { + uint8_t t; + for (int i = 0; i < 16; ++i) s[i] ^= rk[160+i]; + for (int r = 9; r >= 0; --r) { + t = s[13]; s[13] = s[9]; s[9] = s[5]; s[5] = s[1]; s[1] = t; + t = s[2]; s[2] = s[10]; s[10] = t; + t = s[6]; s[6] = s[14]; s[14] = t; + t = s[3]; s[3] = s[7]; s[7] = s[11]; s[11] = s[15]; s[15] = t; + for (int i = 0; i < 16; ++i) s[i] = aes_rsbox[s[i]]; + for (int i = 0; i < 16; ++i) s[i] ^= rk[(r<<4)+i]; + if (r == 0) break; + for (int i = 0; i < 4; ++i) { + int j = i << 2; + uint8_t a = s[j], b = s[j+1], c = s[j+2], d = s[j+3]; + s[j] = mul(a,0x0e) ^ mul(b,0x0b) ^ mul(c,0x0d) ^ mul(d,0x09); + s[j+1] = mul(a,0x09) ^ mul(b,0x0e) ^ mul(c,0x0b) ^ mul(d,0x0d); + s[j+2] = mul(a,0x0d) ^ mul(b,0x09) ^ mul(c,0x0e) ^ mul(d,0x0b); + s[j+3] = mul(a,0x0b) ^ mul(b,0x0d) ^ mul(c,0x09) ^ mul(d,0x0e); + } + } +} + +static void aes_sw_cbc_decrypt(AES_ctx* ctx, uint8_t* buf, size_t len) { + uint8_t tmp[16], niv[16]; + for (size_t i = 0; i < len; i += 16) { + for (int j = 0; j < 16; ++j) { niv[j] = buf[i+j]; tmp[j] = buf[i+j]; } + aes_sw_decrypt_block(tmp, ctx->round_keys); + for (int j = 0; j < 16; ++j) buf[i+j] = tmp[j] ^ ctx->iv[j]; + for (int j = 0; j < 16; ++j) ctx->iv[j] = niv[j]; + } +} + +static void aes_sw_encrypt_block(uint8_t* s, const uint8_t* rk) { + uint8_t t; + for (int i = 0; i < 16; ++i) s[i] ^= rk[i]; + for (int r = 1; ; ++r) { + for (int i = 0; i < 16; ++i) s[i] = aes_sbox[s[i]]; + t = s[1]; s[1] = s[5]; s[5] = s[9]; s[9] = s[13]; s[13] = t; + t = s[2]; s[2] = s[10]; s[10] = t; + t = s[6]; s[6] = s[14]; s[14] = t; + t = s[3]; s[3] = s[15]; s[15] = s[11]; s[11] = s[7]; s[7] = t; + if (r == 10) { for (int i = 0; i < 16; ++i) s[i] ^= rk[(r<<4)+i]; break; } + for (int i = 0; i < 4; ++i) { + int j = i << 2; + uint8_t a = s[j], b = s[j+1], c = s[j+2], d = s[j+3]; + s[j] = xtime(a) ^ xtime(b) ^ b ^ c ^ d; + s[j+1] = a ^ xtime(b) ^ xtime(c) ^ c ^ d; + s[j+2] = a ^ b ^ xtime(c) ^ xtime(d) ^ d; + s[j+3] = xtime(a) ^ a ^ b ^ c ^ xtime(d); + } + for (int i = 0; i < 16; ++i) s[i] ^= rk[(r<<4)+i]; + } +} + +static void aes_sw_cbc_encrypt(AES_ctx* ctx, uint8_t* buf, size_t len) { + for (size_t i = 0; i < len; i += 16) { + for (int j = 0; j < 16; ++j) buf[i+j] ^= ctx->iv[j]; + aes_sw_encrypt_block(buf + i, ctx->round_keys); + for (int j = 0; j < 16; ++j) ctx->iv[j] = buf[i+j]; + } +} + +void AES_init_ctx_iv(AES_ctx* ctx, const uint8_t* key, const uint8_t* iv) { + aes_init_rsbox(); +#if AES_HW_AVAILABLE + if (aes_hw_supported()) { + aes_hw_key_expand(ctx, key); + } else +#endif + { + aes_sw_key_expand(ctx->round_keys, key); + } + for (int i = 0; i < 16; ++i) ctx->iv[i] = iv[i]; +} + +void AES_ctx_set_iv(AES_ctx* ctx, const uint8_t* iv) { + for (int i = 0; i < 16; ++i) ctx->iv[i] = iv[i]; +} + +void AES_CBC_decrypt_buffer(AES_ctx* ctx, uint8_t* buf, size_t len) { +#if AES_HW_AVAILABLE + if (aes_hw_supported()) { aes_hw_cbc_decrypt(ctx, buf, len); return; } +#endif + aes_sw_cbc_decrypt(ctx, buf, len); +} + +void AES_CBC_encrypt_buffer(AES_ctx* ctx, uint8_t* buf, size_t len) { + aes_sw_cbc_encrypt(ctx, buf, len); +} diff --git a/src/crypto.c b/src/crypto.c new file mode 100644 index 0000000..8cf84d3 --- /dev/null +++ b/src/crypto.c @@ -0,0 +1,114 @@ +#include "crypto.h" +#include "aes.h" +#include "common.h" + +const uint8_t NTFS_HEADER[16] = { + 0xeb, 0x52, 0x90, 0x4e, 0x54, 0x46, 0x53, 0x20, + 0x20, 0x20, 0x20, 0x00, 0x10, 0x01, 0x00, 0x00 +}; +const uint8_t EXFAT_HEADER[16] = { + 0xeb, 0x76, 0x90, 0x45, 0x58, 0x46, 0x41, 0x54, + 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00 +}; +const uint8_t OPTION_KEY[16] = { + 0x5c, 0x84, 0xa9, 0xe7, 0x26, 0xea, 0xa5, 0xdd, + 0x35, 0x1f, 0x2b, 0x07, 0x50, 0xc2, 0x36, 0x97 +}; +const uint8_t OPTION_IV[16] = { + 0xc0, 0x63, 0xbf, 0x6f, 0x56, 0x2d, 0x08, 0x4d, + 0x79, 0x63, 0xc9, 0x87, 0xf5, 0x28, 0x17, 0x61 +}; +const uint8_t APM3_SEED[96] = { + 0xC7, 0x3C, 0xDD, 0xBF, 0x7A, 0xFB, 0x0E, 0xBC, 0xE6, 0xDE, 0xD4, 0xD9, 0xB3, 0xDF, 0x3B, 0x03, + 0x3F, 0xE1, 0x40, 0xE4, 0xF4, 0xFF, 0x96, 0xC5, 0x79, 0x90, 0x8B, 0x5B, 0x69, 0x6A, 0xBE, 0xEE, + 0x32, 0x6C, 0x5E, 0xEA, 0x47, 0xC0, 0xA3, 0x40, 0x51, 0xDC, 0x55, 0xBF, 0x8C, 0x2A, 0x80, 0x7B, + 0xE4, 0xC6, 0xE3, 0xEF, 0x2F, 0x15, 0x30, 0x84, 0x69, 0x3C, 0xE2, 0xD2, 0x1E, 0xF1, 0xBB, 0x13, + 0xDC, 0xC9, 0x6D, 0x31, 0x7C, 0x3F, 0xCC, 0x7A, 0xB9, 0x44, 0x63, 0x6D, 0x65, 0xC2, 0x8B, 0xB8, + 0xE2, 0xF7, 0x74, 0x8D, 0xC6, 0x42, 0x08, 0xA8, 0x73, 0x41, 0x4B, 0x78, 0x7E, 0x3F, 0x18, 0x66 +}; +const uint8_t APM3_KEY[16] = { + 0x87, 0x3d, 0xf6, 0x32, 0xb9, 0x88, 0xae, 0x14, + 0xaa, 0x9f, 0x73, 0x6b, 0x03, 0xa5, 0x1c, 0x4f +}; +const uint8_t APM3_IV[16] = { + 0x35, 0x7d, 0xc1, 0x90, 0x30, 0xd8, 0xe8, 0xd4, + 0x94, 0x1a, 0x7e, 0x6a, 0xce, 0xb9, 0x4e, 0x4c +}; +const uint8_t BOOTID_KEY[16] = { + 0x09, 0xCA, 0x5E, 0xFD, 0x30, 0xC9, 0xAA, 0xEF, + 0x38, 0x04, 0xD0, 0xA7, 0xE3, 0xFA, 0x71, 0x20 +}; +const uint8_t BOOTID_IV[16] = { + 0xB1, 0x55, 0xC2, 0x2C, 0x2E, 0x7F, 0x04, 0x91, + 0xFA, 0x7F, 0x0F, 0xDC, 0x21, 0x7A, 0xFF, 0x90 +}; + +bool key_derive(const char* id, uint8_t out_key[16], uint8_t out_iv[16]) { + if (!id || strlen(id) < 4) { + return false; + } + + uint8_t data[96]; + memcpy(data, APM3_SEED, 96); + + AES_ctx ctx; + AES_init_ctx_iv(&ctx, APM3_KEY, APM3_IV); + AES_CBC_decrypt_buffer(&ctx, data, 96); + + uint8_t intermediate[32]; + AES_init_ctx_iv(&ctx, data, data + 16); + memcpy(intermediate, data + 64, 32); + AES_CBC_encrypt_buffer(&ctx, intermediate, 32); + + uint8_t pKey[16]; + uint8_t pIv[16]; + memcpy(pKey, intermediate, 16); + memcpy(pIv, intermediate + 16, 16); + + for (int i = 0; i < 16; i++) { + pKey[i] ^= (uint8_t)id[i % 4]; + pIv[i] ^= (uint8_t)id[i % 4]; + } + + memcpy(out_key, pKey, 16); + memcpy(out_iv, pIv, 16); + + return true; +} + +void iv_page(uint64_t off, const uint8_t* base, uint8_t* out) { + __asm__ volatile ( + "movq %[off], %%xmm0\n\t" + "punpcklqdq %%xmm0, %%xmm0\n\t" + "movdqu (%[base]), %%xmm1\n\t" + "pxor %%xmm0, %%xmm1\n\t" + "movdqu %%xmm1, (%[out])\n\t" + : + : [off] "r" (off), [base] "r" (base), [out] "r" (out) + : "xmm0", "xmm1", "memory" + ); +} + +bool iv_file(const uint8_t key[16], const uint8_t* hdr, const uint8_t* page, uint8_t out[16]) { + uint8_t iv[16]; + uint8_t header[16]; + memcpy(header, page, 16); + + iv_page(0, hdr, iv); + + AES_ctx ctx; + AES_init_ctx_iv(&ctx, key, iv); + AES_CBC_decrypt_buffer(&ctx, header, 16); + + memcpy(out, header, 16); + return true; +} + +bool key_game(const char* id, GameKeys* out) { + if (key_lookup(id, out->key, out->iv, &out->external)) { + out->has_iv = true; + return true; + } + out->external = false; + return false; +} diff --git a/src/exfat.c b/src/exfat.c new file mode 100644 index 0000000..d6d7503 --- /dev/null +++ b/src/exfat.c @@ -0,0 +1,420 @@ +#include "exfat.h" +#include "progress.h" + +static void count_directory_size(ExfatContext* ctx, uint32_t start_cluster); + +static uint64_t get_cluster_offset(ExfatContext* ctx, uint32_t cluster) { + 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 (FSEEKO(ctx->fp, offset, SEEK_SET) != 0) { + return false; + } + return fread(buffer, 1, size, ctx->fp) == size; +} + +static bool read_cluster(ExfatContext* ctx, uint32_t cluster, void* buffer) { + uint32_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 cluster + 1; + } + 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 bool extract_file(ExfatContext* ctx, ExfatFileInfo* file, const char* output_path) { + FILE* out = FOPEN(output_path, "wb"); + if (!out) { + return false; + } + setvbuf(out, NULL, _IOFBF, ctx->bytes_per_cluster); + + uint32_t current_cluster = file->first_cluster; + uint64_t remaining = file->data_length; + + bool success = true; + while (remaining > 0 && current_cluster != 0 && success) { + if (!read_cluster(ctx, current_cluster, ctx->io_buf)) { + success = false; + break; + } + + size_t write_size = (remaining > ctx->bytes_per_cluster) ? ctx->bytes_per_cluster : (size_t)remaining; + if (fwrite(ctx->io_buf, 1, write_size, out) != write_size) { + success = false; + break; + } + + remaining -= write_size; + + if (file->no_fat_chain) { + current_cluster++; + } else { + current_cluster = get_next_cluster(ctx, current_cluster); + } + + ctx->extracted_bytes += write_size; + if (ctx->progress) { + progress_update((Progress*)ctx->progress, ctx->extracted_bytes); + } + } + + fclose(out); + + if (success) { + ctx->files_extracted++; + 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); + } + } + + 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; + bool finished = false; + + while (!finished && current_cluster != 0) { + 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) { + 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; + + 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'; + 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) { + uint32_t new_cap = ctx->deferred_capacity ? ctx->deferred_capacity * 2 : 256; + DeferredDirTime* new_buf = realloc(ctx->deferred_dirs, new_cap * sizeof(DeferredDirTime)); + if (new_buf) { + ctx->deferred_dirs = new_buf; + ctx->deferred_capacity = new_cap; + } + } + 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 process_directory(ExfatContext* ctx, uint32_t start_cluster, const char* output_dir) { + return process_directory_recursive(ctx, start_cluster, output_dir, 0); +} + +static bool exfat_setup_fields(ExfatContext* ctx) { + 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); + 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; + + ctx->fat = malloc(ctx->fat_length_bytes); + ctx->cluster_buf = malloc(ctx->bytes_per_cluster); + ctx->io_buf = malloc(ctx->bytes_per_cluster); + 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 (fseek(ctx->fp, (long)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; +} + +static void count_directory_size_recursive(ExfatContext* ctx, uint32_t start_cluster, int depth) { + if (depth > EXFAT_MAX_RECURSION_DEPTH) return; + + uint32_t current_cluster = start_cluster; + bool finished = false; + + while (!finished && current_cluster != 0) { + if (!read_cluster(ctx, current_cluster, ctx->cluster_buf)) { + break; + } + + 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) { + 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; + } + + bool is_directory = ((file_entry->file_attributes & 0x10) != 0); + uint32_t first_cluster = stream_entry->first_cluster; + uint64_t data_length = stream_entry->data_length; + int num_name_entries = (stream_entry->name_length + 14) / 15; + int total_entries = 2 + num_name_entries; + i += total_entries; + entry_offset += EXFAT_ENTRY_SIZE * total_entries; + + if (is_directory) { + count_directory_size_recursive(ctx, first_cluster, depth + 1); + if (!read_cluster(ctx, current_cluster, ctx->cluster_buf)) { + return; + } + } else { + ctx->total_bytes += data_length; + } + continue; + } + i++; + entry_offset += EXFAT_ENTRY_SIZE; + } + + if (!finished) { + current_cluster = get_next_cluster(ctx, current_cluster); + } + } +} + +static void count_directory_size(ExfatContext* ctx, uint32_t start_cluster) { + count_directory_size_recursive(ctx, start_cluster, 0); +} + +bool exfat_extract_all(ExfatContext* ctx, const char* output_dir) { + if (!create_directories(output_dir)) { + return false; + } + + ctx->total_bytes = 0; + ctx->extracted_bytes = 0; + count_directory_size(ctx, ctx->boot_sector.first_cluster_of_root_dir); + + if (ctx->verbose && !ctx->silent) { + printf("%llu B\n", (unsigned long long)ctx->total_bytes); + } + + Progress progress; + if (!ctx->silent) { + progress_init(&progress, ctx->total_bytes > 0 ? ctx->total_bytes : 1); + ctx->progress = &progress; + } + + bool result = process_directory(ctx, ctx->boot_sector.first_cluster_of_root_dir, output_dir); + + if (!ctx->silent && ctx->progress) { + progress_finish(&progress); + } + ctx->progress = NULL; + + 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) { + 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; +} diff --git a/src/keys.c b/src/keys.c new file mode 100644 index 0000000..99ed3fb --- /dev/null +++ b/src/keys.c @@ -0,0 +1,129 @@ +#include "crypto.h" +#include "common.h" + +typedef struct { + char game_id[8]; + uint8_t key[16]; + uint8_t iv[16]; +} ExternalKey; + +static ExternalKey* external_keys; +static size_t external_keys_count; +static size_t external_keys_cap; +static bool external_keys_loaded; + +#include "keys.h" + +static int hex_val(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static bool parse_hex_bytes(const char* start, const char* end, uint8_t* out, size_t count) { + size_t idx = 0; + const char* p = start; + while (p < end && idx < count) { + while (p < end && !isxdigit(*p)) p++; + if (p >= end) break; + int hi = hex_val(*p++); + while (p < end && !isxdigit(*p)) p++; + if (p >= end) break; + int lo = hex_val(*p++); + if (hi < 0 || lo < 0) return false; + out[idx++] = (hi << 4) | lo; + } + return idx == count; +} + +static void load_keys_from_file(const char* path) { + FILE* f = FOPEN(path, "r"); + if (!f) return; + + char line[512]; + while (fgets(line, sizeof(line), f)) { + char* start = strchr(line, '{'); + if (!start) continue; + + char* quote1 = strchr(start, '"'); + if (!quote1) continue; + char* quote2 = strchr(quote1 + 1, '"'); + if (!quote2) continue; + + size_t id_len = quote2 - quote1 - 1; + if (id_len == 0 || id_len > 7) continue; + + char* brace1 = strchr(quote2, '{'); + if (!brace1) continue; + char* brace1_end = strchr(brace1, '}'); + if (!brace1_end) continue; + + char* brace2 = strchr(brace1_end, '{'); + if (!brace2) continue; + char* brace2_end = strchr(brace2, '}'); + if (!brace2_end) continue; + + if (external_keys_count >= external_keys_cap) { + size_t new_cap = external_keys_cap ? external_keys_cap * 2 : 8; + ExternalKey* new_keys = realloc(external_keys, new_cap * sizeof(ExternalKey)); + if (!new_keys) break; + external_keys = new_keys; + external_keys_cap = new_cap; + } + + ExternalKey* k = &external_keys[external_keys_count]; + memset(k, 0, sizeof(*k)); + memcpy(k->game_id, quote1 + 1, id_len); + + if (!parse_hex_bytes(brace1, brace1_end, k->key, 16)) continue; + if (!parse_hex_bytes(brace2, brace2_end, k->iv, 16)) continue; + + external_keys_count++; + } + fclose(f); +} + +static void try_load_external_keys(void) { + if (external_keys_loaded) return; + external_keys_loaded = true; + load_keys_from_file("keys.inc"); +} + +static size_t count_embedded_keys(void) { + size_t count = 0; + const GameKeyEntry* entry = embedded_keys; + while (entry->game_id != NULL) { count++; entry++; } + return count; +} + +bool key_any(void) { + try_load_external_keys(); + return count_embedded_keys() > 0 || external_keys_count > 0; +} + +bool key_lookup(const char* id, uint8_t out_key[16], uint8_t out_iv[16], bool* from_external) { + try_load_external_keys(); + if (from_external) *from_external = false; + + for (size_t i = 0; i < external_keys_count; i++) { + if (strcmp(external_keys[i].game_id, id) == 0) { + memcpy(out_key, external_keys[i].key, 16); + memcpy(out_iv, external_keys[i].iv, 16); + if (from_external) *from_external = true; + return true; + } + } + + const GameKeyEntry* entry = embedded_keys; + while (entry->game_id != NULL) { + if (strcmp(entry->game_id, id) == 0) { + memcpy(out_key, entry->key, 16); + memcpy(out_iv, entry->iv, 16); + return true; + } + entry++; + } + + return false; +} diff --git a/src/lib.c b/src/lib.c new file mode 100644 index 0000000..f7c9652 --- /dev/null +++ b/src/lib.c @@ -0,0 +1,1249 @@ +#include "lib.h" + +int lib_errno_val = 0; + +static FILE lib_stdout_impl; +static FILE lib_stderr_impl; +static FILE lib_stdin_impl; + +FILE* lib_stdout_file = &lib_stdout_impl; +FILE* lib_stderr_file = &lib_stderr_impl; +FILE* lib_stdin_file = &lib_stdin_impl; + +#ifdef PLATFORM_WINDOWS + +HANDLE lib_heap = NULL; +HANDLE lib_stdout_handle = NULL; +HANDLE lib_stderr_handle = NULL; +HANDLE lib_stdin_handle = NULL; + +#define MAX_FD_TABLE 256 +static HANDLE fd_table[MAX_FD_TABLE]; +static int fd_next = 3; + +#define FILE_POOL_SIZE 16 +typedef struct { + FILE file; + uint8_t* buffer; + bool in_use; +} PooledFile; +static PooledFile file_pool[FILE_POOL_SIZE]; +static int file_pool_next = 0; + +static FILE* pool_alloc_file(void) { + for (int i = 0; i < FILE_POOL_SIZE; i++) { + int idx = (file_pool_next + i) % FILE_POOL_SIZE; + if (!file_pool[idx].in_use) { + file_pool[idx].in_use = true; + file_pool_next = (idx + 1) % FILE_POOL_SIZE; + if (!file_pool[idx].buffer) { + file_pool[idx].buffer = lib_malloc(LIB_IOBUF_SIZE); + if (!file_pool[idx].buffer) { + file_pool[idx].in_use = false; + break; + } + } + FILE* f = &file_pool[idx].file; + f->buffer = file_pool[idx].buffer; + f->buf_cap = LIB_IOBUF_SIZE; + return f; + } + } + FILE* f = lib_malloc(sizeof(FILE) + LIB_IOBUF_SIZE); + if (f) { + f->buffer = (uint8_t*)(f + 1); + f->buf_cap = LIB_IOBUF_SIZE; + } + return f; +} + +static void pool_free_file(FILE* f) { + for (int i = 0; i < FILE_POOL_SIZE; i++) { + if (f == &file_pool[i].file) { + file_pool[i].in_use = false; + return; + } + } + lib_free(f); +} + +_Static_assert(CRT_OFFSETOF(PEB, ProcessParameters) == 0x20, "PEB.ProcessParameters offset"); +_Static_assert(CRT_OFFSETOF(PEB, ProcessHeap) == 0x30, "PEB.ProcessHeap offset"); +_Static_assert(CRT_OFFSETOF(RTL_USER_PROCESS_PARAMETERS, StandardInput) == 0x20, "Params.StandardInput offset"); +_Static_assert(CRT_OFFSETOF(RTL_USER_PROCESS_PARAMETERS, StandardOutput) == 0x28, "Params.StandardOutput offset"); +_Static_assert(CRT_OFFSETOF(RTL_USER_PROCESS_PARAMETERS, StandardError) == 0x30, "Params.StandardError offset"); +_Static_assert(CRT_OFFSETOF(RTL_USER_PROCESS_PARAMETERS, CommandLine) == 0x70, "Params.CommandLine offset"); + +__attribute__((naked, used)) void ___chkstk_ms(void) { + __asm__ volatile ( + "push %%rcx\n\t" "push %%rax\n\t" "cmp $0x1000, %%rax\n\t" "lea 24(%%rsp), %%rcx\n\t" "jb 2f\n\t" + "1:\n\t" "sub $0x1000, %%rcx\n\t" "test %%rcx, (%%rcx)\n\t" "sub $0x1000, %%rax\n\t" "cmp $0x1000, %%rax\n\t" "ja 1b\n\t" + "2:\n\t" "sub %%rax, %%rcx\n\t" "test %%rcx, (%%rcx)\n\t" "pop %%rax\n\t" "pop %%rcx\n\t" "ret\n\t" ::: "memory" + ); +} + +#else + +static uint8_t* heap_base = NULL; +static uint8_t* heap_end = NULL; +static uint8_t* heap_cur = NULL; +#define HEAP_INITIAL_SIZE (16 * 1024 * 1024) + +typedef struct FreeNode { struct FreeNode* next; size_t size; } FreeNode; +static FreeNode* free_list = NULL; + +#endif + +#ifdef PLATFORM_WINDOWS + +void* lib_malloc(size_t size) { + if (!size) return NULL; + return RtlAllocateHeap(lib_heap, 0, size); +} + +void* lib_calloc(size_t count, size_t size) { + size_t total = count * size; + if (!total) return NULL; + return RtlAllocateHeap(lib_heap, 0x08, total); +} + +void* lib_realloc(void* ptr, size_t size) { + if (!ptr) return lib_malloc(size); + if (!size) { lib_free(ptr); return NULL; } + return RtlReAllocateHeap(lib_heap, 0, ptr, size); +} + +void lib_free(void* ptr) { + if (ptr) RtlFreeHeap(lib_heap, 0, ptr); +} + +#else + +static void heap_init(void) { + if (heap_base) return; + long ret = syscall6(SYS_mmap, 0, HEAP_INITIAL_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (ret < 0 && ret > -4096) { heap_base = NULL; return; } + heap_base = (uint8_t*)ret; + heap_cur = heap_base; + heap_end = heap_base + HEAP_INITIAL_SIZE; +} + +void* lib_malloc(size_t size) { + if (!size) return NULL; + if (!heap_base) heap_init(); + if (!heap_base) return NULL; + + size = (size + 15) & ~15UL; + size_t total = size + 16; + + FreeNode** pp = &free_list; + while (*pp) { + if ((*pp)->size >= total) { + FreeNode* node = *pp; + size_t node_size = node->size; + *pp = node->next; + if (node_size >= total + 48) { + FreeNode* rem = (FreeNode*)((uint8_t*)node + total); + rem->size = node_size - total; + rem->next = free_list; + free_list = rem; + *(size_t*)node = total; + } else { + *(size_t*)node = node_size; + } + return (uint8_t*)node + 16; + } + pp = &(*pp)->next; + } + + if (heap_cur + total > heap_end) { + size_t mmap_size = (total + 4095) & ~4095UL; + long ret = syscall6(SYS_mmap, 0, mmap_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (ret < 0 && ret > -4096) return NULL; + uint8_t* ptr = (uint8_t*)ret; + *(size_t*)ptr = mmap_size | 0x8000000000000000ULL; + return ptr + 16; + } + + uint8_t* ptr = heap_cur; + *(size_t*)ptr = total; + heap_cur += total; + return ptr + 16; +} + +void* lib_calloc(size_t count, size_t size) { + size_t total = count * size; + void* ptr = lib_malloc(total); + if (ptr) lib_memset(ptr, 0, total); + return ptr; +} + +void* lib_realloc(void* ptr, size_t size) { + if (!ptr) return lib_malloc(size); + if (!size) { lib_free(ptr); return NULL; } + + uint8_t* base = (uint8_t*)ptr - 16; + size_t old_total = *(size_t*)base; + old_total &= 0x7FFFFFFFFFFFFFFFULL; + size_t old_size = old_total - 16; + + if (size <= old_size) return ptr; + + void* new_ptr = lib_malloc(size); + if (new_ptr) { + lib_memcpy(new_ptr, ptr, old_size < size ? old_size : size); + lib_free(ptr); + } + return new_ptr; +} + +void lib_free(void* ptr) { + if (!ptr) return; + uint8_t* base = (uint8_t*)ptr - 16; + size_t size = *(size_t*)base; + if (size & 0x8000000000000000ULL) { + size &= 0x7FFFFFFFFFFFFFFFULL; + syscall2(SYS_munmap, (long)base, size); + } else if (base >= heap_base && base < heap_end) { + FreeNode* node = (FreeNode*)base; + node->size = size; + node->next = free_list; + free_list = node; + } +} + +#endif + +size_t lib_strlen(const char* s) { + size_t len; + __asm__ volatile ( + "xor %%al, %%al\n\t" + "mov $-1, %[len]\n\t" + "repne scasb\n\t" + "not %[len]\n\t" + "dec %[len]\n\t" + : [len] "=c" (len), "+D" (s) + : + : "al", "memory" + ); + return len; +} + +char* lib_strcpy(char* dst, const char* src) { + char* d = dst; + while ((*d++ = *src++)); + return dst; +} + +char* lib_strncpy(char* dst, const char* src, size_t n) { + size_t i; + for (i = 0; i < n && src[i]; i++) dst[i] = src[i]; + for (; i < n; i++) dst[i] = '\0'; + return dst; +} + +int lib_strcmp(const char* s1, const char* s2) { + while (*s1 && *s1 == *s2) { s1++; s2++; } + return (unsigned char)*s1 - (unsigned char)*s2; +} + +int lib_strncmp(const char* s1, const char* s2, size_t n) { + while (n && *s1 && *s1 == *s2) { s1++; s2++; n--; } + return n ? (unsigned char)*s1 - (unsigned char)*s2 : 0; +} + +char* lib_strchr(const char* s, int c) { + while (*s) { if (*s == (char)c) return (char*)s; s++; } + return (c == 0) ? (char*)s : NULL; +} + +char* lib_strrchr(const char* s, int c) { + const char* last = NULL; + while (*s) { if (*s == (char)c) last = s; s++; } + return (c == 0) ? (char*)s : (char*)last; +} + +char* lib_strstr(const char* haystack, const char* needle) { + if (!*needle) return (char*)haystack; + for (; *haystack; haystack++) { + const char* h = haystack; const char* n = needle; + while (*h && *n && *h == *n) { h++; n++; } + if (!*n) return (char*)haystack; + } + return NULL; +} + +char* lib_strncat(char* dst, const char* src, size_t n) { + char* d = dst; + while (*d) d++; + while (n-- && *src) *d++ = *src++; + *d = '\0'; + return dst; +} + +void* lib_memcpy(void* dst, const void* src, size_t n) { + void* ret = dst; + __asm__ volatile ( + "rep movsb" + : "+D"(dst), "+S"(src), "+c"(n) + : + : "memory" + ); + return ret; +} + +#undef memcpy +void* memcpy(void* dst, const void* src, size_t n) __attribute__((alias("lib_memcpy"))); +#define memcpy lib_memcpy + +void* lib_memset(void* dst, int c, size_t n) { + void* ret = dst; + __asm__ volatile ( + "rep stosb" + : "+D"(dst), "+c"(n) + : "a"((uint8_t)c) + : "memory" + ); + return ret; +} + +#undef memset +void* memset(void* dst, int c, size_t n) __attribute__((alias("lib_memset"))); +#define memset lib_memset + +int lib_memcmp(const void* s1, const void* s2, size_t n) { + const uint8_t* p1 = s1; const uint8_t* p2 = s2; + while (n--) { if (*p1 != *p2) return *p1 - *p2; p1++; p2++; } + return 0; +} + +#ifdef PLATFORM_WINDOWS + +static size_t utf8_to_utf16(const char* utf8, WCHAR* utf16, size_t utf16_max) { + size_t out = 0; + const uint8_t* s = (const uint8_t*)utf8; + while (*s && out < utf16_max - 1) { + uint32_t cp; + if (s[0] < 0x80) { cp = s[0]; s += 1; } + else if ((s[0] & 0xE0) == 0xC0) { cp = ((s[0] & 0x1F) << 6) | (s[1] & 0x3F); s += 2; } + else if ((s[0] & 0xF0) == 0xE0) { cp = ((s[0] & 0x0F) << 12) | ((s[1] & 0x3F) << 6) | (s[2] & 0x3F); s += 3; } + else if ((s[0] & 0xF8) == 0xF0) { cp = ((s[0] & 0x07) << 18) | ((s[1] & 0x3F) << 12) | ((s[2] & 0x3F) << 6) | (s[3] & 0x3F); s += 4; } + else { cp = '?'; s += 1; } + if (cp <= 0xFFFF) utf16[out++] = (WCHAR)cp; + else if (cp <= 0x10FFFF && out < utf16_max - 2) { + cp -= 0x10000; + utf16[out++] = (WCHAR)(0xD800 | (cp >> 10)); + utf16[out++] = (WCHAR)(0xDC00 | (cp & 0x3FF)); + } + } + utf16[out] = 0; + return out; +} + +static size_t utf16_to_utf8(const WCHAR* utf16, char* utf8, size_t utf8_max) { + size_t out = 0; + while (*utf16 && out < utf8_max - 1) { + uint32_t cp = *utf16++; + if (cp >= 0xD800 && cp <= 0xDBFF && *utf16 >= 0xDC00 && *utf16 <= 0xDFFF) + cp = 0x10000 + ((cp - 0xD800) << 10) + (*utf16++ - 0xDC00); + if (cp < 0x80) utf8[out++] = (char)cp; + else if (cp < 0x800) { + if (out + 2 > utf8_max - 1) break; + utf8[out++] = (char)(0xC0 | (cp >> 6)); + utf8[out++] = (char)(0x80 | (cp & 0x3F)); + } else if (cp < 0x10000) { + if (out + 3 > utf8_max - 1) break; + utf8[out++] = (char)(0xE0 | (cp >> 12)); + utf8[out++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + utf8[out++] = (char)(0x80 | (cp & 0x3F)); + } else { + if (out + 4 > utf8_max - 1) break; + utf8[out++] = (char)(0xF0 | (cp >> 18)); + utf8[out++] = (char)(0x80 | ((cp >> 12) & 0x3F)); + utf8[out++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + utf8[out++] = (char)(0x80 | (cp & 0x3F)); + } + } + utf8[out] = '\0'; + return out; +} + +static bool path_to_nt(const WCHAR* dos_path, UNICODE_STRING* nt_path) { + return NT_SUCCESS(RtlDosPathNameToNtPathName_U_WithStatus(dos_path, nt_path, NULL, NULL)); +} + +static HANDLE nt_open_file(const WCHAR* path, ULONG access, ULONG share, ULONG disp, ULONG opts) { + UNICODE_STRING nt_path = {0}; + if (!path_to_nt(path, &nt_path)) return INVALID_HANDLE_VALUE; + OBJECT_ATTRIBUTES oa = { sizeof(OBJECT_ATTRIBUTES), NULL, &nt_path, OBJ_CASE_INSENSITIVE, NULL, NULL }; + HANDLE h = INVALID_HANDLE_VALUE; IO_STATUS_BLOCK iosb = {0}; + NTSTATUS status = NtCreateFile(&h, access | SYNCHRONIZE, &oa, &iosb, NULL, + FILE_ATTRIBUTE_NORMAL, share, disp, opts | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + RtlFreeUnicodeString(&nt_path); + if (!NT_SUCCESS(status)) { lib_errno_val = (int)status; return INVALID_HANDLE_VALUE; } + return h; +} + +static int parse_mode(const char* mode, ULONG* access, ULONG* share, ULONG* disp, ULONG* opts, uint32_t* flags) { + *access = 0; *share = FILE_SHARE_READ; *disp = FILE_OPEN; + *opts = FILE_NON_DIRECTORY_FILE; *flags = 0; + if (!mode || !*mode) return -1; + switch (mode[0]) { + case 'r': *access = FILE_READ_DATA | FILE_READ_ATTRIBUTES; *disp = FILE_OPEN; *flags = LIB_FILE_READ; break; + case 'w': *access = FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES; *disp = FILE_OVERWRITE_IF; *flags = LIB_FILE_WRITE; break; + case 'a': *access = FILE_APPEND_DATA | FILE_WRITE_ATTRIBUTES; *disp = FILE_OPEN_IF; *flags = LIB_FILE_WRITE | LIB_FILE_APPEND; break; + default: return -1; + } + for (const char* p = mode + 1; *p; p++) { + if (*p == '+') { *access |= FILE_READ_DATA | FILE_WRITE_DATA | FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES; *flags |= LIB_FILE_READ | LIB_FILE_WRITE; } + } + if ((*flags & LIB_FILE_READ) && !(*flags & LIB_FILE_WRITE)) *opts |= FILE_SEQUENTIAL_ONLY; + return 0; +} + +#endif + +#ifdef PLATFORM_WINDOWS + +FILE* lib_fopen(const char* path, const char* mode) { + WCHAR wpath[1024]; utf8_to_utf16(path, wpath, 1024); + WCHAR wmode[16]; utf8_to_utf16(mode, wmode, 16); + return lib_wfopen(wpath, wmode); +} + +FILE* lib_wfopen(const WCHAR* path, const WCHAR* mode) { + char narrow_mode[16] = {0}; + for (int i = 0; mode[i] && i < 15; i++) narrow_mode[i] = (char)mode[i]; + + ULONG access, share, disp, opts; uint32_t flags; + if (parse_mode(narrow_mode, &access, &share, &disp, &opts, &flags) < 0) { lib_errno_val = EINVAL; return NULL; } + + HANDLE h = nt_open_file(path, access, share, disp, opts); + if (h == INVALID_HANDLE_VALUE) return NULL; + + FILE* f = pool_alloc_file(); + if (!f) { NtClose(h); return NULL; } + + f->handle = h; f->buf_pos = 0; f->buf_fill = 0; f->file_pos = 0; f->flags = flags; + + if (flags & LIB_FILE_APPEND) { + FILE_STANDARD_INFORMATION info; IO_STATUS_BLOCK iosb; + if (NT_SUCCESS(NtQueryInformationFile(h, &iosb, &info, sizeof(info), FileStandardInformation))) + f->file_pos = info.EndOfFile.QuadPart; + } + if (fd_next < MAX_FD_TABLE) fd_table[fd_next++] = h; + return f; +} + +FILE* lib_wfopen_prealloc(const WCHAR* path, uint64_t size) { + UNICODE_STRING nt_path = {0}; + if (!path_to_nt(path, &nt_path)) return NULL; + + OBJECT_ATTRIBUTES oa = { sizeof(OBJECT_ATTRIBUTES), NULL, &nt_path, OBJ_CASE_INSENSITIVE, NULL, NULL }; + HANDLE h = INVALID_HANDLE_VALUE; + IO_STATUS_BLOCK iosb = {0}; + LARGE_INTEGER alloc_size; alloc_size.QuadPart = (LONGLONG)size; + + NTSTATUS status = NtCreateFile(&h, FILE_WRITE_DATA | FILE_WRITE_ATTRIBUTES | SYNCHRONIZE, &oa, &iosb, + &alloc_size, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OVERWRITE_IF, + FILE_NON_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_SEQUENTIAL_ONLY, NULL, 0); + RtlFreeUnicodeString(&nt_path); + if (!NT_SUCCESS(status)) { lib_errno_val = (int)status; return NULL; } + + FILE* f = pool_alloc_file(); + if (!f) { NtClose(h); return NULL; } + + f->handle = h; f->buf_pos = 0; f->buf_fill = 0; f->file_pos = 0; f->flags = LIB_FILE_WRITE; + if (fd_next < MAX_FD_TABLE) fd_table[fd_next++] = h; + return f; +} + +size_t lib_fwrite_direct(FILE* f, const void* buf, size_t size) { + if (!f || !buf || !size || !(f->flags & LIB_FILE_WRITE)) return 0; + IO_STATUS_BLOCK iosb = {0}; + LARGE_INTEGER offset; offset.QuadPart = f->file_pos; + NTSTATUS status = NtWriteFile(f->handle, NULL, NULL, NULL, &iosb, (void*)buf, (ULONG)size, &offset, NULL); + if (!NT_SUCCESS(status)) { f->flags |= LIB_FILE_ERROR; return 0; } + f->file_pos += iosb.Information; + return iosb.Information; +} + +static bool flush_write_buffer(FILE* f) { + if (!(f->flags & LIB_FILE_WRITE) || f->buf_fill == 0) return true; + IO_STATUS_BLOCK iosb = {0}; + bool is_console = (f == lib_stdout_file || f == lib_stderr_file); + LARGE_INTEGER offset; offset.QuadPart = f->file_pos; + NTSTATUS status = NtWriteFile(f->handle, NULL, NULL, NULL, &iosb, f->buffer, f->buf_fill, is_console ? NULL : &offset, NULL); + if (!NT_SUCCESS(status)) { f->flags |= LIB_FILE_ERROR; return false; } + f->file_pos += f->buf_fill; f->buf_fill = 0; f->buf_pos = 0; + return true; +} + +static bool refill_read_buffer(FILE* f) { + if (!(f->flags & LIB_FILE_READ) || (f->flags & LIB_FILE_EOF)) return false; + IO_STATUS_BLOCK iosb = {0}; + bool is_console = (f == lib_stdin_file); + LARGE_INTEGER offset; offset.QuadPart = f->file_pos; + NTSTATUS status = NtReadFile(f->handle, NULL, NULL, NULL, &iosb, f->buffer, f->buf_cap, is_console ? NULL : &offset, NULL); + if (status == STATUS_END_OF_FILE || iosb.Information == 0) { f->flags |= LIB_FILE_EOF; f->buf_fill = 0; f->buf_pos = 0; return false; } + if (!NT_SUCCESS(status)) { f->flags |= LIB_FILE_ERROR; return false; } + f->buf_fill = (uint32_t)iosb.Information; f->buf_pos = 0; f->file_pos += iosb.Information; + return true; +} + +size_t lib_fread(void* buf, size_t size, size_t count, FILE* f) { + if (!f || !buf || !size || !count || !(f->flags & LIB_FILE_READ)) return 0; + size_t total = size * count, read_total = 0; + uint8_t* dst = buf; + while (total > 0) { + size_t avail = f->buf_fill - f->buf_pos; + if (avail > 0) { + size_t to_copy = (avail < total) ? avail : total; + lib_memcpy(dst, f->buffer + f->buf_pos, to_copy); + f->buf_pos += (uint32_t)to_copy; dst += to_copy; total -= to_copy; read_total += to_copy; + } else { + if (total >= f->buf_cap) { + IO_STATUS_BLOCK iosb = {0}; LARGE_INTEGER offset; offset.QuadPart = f->file_pos; + size_t chunk = (total / f->buf_cap) * f->buf_cap; + NTSTATUS status = NtReadFile(f->handle, NULL, NULL, NULL, &iosb, dst, (ULONG)chunk, &offset, NULL); + if (status == STATUS_END_OF_FILE || iosb.Information == 0) { f->flags |= LIB_FILE_EOF; break; } + if (!NT_SUCCESS(status)) { f->flags |= LIB_FILE_ERROR; break; } + f->file_pos += iosb.Information; dst += iosb.Information; total -= iosb.Information; read_total += iosb.Information; + if (iosb.Information < chunk) { f->flags |= LIB_FILE_EOF; break; } + } else { if (!refill_read_buffer(f)) break; } + } + } + return read_total / size; +} + +size_t lib_fwrite(const void* buf, size_t size, size_t count, FILE* f) { + if (!f || !buf || !size || !count || !(f->flags & LIB_FILE_WRITE)) return 0; + size_t total = size * count, written = 0; + const uint8_t* src = buf; + while (total >= f->buf_cap) { + if (f->buf_fill > 0 && !flush_write_buffer(f)) return written / size; + IO_STATUS_BLOCK iosb = {0}; LARGE_INTEGER offset; offset.QuadPart = f->file_pos; + size_t chunk = (total / f->buf_cap) * f->buf_cap; + NTSTATUS status = NtWriteFile(f->handle, NULL, NULL, NULL, &iosb, (void*)src, (ULONG)chunk, &offset, NULL); + if (!NT_SUCCESS(status)) { f->flags |= LIB_FILE_ERROR; return written / size; } + f->file_pos += iosb.Information; src += iosb.Information; total -= iosb.Information; written += iosb.Information; + if (iosb.Information < chunk) return written / size; + } + while (total > 0) { + size_t space = f->buf_cap - f->buf_fill; + if (space > 0) { + size_t to_copy = (space < total) ? space : total; + lib_memcpy(f->buffer + f->buf_fill, src, to_copy); + f->buf_fill += (uint32_t)to_copy; src += to_copy; total -= to_copy; written += to_copy; + } + if (f->buf_fill >= f->buf_cap) if (!flush_write_buffer(f)) break; + } + return written / size; +} + +int lib_fseeki64(FILE* f, int64_t offset, int whence) { + if (!f) return -1; + if (f->flags & LIB_FILE_WRITE) if (!flush_write_buffer(f)) return -1; + int64_t new_pos; + switch (whence) { + case SEEK_SET: new_pos = offset; break; + case SEEK_CUR: new_pos = (f->file_pos - (f->buf_fill - f->buf_pos)) + offset; break; + case SEEK_END: { + FILE_STANDARD_INFORMATION info; IO_STATUS_BLOCK iosb; + if (!NT_SUCCESS(NtQueryInformationFile(f->handle, &iosb, &info, sizeof(info), FileStandardInformation))) return -1; + new_pos = info.EndOfFile.QuadPart + offset; break; + } + default: return -1; + } + if (new_pos < 0) return -1; + FILE_POSITION_INFORMATION pos_info; pos_info.CurrentByteOffset.QuadPart = new_pos; + IO_STATUS_BLOCK iosb; + if (!NT_SUCCESS(NtSetInformationFile(f->handle, &iosb, &pos_info, sizeof(pos_info), FilePositionInformation))) return -1; + f->file_pos = new_pos; f->buf_pos = 0; f->buf_fill = 0; f->flags &= ~LIB_FILE_EOF; + return 0; +} + +int64_t lib_ftelli64(FILE* f) { + if (!f) return -1; + return (f->flags & LIB_FILE_WRITE) ? f->file_pos + f->buf_fill : f->file_pos - (f->buf_fill - f->buf_pos); +} + +int lib_fflush(FILE* f) { return (!f || flush_write_buffer(f)) ? 0 : EOF; } + +int lib_fclose(FILE* f) { + if (!f) return EOF; + int ret = (f->flags & LIB_FILE_WRITE) ? (flush_write_buffer(f) ? 0 : EOF) : 0; + NtClose(f->handle); + if (f != &lib_stdout_impl && f != &lib_stderr_impl && f != &lib_stdin_impl) pool_free_file(f); + return ret; +} + +int lib_isatty(int fd) { + HANDLE h = (fd == 0) ? lib_stdin_handle : (fd == 1) ? lib_stdout_handle : (fd == 2) ? lib_stderr_handle : NULL; + if (!h || h == INVALID_HANDLE_VALUE) return 0; + FILE_STANDARD_INFORMATION info; IO_STATUS_BLOCK iosb; + return !NT_SUCCESS(NtQueryInformationFile(h, &iosb, &info, sizeof(info), FileStandardInformation)); +} + +#define EPOCH_DIFF 116444736000000000ULL + +time_t lib_time(time_t* t) { + LARGE_INTEGER sys_time; NtQuerySystemTime(&sys_time); + time_t result = (time_t)((sys_time.QuadPart - EPOCH_DIFF) / 10000000ULL); + if (t) *t = result; + return result; +} + +#else + +FILE* lib_fopen(const char* path, const char* mode) { + int flags = 0; + uint32_t fflags = 0; + + if (mode[0] == 'r') { + flags = (mode[1] == '+') ? O_RDWR : O_RDONLY; + fflags = LIB_FILE_READ | ((mode[1] == '+') ? LIB_FILE_WRITE : 0); + } else if (mode[0] == 'w') { + flags = O_CREAT | O_TRUNC | ((mode[1] == '+') ? O_RDWR : O_WRONLY); + fflags = LIB_FILE_WRITE | ((mode[1] == '+') ? LIB_FILE_READ : 0); + } else if (mode[0] == 'a') { + flags = O_CREAT | O_APPEND | ((mode[1] == '+') ? O_RDWR : O_WRONLY); + fflags = LIB_FILE_WRITE | LIB_FILE_APPEND | ((mode[1] == '+') ? LIB_FILE_READ : 0); + } else { + lib_errno_val = EINVAL; + return NULL; + } + + long fd = syscall3(SYS_open, (long)path, flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + if (fd < 0) { lib_errno_val = (int)(-fd); return NULL; } + + FILE* f = lib_malloc(sizeof(FILE)); + if (!f) { syscall1(SYS_close, fd); return NULL; } + f->buffer = lib_malloc(LIB_IOBUF_SIZE); + if (!f->buffer) { lib_free(f); syscall1(SYS_close, fd); return NULL; } + + f->fd = (int)fd; f->buf_cap = LIB_IOBUF_SIZE; f->buf_pos = 0; f->buf_fill = 0; f->file_pos = 0; f->flags = fflags; + + if (fflags & LIB_FILE_APPEND) { + long pos = syscall3(SYS_lseek, fd, 0, SEEK_END); + if (pos >= 0) f->file_pos = pos; + } + return f; +} + +static bool flush_write_buffer(FILE* f) { + if (!(f->flags & LIB_FILE_WRITE) || f->buf_fill == 0) return true; + long ret = syscall3(SYS_write, f->fd, (long)f->buffer, f->buf_fill); + if (ret < 0) { f->flags |= LIB_FILE_ERROR; return false; } + f->file_pos += f->buf_fill; f->buf_fill = 0; f->buf_pos = 0; + return true; +} + +static bool refill_read_buffer(FILE* f) { + if (!(f->flags & LIB_FILE_READ) || (f->flags & LIB_FILE_EOF)) return false; + long ret = syscall3(SYS_read, f->fd, (long)f->buffer, f->buf_cap); + if (ret <= 0) { if (ret == 0) f->flags |= LIB_FILE_EOF; else f->flags |= LIB_FILE_ERROR; f->buf_fill = 0; f->buf_pos = 0; return false; } + f->buf_fill = (uint32_t)ret; f->buf_pos = 0; f->file_pos += ret; + return true; +} + +size_t lib_fread(void* buf, size_t size, size_t count, FILE* f) { + if (!f || !buf || !size || !count || !(f->flags & LIB_FILE_READ)) return 0; + size_t total = size * count, read_total = 0; + uint8_t* dst = buf; + while (total > 0) { + size_t avail = f->buf_fill - f->buf_pos; + if (avail > 0) { + size_t to_copy = (avail < total) ? avail : total; + lib_memcpy(dst, f->buffer + f->buf_pos, to_copy); + f->buf_pos += (uint32_t)to_copy; dst += to_copy; total -= to_copy; read_total += to_copy; + } else { + if (total >= f->buf_cap) { + long ret = syscall3(SYS_read, f->fd, (long)dst, total); + if (ret <= 0) { if (ret == 0) f->flags |= LIB_FILE_EOF; else f->flags |= LIB_FILE_ERROR; break; } + f->file_pos += ret; dst += ret; total -= ret; read_total += ret; + } else { if (!refill_read_buffer(f)) break; } + } + } + return read_total / size; +} + +size_t lib_fwrite(const void* buf, size_t size, size_t count, FILE* f) { + if (!f || !buf || !size || !count || !(f->flags & LIB_FILE_WRITE)) return 0; + size_t total = size * count, written = 0; + const uint8_t* src = buf; + while (total > 0) { + size_t space = f->buf_cap - f->buf_fill; + if (space > 0) { + size_t to_copy = (space < total) ? space : total; + lib_memcpy(f->buffer + f->buf_fill, src, to_copy); + f->buf_fill += (uint32_t)to_copy; src += to_copy; total -= to_copy; written += to_copy; + } + if (f->buf_fill >= f->buf_cap) if (!flush_write_buffer(f)) break; + } + return written / size; +} + +int lib_fseeki64(FILE* f, int64_t offset, int whence) { + if (!f) return -1; + if (f->flags & LIB_FILE_WRITE) if (!flush_write_buffer(f)) return -1; + long new_pos = syscall3(SYS_lseek, f->fd, offset, whence); + if (new_pos < 0) return -1; + f->file_pos = new_pos; f->buf_pos = 0; f->buf_fill = 0; f->flags &= ~LIB_FILE_EOF; + return 0; +} + +int64_t lib_ftelli64(FILE* f) { + if (!f) return -1; + return (f->flags & LIB_FILE_WRITE) ? f->file_pos + f->buf_fill : f->file_pos - (f->buf_fill - f->buf_pos); +} + +int lib_fflush(FILE* f) { return (!f || flush_write_buffer(f)) ? 0 : EOF; } + +int lib_fclose(FILE* f) { + if (!f) return EOF; + int ret = (f->flags & LIB_FILE_WRITE) ? (flush_write_buffer(f) ? 0 : EOF) : 0; + syscall1(SYS_close, f->fd); + if (f->buffer) lib_free(f->buffer); + if (f != &lib_stdout_impl && f != &lib_stderr_impl && f != &lib_stdin_impl) lib_free(f); + return ret; +} + +int lib_isatty(int fd) { + char buf[64]; + return syscall3(SYS_ioctl, fd, 0x5413, (long)buf) >= 0; +} + +time_t lib_time(time_t* t) { + struct linux_timespec ts; + syscall2(228, 0, (long)&ts); + if (t) *t = ts.tv_sec; + return ts.tv_sec; +} + +#endif + +int lib_fseek(FILE* f, long offset, int whence) { return lib_fseeki64(f, (int64_t)offset, whence); } +long lib_ftell(FILE* f) { return (long)lib_ftelli64(f); } +int lib_feof(FILE* f) { return f ? (f->flags & LIB_FILE_EOF) != 0 : 0; } +static int lib_fgetc(FILE* f) { unsigned char c; return (lib_fread(&c, 1, 1, f) == 1) ? c : EOF; } +static int lib_fputc(int c, FILE* f) { unsigned char ch = (unsigned char)c; return (lib_fwrite(&ch, 1, 1, f) == 1) ? ch : EOF; } +static int lib_fputs(const char* s, FILE* f) { size_t len = lib_strlen(s); return (lib_fwrite(s, 1, len, f) == len) ? 0 : EOF; } + +char* lib_fgets(char* buf, int n, FILE* f) { + if (!buf || n <= 0 || !f) return NULL; + int i = 0; + while (i < n - 1) { + int c = lib_fgetc(f); + if (c == EOF) { if (i == 0) return NULL; break; } + buf[i++] = (char)c; + if (c == '\n') break; + } + buf[i] = '\0'; + return buf; +} + +void lib_rewind(FILE* f) { if (f) { lib_fseeki64(f, 0, SEEK_SET); } } +void lib_setvbuf(FILE* f, char* buf, int mode, size_t size) { (void)f; (void)buf; (void)mode; (void)size; } +double lib_difftime(time_t t1, time_t t0) { return (double)(t1 - t0); } + +static char* fmt_uint64(char* buf, uint64_t val, int base, int width, char pad, bool upper) { + char tmp[24]; char* p = tmp + sizeof(tmp) - 1; *p = '\0'; + if (width > 23) width = 23; + const char* digits = upper ? "0123456789ABCDEF" : "0123456789abcdef"; + do { *--p = digits[val % base]; val /= base; } while (val); + int len = (int)(tmp + sizeof(tmp) - 1 - p); + while (len < width) { *--p = pad; len++; } + while (*p) *buf++ = *p++; + return buf; +} + +static char* fmt_int64(char* buf, int64_t val, int width, char pad) { + if (val < 0) { *buf++ = '-'; val = -val; if (width > 0) width--; } + return fmt_uint64(buf, (uint64_t)val, 10, width, pad, false); +} + + + +int lib_vsnprintf(char* buf, size_t n, const char* fmt, va_list ap) { + if (!buf || !n) return 0; + char* dst = buf; char* end = buf + n - 1; + while (*fmt && dst < end) { + if (*fmt != '%') { *dst++ = *fmt++; continue; } + fmt++; + char pad = ' '; if (*fmt == '0') { pad = '0'; fmt++; } + int width = 0; while (*fmt >= '0' && *fmt <= '9') { width = width * 10 + (*fmt - '0'); fmt++; } + bool is_ll = (*fmt == 'l' && fmt[1] == 'l'); + if (is_ll) fmt += 2; + else if (*fmt == 'l' || *fmt == 'z') fmt++; + char tmp[24]; char* p; + switch (*fmt) { + case 's': { const char* s = va_arg(ap, const char*); if (!s) s = "(null)"; while (*s && dst < end) *dst++ = *s++; break; } + case 'c': *dst++ = (char)va_arg(ap, int); break; + case 'd': case 'i': + p = is_ll ? fmt_int64(tmp, va_arg(ap, long long), width, pad) : fmt_int64(tmp, va_arg(ap, int), width, pad); + *p = '\0'; for (p = tmp; *p && dst < end;) *dst++ = *p++; break; + case 'u': + p = is_ll ? fmt_uint64(tmp, va_arg(ap, unsigned long long), 10, width, pad, false) : fmt_uint64(tmp, va_arg(ap, unsigned int), 10, width, pad, false); + *p = '\0'; for (p = tmp; *p && dst < end;) *dst++ = *p++; break; + case 'x': case 'X': + p = is_ll ? fmt_uint64(tmp, va_arg(ap, unsigned long long), 16, width, pad, *fmt == 'X') : fmt_uint64(tmp, va_arg(ap, unsigned int), 16, width, pad, *fmt == 'X'); + *p = '\0'; for (p = tmp; *p && dst < end;) *dst++ = *p++; break; + case 'p': *dst++ = '0'; if (dst < end) *dst++ = 'x'; p = fmt_uint64(tmp, (uintptr_t)va_arg(ap, void*), 16, 0, '0', false); *p = '\0'; for (p = tmp; *p && dst < end;) *dst++ = *p++; break; + case '%': *dst++ = '%'; break; + default: break; + } + fmt++; + } + *dst = '\0'; + return (int)(dst - buf); +} + +int lib_snprintf(char* buf, size_t n, const char* fmt, ...) { va_list ap; va_start(ap, fmt); int ret = lib_vsnprintf(buf, n, fmt, ap); va_end(ap); return ret; } +int lib_vfprintf(FILE* f, const char* fmt, va_list ap) { char buf[4096]; int len = lib_vsnprintf(buf, sizeof(buf), fmt, ap); if (len > 0) lib_fwrite(buf, 1, len, f); return len; } +int lib_fprintf(FILE* f, const char* fmt, ...) { va_list ap; va_start(ap, fmt); int ret = lib_vfprintf(f, fmt, ap); va_end(ap); return ret; } +int lib_vprintf(const char* fmt, va_list ap) { return lib_vfprintf(lib_stdout_file, fmt, ap); } +int lib_printf(const char* fmt, ...) { va_list ap; va_start(ap, fmt); int ret = lib_vprintf(fmt, ap); va_end(ap); return ret; } +int lib_puts(const char* s) { int ret = lib_fputs(s, lib_stdout_file); if (ret >= 0) ret = lib_fputc('\n', lib_stdout_file); return ret; } + +#ifdef PLATFORM_WINDOWS + +#define STATUS_OBJECT_NAME_COLLISION ((NTSTATUS)0xC0000035L) +#define STATUS_OBJECT_NAME_NOT_FOUND ((NTSTATUS)0xC0000034L) +#define STATUS_OBJECT_PATH_NOT_FOUND ((NTSTATUS)0xC000003AL) +#define STATUS_ACCESS_DENIED ((NTSTATUS)0xC0000022L) + +static int ntstatus_to_errno(NTSTATUS status) { + switch (status) { + case STATUS_OBJECT_NAME_COLLISION: return EEXIST; + case STATUS_OBJECT_NAME_NOT_FOUND: + case STATUS_OBJECT_PATH_NOT_FOUND: return ENOENT; + case STATUS_ACCESS_DENIED: return EACCES; + default: return EINVAL; + } +} + +int lib_wmkdir(const WCHAR* path) { + UNICODE_STRING nt_path = {0}; + if (!path_to_nt(path, &nt_path)) return -1; + OBJECT_ATTRIBUTES oa = { sizeof(OBJECT_ATTRIBUTES), NULL, &nt_path, OBJ_CASE_INSENSITIVE, NULL, NULL }; + HANDLE h; IO_STATUS_BLOCK iosb; + NTSTATUS status = NtCreateFile(&h, FILE_LIST_DIRECTORY | SYNCHRONIZE, &oa, &iosb, NULL, + FILE_ATTRIBUTE_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_CREATE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + RtlFreeUnicodeString(&nt_path); + if (NT_SUCCESS(status)) { NtClose(h); return 0; } + lib_errno_val = ntstatus_to_errno(status); + return -1; +} + +int lib_mkdir(const char* path) { + WCHAR wpath[1024]; utf8_to_utf16(path, wpath, 1024); + return lib_wmkdir(wpath); +} + +int lib_wremove(const WCHAR* path) { + UNICODE_STRING nt_path = {0}; + if (!path_to_nt(path, &nt_path)) return -1; + OBJECT_ATTRIBUTES oa = { sizeof(OBJECT_ATTRIBUTES), NULL, &nt_path, OBJ_CASE_INSENSITIVE, NULL, NULL }; + HANDLE h; IO_STATUS_BLOCK iosb; + NTSTATUS status = NtCreateFile(&h, DELETE | SYNCHRONIZE, &oa, &iosb, NULL, + FILE_ATTRIBUTE_NORMAL, FILE_SHARE_DELETE, FILE_OPEN, + FILE_NON_DIRECTORY_FILE | FILE_DELETE_ON_CLOSE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + RtlFreeUnicodeString(&nt_path); + if (NT_SUCCESS(status)) { NtClose(h); return 0; } + lib_errno_val = (int)status; + return -1; +} + +int lib_remove(const char* path) { + WCHAR wpath[1024]; utf8_to_utf16(path, wpath, 1024); + return lib_wremove(wpath); +} + +int lib_rmdir(const char* path) { + WCHAR wpath[1024]; utf8_to_utf16(path, wpath, 1024); + UNICODE_STRING nt_path = {0}; + if (!path_to_nt(wpath, &nt_path)) return -1; + OBJECT_ATTRIBUTES oa = { sizeof(OBJECT_ATTRIBUTES), NULL, &nt_path, OBJ_CASE_INSENSITIVE, NULL, NULL }; + HANDLE h; IO_STATUS_BLOCK iosb; + NTSTATUS status = NtCreateFile(&h, DELETE | SYNCHRONIZE, &oa, &iosb, NULL, + FILE_ATTRIBUTE_DIRECTORY, FILE_SHARE_DELETE, FILE_OPEN, + FILE_DIRECTORY_FILE | FILE_DELETE_ON_CLOSE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + RtlFreeUnicodeString(&nt_path); + if (NT_SUCCESS(status)) { NtClose(h); return 0; } + lib_errno_val = (int)status; + return -1; +} + +typedef struct { HANDLE dir_handle; WCHAR pattern[260]; uint8_t buffer[4096]; uint32_t buf_pos; uint32_t buf_len; bool first_call; } FindState; + +static HANDLE open_directory_for_enum(const char* pattern, WCHAR* out_pattern) { + char dir_path[1024]; lib_strncpy(dir_path, pattern, sizeof(dir_path) - 1); dir_path[sizeof(dir_path) - 1] = '\0'; + char* last_sep = lib_strrchr(dir_path, '\\'); if (!last_sep) last_sep = lib_strrchr(dir_path, '/'); + const char* wildcard = last_sep ? (last_sep + 1) : pattern; + if (last_sep) *last_sep = '\0'; else { dir_path[0] = '.'; dir_path[1] = '\0'; } + utf8_to_utf16(wildcard, out_pattern, 260); + WCHAR wdir[1024]; utf8_to_utf16(dir_path, wdir, 1024); + return nt_open_file(wdir, FILE_LIST_DIRECTORY | SYNCHRONIZE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, FILE_OPEN, FILE_DIRECTORY_FILE); +} + +static bool fill_finddata(FILE_BOTH_DIR_INFORMATION* info, lib_finddata_t* data) { + data->attrib = info->FileAttributes; + data->time_create = info->CreationTime.QuadPart; + data->time_access = info->LastAccessTime.QuadPart; + data->time_write = info->LastWriteTime.QuadPart; + data->size = info->EndOfFile.QuadPart; + utf16_to_utf8(info->FileName, data->name, 260); + return true; +} + +intptr_t lib_findfirst(const char* pattern, lib_finddata_t* data) { + FindState* state = lib_malloc(sizeof(FindState)); + if (!state) return -1; + lib_memset(state, 0, sizeof(FindState)); state->first_call = true; + state->dir_handle = open_directory_for_enum(pattern, state->pattern); + if (state->dir_handle == INVALID_HANDLE_VALUE) { lib_free(state); return -1; } + if (lib_findnext((intptr_t)state, data) != 0) { NtClose(state->dir_handle); lib_free(state); return -1; } + return (intptr_t)state; +} + +int lib_findnext(intptr_t handle, lib_finddata_t* data) { + if (handle == -1) return -1; + FindState* state = (FindState*)handle; + while (1) { + if (state->buf_pos < state->buf_len) { + FILE_BOTH_DIR_INFORMATION* info = (FILE_BOTH_DIR_INFORMATION*)(state->buffer + state->buf_pos); + state->buf_pos = info->NextEntryOffset ? state->buf_pos + info->NextEntryOffset : state->buf_len; + fill_finddata(info, data); + return 0; + } + IO_STATUS_BLOCK iosb = {0}; UNICODE_STRING pattern_str = {0}; + if (state->first_call && state->pattern[0]) { + pattern_str.Buffer = state->pattern; pattern_str.Length = 0; + while (state->pattern[pattern_str.Length / 2]) pattern_str.Length += 2; + pattern_str.MaximumLength = pattern_str.Length + 2; + } + NTSTATUS status = NtQueryDirectoryFile(state->dir_handle, NULL, NULL, NULL, &iosb, state->buffer, sizeof(state->buffer), + FileBothDirectoryInformation, 0, state->first_call ? &pattern_str : NULL, state->first_call ? 1 : 0); + state->first_call = false; + if (status == STATUS_NO_MORE_FILES || !NT_SUCCESS(status)) return -1; + state->buf_pos = 0; state->buf_len = (uint32_t)iosb.Information; + } +} + +int lib_findclose(intptr_t handle) { + if (handle == -1) return 0; + FindState* state = (FindState*)handle; + NtClose(state->dir_handle); lib_free(state); + return 0; +} + +bool lib_set_file_times_ntfs(FILE* f, int64_t modified_time, int64_t access_time) { + if (!f || !f->handle || f->handle == INVALID_HANDLE_VALUE) return false; + FILE_BASIC_INFORMATION info = {0}; IO_STATUS_BLOCK iosb; + NtQueryInformationFile(f->handle, &iosb, &info, sizeof(info), FileBasicInformation); + info.LastWriteTime.QuadPart = modified_time; + info.LastAccessTime.QuadPart = access_time; + return NT_SUCCESS(NtSetInformationFile(f->handle, &iosb, &info, sizeof(info), FileBasicInformation)); +} + +int lib_wutime(const WCHAR* path, const void* times_ptr) { + HANDLE h = nt_open_file(path, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN, FILE_NON_DIRECTORY_FILE); + if (h == INVALID_HANDLE_VALUE) return -1; + typedef struct { int64_t actime; int64_t modtime; } utimbuf64; + const utimbuf64* times = times_ptr; + FILE_BASIC_INFORMATION info = {0}; IO_STATUS_BLOCK iosb; + NtQueryInformationFile(h, &iosb, &info, sizeof(info), FileBasicInformation); + if (times) { + info.LastAccessTime.QuadPart = (times->actime + 11644473600LL) * 10000000LL; + info.LastWriteTime.QuadPart = (times->modtime + 11644473600LL) * 10000000LL; + } + NTSTATUS status = NtSetInformationFile(h, &iosb, &info, sizeof(info), FileBasicInformation); + NtClose(h); + return NT_SUCCESS(status) ? 0 : -1; +} + +int lib_wutime_dir(const WCHAR* path, const void* times_ptr) { + HANDLE h = nt_open_file(path, FILE_WRITE_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_OPEN, FILE_DIRECTORY_FILE); + if (h == INVALID_HANDLE_VALUE) return -1; + typedef struct { int64_t actime; int64_t modtime; } utimbuf64; + const utimbuf64* times = times_ptr; + FILE_BASIC_INFORMATION info = {0}; IO_STATUS_BLOCK iosb; + NtQueryInformationFile(h, &iosb, &info, sizeof(info), FileBasicInformation); + if (times) { + info.LastAccessTime.QuadPart = (times->actime + 11644473600LL) * 10000000LL; + info.LastWriteTime.QuadPart = (times->modtime + 11644473600LL) * 10000000LL; + } + NTSTATUS status = NtSetInformationFile(h, &iosb, &info, sizeof(info), FileBasicInformation); + NtClose(h); + return NT_SUCCESS(status) ? 0 : -1; +} + +#else + +int lib_mkdir(const char* path) { + long ret = syscall3(SYS_mkdirat, AT_FDCWD, (long)path, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH); + if (ret < 0) { lib_errno_val = (int)(-ret); return -1; } + return 0; +} + +int lib_remove(const char* path) { + long ret = syscall3(SYS_unlinkat, AT_FDCWD, (long)path, 0); + if (ret < 0) { lib_errno_val = (int)(-ret); return -1; } + return 0; +} + +int lib_rmdir(const char* path) { + long ret = syscall3(SYS_unlinkat, AT_FDCWD, (long)path, 0x200); + if (ret < 0) { lib_errno_val = (int)(-ret); return -1; } + return 0; +} + +typedef struct { + int fd; + char dir_path[1024]; + char pattern[260]; + uint8_t buffer[4096]; + uint32_t buf_pos; + uint32_t buf_len; +} LinuxFindState; + +static bool match_pattern(const char* name, const char* pattern) { + while (*pattern) { + if (*pattern == '*') { + pattern++; + if (!*pattern) return true; + while (*name) { if (match_pattern(name, pattern)) return true; name++; } + return false; + } + if (*pattern == '?' || *pattern == *name) { pattern++; name++; } + else return false; + } + return *name == '\0'; +} + +intptr_t lib_findfirst(const char* pattern, lib_finddata_t* data) { + LinuxFindState* state = lib_malloc(sizeof(LinuxFindState)); + if (!state) return -1; + lib_memset(state, 0, sizeof(LinuxFindState)); + + lib_strncpy(state->dir_path, pattern, sizeof(state->dir_path) - 1); + char* last_slash = lib_strrchr(state->dir_path, '/'); + if (last_slash) { + lib_strncpy(state->pattern, last_slash + 1, sizeof(state->pattern) - 1); + *last_slash = '\0'; + if (state->dir_path[0] == '\0') lib_strcpy(state->dir_path, "/"); + } else { + lib_strncpy(state->pattern, state->dir_path, sizeof(state->pattern) - 1); + lib_strcpy(state->dir_path, "."); + } + + long fd = syscall3(SYS_open, (long)state->dir_path, O_RDONLY | O_DIRECTORY, 0); + if (fd < 0) { lib_free(state); lib_errno_val = (int)(-fd); return -1; } + state->fd = (int)fd; + + if (lib_findnext((intptr_t)state, data) != 0) { + syscall1(SYS_close, state->fd); + lib_free(state); + return -1; + } + return (intptr_t)state; +} + +int lib_findnext(intptr_t handle, lib_finddata_t* data) { + if (handle == -1) return -1; + LinuxFindState* state = (LinuxFindState*)handle; + + while (1) { + while (state->buf_pos < state->buf_len) { + struct linux_dirent64* d = (struct linux_dirent64*)(state->buffer + state->buf_pos); + state->buf_pos += d->d_reclen; + + if (d->d_name[0] == '.' && (d->d_name[1] == '\0' || (d->d_name[1] == '.' && d->d_name[2] == '\0'))) + continue; + + if (!match_pattern(d->d_name, state->pattern)) continue; + + lib_strncpy(data->name, d->d_name, 260); + data->attrib = (d->d_type == DT_DIR) ? _A_SUBDIR : 0; + + char full_path[1280]; + lib_snprintf(full_path, sizeof(full_path), "%s/%s", state->dir_path, d->d_name); + struct linux_stat st; + if (syscall4(SYS_newfstatat, AT_FDCWD, (long)full_path, (long)&st, 0) >= 0) { + data->size = st.st_size; + data->time_write = st.st_mtime_sec; + data->time_access = st.st_atime_sec; + data->time_create = st.st_ctime_sec; + } + return 0; + } + + long ret = syscall3(SYS_getdents64, state->fd, (long)state->buffer, sizeof(state->buffer)); + if (ret <= 0) return -1; + state->buf_pos = 0; + state->buf_len = (uint32_t)ret; + } +} + +int lib_findclose(intptr_t handle) { + if (handle == -1) return 0; + LinuxFindState* state = (LinuxFindState*)handle; + syscall1(SYS_close, state->fd); + lib_free(state); + return 0; +} + +#endif + +int lib_atoi(const char* s) { + int result = 0, sign = 1; + while (*s == ' ' || *s == '\t') s++; + if (*s == '-') { sign = -1; s++; } else if (*s == '+') s++; + while (*s >= '0' && *s <= '9') { result = result * 10 + (*s - '0'); s++; } + return sign * result; +} + +int lib_isxdigit(int c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } + +int* lib_errno_func(void) { return &lib_errno_val; } + +#ifdef PLATFORM_WINDOWS +void lib_exit(int status) { lib_fflush(lib_stdout_file); lib_fflush(lib_stderr_file); RtlExitUserProcess((NTSTATUS)status); } +#else +void lib_exit(int status) { lib_fflush(lib_stdout_file); lib_fflush(lib_stderr_file); syscall1(SYS_exit_group, status); __builtin_unreachable(); } +#endif + +#ifdef PLATFORM_WINDOWS + +static void init_stdio_handles(void) { + PEB* peb = lib_get_peb(); + RTL_USER_PROCESS_PARAMETERS* params = peb->ProcessParameters; + lib_stdin_handle = params->StandardInput; + lib_stdout_handle = params->StandardOutput; + lib_stderr_handle = params->StandardError; + + lib_stdout_impl.handle = lib_stdout_handle; + lib_stdout_impl.buffer = lib_malloc(4096); + lib_stdout_impl.buf_cap = 4096; + lib_stdout_impl.flags = LIB_FILE_WRITE; + + lib_stderr_impl.handle = lib_stderr_handle; + lib_stderr_impl.buffer = lib_malloc(256); + lib_stderr_impl.buf_cap = 256; + lib_stderr_impl.flags = LIB_FILE_WRITE; + + lib_stdin_impl.handle = lib_stdin_handle; + lib_stdin_impl.buffer = lib_malloc(4096); + lib_stdin_impl.buf_cap = 4096; + lib_stdin_impl.flags = LIB_FILE_READ; + + fd_table[0] = lib_stdin_handle; + fd_table[1] = lib_stdout_handle; + fd_table[2] = lib_stderr_handle; +} + +void lib_init(void) { + PEB* peb = lib_get_peb(); + lib_heap = peb->ProcessHeap; + init_stdio_handles(); +} + +static int parse_command_line(char*** out_argv) { + PEB* peb = lib_get_peb(); + RTL_USER_PROCESS_PARAMETERS* params = peb->ProcessParameters; + WCHAR* cmd_line = params->CommandLine.Buffer; + int cmd_len = params->CommandLine.Length / 2; + + char* utf8_cmd = lib_malloc(cmd_len * 4 + 1); + if (!utf8_cmd) return 0; + utf16_to_utf8(cmd_line, utf8_cmd, cmd_len * 4 + 1); + + int argc = 0; + char* p = utf8_cmd; + while (*p) { + while (*p == ' ' || *p == '\t') p++; + if (!*p) break; + argc++; + if (*p == '"') { p++; while (*p && *p != '"') p++; if (*p == '"') p++; } + else { while (*p && *p != ' ' && *p != '\t') p++; } + } + + char** argv = lib_malloc((argc + 1) * sizeof(char*)); + if (!argv) { lib_free(utf8_cmd); return 0; } + + p = utf8_cmd; + int arg_idx = 0; + while (*p && arg_idx < argc) { + while (*p == ' ' || *p == '\t') p++; + if (!*p) break; + char* arg_start, *arg_end; + if (*p == '"') { arg_start = ++p; while (*p && *p != '"') p++; arg_end = p; if (*p == '"') p++; } + else { arg_start = p; while (*p && *p != ' ' && *p != '\t') p++; arg_end = p; } + size_t len = arg_end - arg_start; + char* arg = lib_malloc(len + 1); + if (arg) { lib_memcpy(arg, arg_start, len); arg[len] = '\0'; argv[arg_idx++] = arg; } + } + argv[arg_idx] = NULL; + *out_argv = argv; + lib_free(utf8_cmd); + return argc; +} + +void _start(void) { + lib_init(); + char** argv; int argc = parse_command_line(&argv); + int result = lib_main(argc, argv); + for (int i = 0; i < argc; i++) lib_free(argv[i]); + lib_free(argv); + lib_exit(result); +} + +#else + +static uint8_t stdout_buf[4096]; +static uint8_t stderr_buf[256]; +static uint8_t stdin_buf[4096]; + +void lib_init(void) { + heap_init(); + + lib_stdout_impl.fd = 1; + lib_stdout_impl.buffer = stdout_buf; + lib_stdout_impl.buf_cap = sizeof(stdout_buf); + lib_stdout_impl.flags = LIB_FILE_WRITE; + + lib_stderr_impl.fd = 2; + lib_stderr_impl.buffer = stderr_buf; + lib_stderr_impl.buf_cap = sizeof(stderr_buf); + lib_stderr_impl.flags = LIB_FILE_WRITE; + + lib_stdin_impl.fd = 0; + lib_stdin_impl.buffer = stdin_buf; + lib_stdin_impl.buf_cap = sizeof(stdin_buf); + lib_stdin_impl.flags = LIB_FILE_READ; +} + +__attribute__((naked, noreturn)) void _start(void) { + __asm__ volatile ( + "xor %%rbp, %%rbp\n\t" + "mov (%%rsp), %%rdi\n\t" + "lea 8(%%rsp), %%rsi\n\t" + "call _start_main\n\t" + "mov %%rax, %%rdi\n\t" + "mov $231, %%rax\n\t" + "syscall\n\t" + ::: "memory" + ); +} + +__attribute__((used)) int _start_main(int argc, char** argv) { + lib_init(); + int ret = lib_main(argc, argv); + lib_fflush(lib_stdout_file); + lib_fflush(lib_stderr_file); + return ret; +} + +#endif diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..8d32d97 --- /dev/null +++ b/src/main.c @@ -0,0 +1,1188 @@ +#include "lib.h" +#include "bootid.h" +#include "crypto.h" +#include "aes.h" +#include "progress.h" +#include "exfat.h" +#include "ntfs.h" +#include "error.h" +#include "stream.h" +#include "common.h" + +#define PAGE_SIZE 4096 +#define BUFFER_SIZE (PAGE_SIZE * 256) +#define MAX_PATH_LENGTH 256 +#define VERSION "2026020600" + +typedef struct { + bool silent; + bool verbose; + bool extract_fs; + bool write_intermediate; + char* output_filename; + const char* output_dir; + const char* parent_file; + uint64_t total_files_extracted; + uint64_t total_bytes_extracted; + time_t start_time; +} AppContext; + +typedef struct { + BootId bootid; + uint8_t key[16]; + uint8_t iv[16]; + char os_id[4]; + char game_id[5]; + char timestamp_str[20]; + uint64_t data_offset; + uint64_t data_size; + bool is_apm3; + bool is_inner_apm3; +} DecryptInfo; + +#define PRINT(ctx, ...) do { if (!(ctx)->silent) printf(__VA_ARGS__); } while(0) +#define VERBOSE(ctx, ...) do { if ((ctx)->verbose && !(ctx)->silent) printf(__VA_ARGS__); } while(0) + +static inline void path_join(char* dest, size_t size, const char* dir, const char* name) { + if (dir) { + snprintf(dest, size, "%s%s%s", dir, PATH_SEPARATOR, name); + } else { + snprintf(dest, size, "%s", name); + } +} + +static ErrorCode do_file(AppContext* app_ctx, const char* path); + +static bool test_keys(FILE* file, uint64_t data_offset, const uint8_t* test_key, + const uint8_t* test_iv, const uint8_t* expected_header) { + uint8_t buffer[16]; + uint8_t page_iv[16]; + uint8_t decrypted[16]; + + long saved_pos = ftell(file); + if (fseek(file, (long)data_offset, SEEK_SET) != 0) { + fseek(file, saved_pos, SEEK_SET); + return false; + } + + if (fread(buffer, 1, 16, file) != 16) { + fseek(file, saved_pos, SEEK_SET); + return false; + } + fseek(file, saved_pos, SEEK_SET); + + iv_page(0, test_iv, page_iv); + + memcpy(decrypted, buffer, 16); + AES_ctx ctx; + AES_init_ctx_iv(&ctx, test_key, page_iv); + AES_CBC_decrypt_buffer(&ctx, decrypted, 16); + + return memcmp(decrypted, expected_header, 8) == 0; +} + +static ErrorCode parse_bootid(AppContext* app_ctx, FILE* file, DecryptInfo* info, uint8_t* read_buffer) { + uint8_t bootid_bytes[96]; + if (fread(bootid_bytes, 1, 96, file) != 96) { + FAIL(ERR_INVALID_BOOTID); + return ERR_INVALID_BOOTID; + } + + uint8_t decrypted[96]; + memcpy(decrypted, bootid_bytes, 96); + AES_ctx ctx; + AES_init_ctx_iv(&ctx, BOOTID_KEY, BOOTID_IV); + AES_CBC_decrypt_buffer(&ctx, decrypted, 96); + memcpy(&info->bootid, decrypted, sizeof(BootId)); + + if (info->bootid.container_type != CONTAINER_TYPE_OS && + info->bootid.container_type != CONTAINER_TYPE_APP && + info->bootid.container_type != CONTAINER_TYPE_OPTION) { + char msg[64]; + snprintf(msg, sizeof(msg), "type %d", info->bootid.container_type); + FAIL_MSG(ERR_UNKNOWN_CONTAINER, msg); + return ERR_UNKNOWN_CONTAINER; + } + + format_timestamp(&info->bootid.target_timestamp, info->timestamp_str, sizeof(info->timestamp_str)); + memcpy(info->os_id, info->bootid.os_id, 3); + info->os_id[3] = '\0'; + memcpy(info->game_id, info->bootid.game_id, 4); + info->game_id[4] = '\0'; + + info->is_apm3 = (info->bootid.container_type == CONTAINER_TYPE_OPTION) && IS_APM3_OPTION(info->bootid.game_id); + info->is_inner_apm3 = false; + info->data_offset = info->bootid.header_block_count * info->bootid.block_size; + info->data_size = (info->bootid.block_count - info->bootid.header_block_count) * info->bootid.block_size; + + const char* id = (info->bootid.container_type == CONTAINER_TYPE_OS) ? info->os_id : info->game_id; + + VERBOSE(app_ctx, " %s %s %s #%d AES:%s\n", + (info->bootid.container_type == CONTAINER_TYPE_OS) ? "OS" : + (info->bootid.container_type == CONTAINER_TYPE_APP) ? "APP" : + info->is_apm3 ? "APM3" : "OPT", id, info->timestamp_str, info->bootid.sequence_number, + aes_hw_supported() ? "HW" : "SW"); + + GameKeys keys; + bool got_keys = false; + const char* key_source = NULL; + + if (info->bootid.container_type == CONTAINER_TYPE_OS || info->bootid.container_type == CONTAINER_TYPE_APP) { + got_keys = key_game(id, &keys); + if (got_keys) key_source = keys.external ? "ext" : "int"; + } else if (info->is_apm3) { + memcpy(keys.key, OPTION_KEY, 16); + memcpy(keys.iv, OPTION_IV, 16); + keys.has_iv = true; + got_keys = true; + key_source = "apm3"; + } else { + uint8_t derived_key[16], derived_iv[16]; + if (key_derive(info->game_id, derived_key, derived_iv) && + test_keys(file, info->data_offset, derived_key, derived_iv, NTFS_HEADER)) { + memcpy(keys.key, derived_key, 16); + memcpy(keys.iv, derived_iv, 16); + keys.has_iv = true; + got_keys = true; + info->is_inner_apm3 = true; + key_source = "derived"; + } else { + memcpy(keys.key, OPTION_KEY, 16); + keys.has_iv = false; + got_keys = true; + key_source = "optkey"; + } + } + + if (!got_keys) { + FAIL_MSG(ERR_KEY_NOT_FOUND, id); + return ERR_KEY_NOT_FOUND; + } + + VERBOSE(app_ctx, " key:%s\n", key_source); + memcpy(info->key, keys.key, 16); + + bool has_iv = !info->bootid.use_custom_iv && keys.has_iv; + if (has_iv) { + memcpy(info->iv, keys.iv, 16); + VERBOSE(app_ctx, " iv:key\n"); + } else { + if (fseek(file, (long)info->data_offset, SEEK_SET) != 0) { + FAIL(ERR_FILE_SEEK); + return ERR_FILE_SEEK; + } + if (fread(read_buffer, 1, PAGE_SIZE, file) != PAGE_SIZE) { + FAIL(ERR_FILE_READ); + return ERR_FILE_READ; + } + const uint8_t* header = (info->bootid.container_type == CONTAINER_TYPE_OPTION && !info->is_apm3 && !info->is_inner_apm3) ? EXFAT_HEADER : NTFS_HEADER; + if (!iv_file(info->key, header, read_buffer, info->iv)) { + FAIL(ERR_IV_CALCULATION); + return ERR_IV_CALCULATION; + } + VERBOSE(app_ctx, " iv:calc\n"); + } + + return ERR_OK; +} + +static void format_basename(const DecryptInfo* info, char* out, size_t size) { + if (info->bootid.container_type == CONTAINER_TYPE_OS) { + snprintf(out, size, "%s_%04d%02d%02d_%s_%d", + info->os_id, info->bootid.os_version.major, info->bootid.os_version.minor, + info->bootid.os_version.release, info->timestamp_str, info->bootid.sequence_number); + } else if (info->bootid.container_type == CONTAINER_TYPE_APP) { + if (info->bootid.sequence_number > 0) { + snprintf(out, size, "%s_%d%02d%02d_%s_%d_%d%02d%02d", + info->game_id, info->bootid.target_version.version.major, info->bootid.target_version.version.minor, + info->bootid.target_version.version.release, info->timestamp_str, info->bootid.sequence_number, + info->bootid.source_version.major, info->bootid.source_version.minor, info->bootid.source_version.release); + } else { + snprintf(out, size, "%s_%d%02d%02d_%s_%d", + info->game_id, info->bootid.target_version.version.major, info->bootid.target_version.version.minor, + info->bootid.target_version.version.release, info->timestamp_str, info->bootid.sequence_number); + } + } else if (info->bootid.container_type == CONTAINER_TYPE_OPTION) { + char option_str[5]; + memcpy(option_str, info->bootid.target_version.option, 4); + option_str[4] = '\0'; + snprintf(out, size, "%s_%s_%s_%d", + info->game_id, option_str, info->timestamp_str, info->bootid.sequence_number); + } +} + +static const char* fmt_size(uint64_t bytes, char* buffer, size_t buffer_size) { + uint64_t unit, frac; + const char* suffix; + if (bytes >= 1024ULL * 1024 * 1024) { + unit = 1024ULL * 1024 * 1024; suffix = " GB"; + } else if (bytes >= 1024ULL * 1024) { + unit = 1024ULL * 1024; suffix = " MB"; + } else if (bytes >= 1024) { + unit = 1024; suffix = " KB"; + } else { + snprintf(buffer, buffer_size, "%llu bytes", (unsigned long long)bytes); + return buffer; + } + uint64_t whole = bytes / unit; + frac = (bytes % unit) * 100 / unit; + snprintf(buffer, buffer_size, "%llu.%02llu%s", (unsigned long long)whole, (unsigned long long)frac, suffix); + return buffer; +} + +static ErrorCode do_stream(AppContext* app_ctx, const char* path); + +static bool do_inner_opt(AppContext* app_ctx, const char* opt_path, const char* output_dir) { + FILE* file = FOPEN(opt_path, "rb"); + if (!file) return false; + + uint8_t bootid_enc[96]; + if (fread(bootid_enc, 1, 96, file) != 96) { + fclose(file); + return false; + } + + uint8_t bootid_dec[96]; + memcpy(bootid_dec, bootid_enc, 96); + AES_ctx bootid_ctx; + AES_init_ctx_iv(&bootid_ctx, BOOTID_KEY, BOOTID_IV); + AES_CBC_decrypt_buffer(&bootid_ctx, bootid_dec, 96); + + BootId bootid; + memcpy(&bootid, bootid_dec, sizeof(BootId)); + + if (bootid.container_type != CONTAINER_TYPE_OPTION) { + fclose(file); + return false; + } + + char inner_game_id[5]; + memcpy(inner_game_id, bootid.game_id, 4); + inner_game_id[4] = '\0'; + + uint64_t data_offset = bootid.header_block_count * bootid.block_size; + uint64_t data_size = (bootid.block_count - bootid.header_block_count) * bootid.block_size; + + uint8_t first_page[PAGE_SIZE]; + if (fseek(file, (long)data_offset, SEEK_SET) != 0 || + fread(first_page, 1, PAGE_SIZE, file) != PAGE_SIZE) { + fclose(file); + return false; + } + + uint8_t derived_key[16], derived_iv[16]; + uint8_t inner_iv[16]; + bool is_inner_apm3 = false; + + if (key_derive(inner_game_id, derived_key, derived_iv)) { + uint8_t page_iv[16]; + uint8_t test_decrypt[16]; + + iv_page(0, derived_iv, page_iv); + memcpy(test_decrypt, first_page, 16); + AES_ctx test_ctx; + AES_init_ctx_iv(&test_ctx, derived_key, page_iv); + AES_CBC_decrypt_buffer(&test_ctx, test_decrypt, 16); + + if (memcmp(test_decrypt, NTFS_HEADER, 8) == 0) { + is_inner_apm3 = true; + memcpy(inner_iv, derived_iv, 16); + } + } + + if (!is_inner_apm3) { + memcpy(derived_key, OPTION_KEY, 16); + if (!iv_file(derived_key, EXFAT_HEADER, first_page, inner_iv)) { + fclose(file); + return false; + } + } + + DecryptStream* stream = malloc(sizeof(DecryptStream)); + if (!stream) { + fclose(file); + return false; + } + + if (!stream_init(stream, file, data_offset, data_size, derived_key, inner_iv)) { + free(stream); + fclose(file); + return false; + } + + bool success = false; + + if (is_inner_apm3) { + NTFSContext inner_ntfs = {0}; + inner_ntfs.silent = app_ctx->silent; + inner_ntfs.verbose = app_ctx->verbose; + + if (ntfs_init_stream(&inner_ntfs, stream, output_dir)) { + inner_ntfs.silent = true; + + if (ntfs_extract_all(&inner_ntfs)) { + inner_ntfs.silent = app_ctx->silent; + + bool is_orphan = false; + if (inner_ntfs.pending_vhd_count > 0) { + ntfs_extract_pending_vhds(&inner_ntfs, app_ctx->silent, app_ctx->verbose, &is_orphan); + } + + if (is_orphan) { + VERBOSE(app_ctx, " orphan (use -p)\n"); + RMDIR(output_dir); + } else { + VERBOSE(app_ctx, " inner ok\n"); + app_ctx->total_files_extracted += inner_ntfs.files_extracted; + app_ctx->total_bytes_extracted += inner_ntfs.extracted_bytes; + success = true; + } + } + ntfs_close(&inner_ntfs); + } + } else { + ExfatContext inner_exfat; + if (exfat_init_stream(&inner_exfat, stream)) { + inner_exfat.silent = app_ctx->silent; + inner_exfat.verbose = app_ctx->verbose; + + if (exfat_extract_all(&inner_exfat, output_dir)) { + VERBOSE(app_ctx, " inner ok\n"); + app_ctx->total_files_extracted += inner_exfat.files_extracted; + app_ctx->total_bytes_extracted += inner_exfat.extracted_bytes; + success = true; + } + exfat_close(&inner_exfat); + } + } + + free(stream); + fclose(file); + return success; +} + +static void do_inner_opts(AppContext* app_ctx, const char* dir_path) { + char search_path[MAX_PATH_LENGTH]; + snprintf(search_path, sizeof(search_path), "%s%s*.opt", dir_path, PATH_SEPARATOR); + + lib_finddata_t find_data; + intptr_t hFind = _findfirst(search_path, &find_data); + + if (hFind == -1) return; + + do { + if (find_data.attrib & _A_SUBDIR) continue; + + char opt_path[MAX_PATH_LENGTH]; + path_join(opt_path, sizeof(opt_path), dir_path, find_data.name); + + char opt_basename[MAX_PATH_LENGTH]; + strncpy(opt_basename, find_data.name, sizeof(opt_basename) - 1); + opt_basename[sizeof(opt_basename) - 1] = '\0'; + char* ext = strrchr(opt_basename, '.'); + if (ext) *ext = '\0'; + + char inner_output_dir[MAX_PATH_LENGTH]; + path_join(inner_output_dir, sizeof(inner_output_dir), dir_path, opt_basename); + + VERBOSE(app_ctx, " opt:%s\n", find_data.name); + + if (do_inner_opt(app_ctx, opt_path, inner_output_dir)) { + REMOVE(opt_path); + } + } while (_findnext(hFind, &find_data) == 0); + + _findclose(hFind); +} + +ErrorCode do_file(AppContext* app_ctx, const char* path) { + if (!app_ctx->write_intermediate && app_ctx->extract_fs) { + return do_stream(app_ctx, path); + } + + ErrorCode result = ERR_EXTRACTION_FAILED; + uint8_t* read_buffer = NULL; + uint8_t* decrypted_buffer = NULL; + char* output_filename = NULL; + FILE* file = NULL; + FILE* output_file = NULL; + uint8_t page_iv[16]; + + read_buffer = malloc(BUFFER_SIZE); + decrypted_buffer = malloc(BUFFER_SIZE); + if (!read_buffer || !decrypted_buffer) { + FAIL(ERR_MEMORY); + result = ERR_MEMORY; + goto cleanup; + } + + file = FOPEN(path, "rb"); + if (!file) { + FAIL_MSG(ERR_FILE_OPEN, path); + result = ERR_FILE_OPEN; + goto cleanup; + } + + DecryptInfo info; + result = parse_bootid(app_ctx, file, &info, read_buffer); + if (result != ERR_OK) goto cleanup; + + output_filename = malloc(MAX_PATH_LENGTH); + if (!output_filename) { + FAIL(ERR_MEMORY); + result = ERR_MEMORY; + goto cleanup; + } + + char basename[MAX_PATH_LENGTH]; + format_basename(&info, basename, MAX_PATH_LENGTH); + const char* ext = (info.bootid.container_type == CONTAINER_TYPE_OPTION && !info.is_apm3 && !info.is_inner_apm3) ? ".exfat" : ".ntfs"; + strncat(basename, ext, MAX_PATH_LENGTH - strlen(basename) - 1); + path_join(output_filename, MAX_PATH_LENGTH, app_ctx->output_dir, basename); + + if (app_ctx->output_dir) create_directories(app_ctx->output_dir); + + output_file = FOPEN(output_filename, "wb"); + if (!output_file) { + FAIL_MSG(ERR_FILE_OPEN, output_filename); + result = ERR_FILE_OPEN; + goto cleanup; + } + + if (info.data_size % 16 != 0) { + char msg[64]; + snprintf(msg, sizeof(msg), "%llu bytes", (unsigned long long)info.data_size); + FAIL_MSG(ERR_AES_ALIGNMENT, msg); + result = ERR_AES_ALIGNMENT; + goto cleanup; + } + + if (fseek(file, (long)info.data_offset, SEEK_SET) != 0) { + FAIL(ERR_FILE_SEEK); + result = ERR_FILE_SEEK; + goto cleanup; + } + + AES_ctx page_ctx; + AES_init_ctx_iv(&page_ctx, info.key, info.iv); + + Progress progress; + if (!app_ctx->silent) { + progress_init(&progress, info.data_size); + } + + uint64_t total_bytes_read = 0; + uint64_t bytes_remaining = info.data_size; + + while (bytes_remaining > 0) { + size_t chunk_size = (bytes_remaining > BUFFER_SIZE) ? BUFFER_SIZE : (size_t)bytes_remaining; + + size_t read_size = fread(read_buffer, 1, chunk_size, file); + if (read_size != chunk_size) { + FAIL_MSG(ERR_FILE_READ, feof(file) ? "unexpected end of file" : "read error"); + result = ERR_FILE_READ; + goto cleanup; + } + + size_t offset = 0; + while (offset < read_size) { + size_t block_size = (read_size - offset > PAGE_SIZE) ? PAGE_SIZE : (read_size - offset); + + uint64_t file_offset = total_bytes_read + offset; + iv_page(file_offset, info.iv, page_iv); + + memcpy(decrypted_buffer + offset, read_buffer + offset, block_size); + AES_ctx_set_iv(&page_ctx, page_iv); + AES_CBC_decrypt_buffer(&page_ctx, decrypted_buffer + offset, block_size); + + offset += block_size; + } + + if (fwrite(decrypted_buffer, 1, read_size, output_file) != read_size) { + FAIL(ERR_FILE_WRITE); + result = ERR_FILE_WRITE; + goto cleanup; + } + + total_bytes_read += read_size; + bytes_remaining -= read_size; + + if (!app_ctx->silent) { + progress_update(&progress, total_bytes_read); + } + } + + if (!app_ctx->silent) { + progress_finish(&progress); + } + PRINT(app_ctx, "ok: %s\n", output_filename); + + if (app_ctx->extract_fs) { + if (app_ctx->output_filename) free(app_ctx->output_filename); + app_ctx->output_filename = output_filename; + output_filename = NULL; + } + + result = ERR_OK; + +cleanup: + if (file) fclose(file); + if (output_file) fclose(output_file); + free(read_buffer); + free(decrypted_buffer); + free(output_filename); + return result; +} + +static ErrorCode do_stream(AppContext* app_ctx, const char* path) { + ErrorCode result = ERR_EXTRACTION_FAILED; + uint8_t* read_buffer = NULL; + FILE* file = NULL; + DecryptStream* stream = NULL; + + read_buffer = malloc(PAGE_SIZE); + stream = malloc(sizeof(DecryptStream)); + if (!read_buffer || !stream) { + FAIL(ERR_MEMORY); + result = ERR_MEMORY; + goto cleanup; + } + + file = FOPEN(path, "rb"); + if (!file) { + FAIL_MSG(ERR_FILE_OPEN, path); + result = ERR_FILE_OPEN; + goto cleanup; + } + + DecryptInfo info; + result = parse_bootid(app_ctx, file, &info, read_buffer); + if (result != ERR_OK) goto cleanup; + + if (!stream_init(stream, file, info.data_offset, info.data_size, info.key, info.iv)) { + FAIL(ERR_EXTRACTION_FAILED); + result = ERR_EXTRACTION_FAILED; + goto cleanup; + } + + char output_dir[MAX_PATH_LENGTH]; + char basename_no_ext[MAX_PATH_LENGTH]; + format_basename(&info, basename_no_ext, sizeof(basename_no_ext)); + path_join(output_dir, sizeof(output_dir), app_ctx->output_dir, basename_no_ext); + + bool extraction_success = false; + + if (info.bootid.container_type == CONTAINER_TYPE_OPTION && !info.is_apm3 && !info.is_inner_apm3) { + ExfatContext ctx; + if (exfat_init_stream(&ctx, stream)) { + ctx.silent = app_ctx->silent; + ctx.verbose = app_ctx->verbose; + VERBOSE(app_ctx, "exfat c=%u f=%u\n", ctx.bytes_per_cluster, ctx.fat_length_bytes); + if (exfat_extract_all(&ctx, output_dir)) { + extraction_success = true; + app_ctx->total_files_extracted += ctx.files_extracted; + app_ctx->total_bytes_extracted += ctx.extracted_bytes; + } + exfat_close(&ctx); + } + } + else if (info.is_apm3 || info.is_inner_apm3) { + char inner_game_id[5] = {0}; + if (info.is_apm3) { + memcpy(inner_game_id, info.bootid.target_version.option, 4); + } else { + memcpy(inner_game_id, info.game_id, 4); + } + inner_game_id[4] = '\0'; + VERBOSE(app_ctx, "apm3:%s\n", inner_game_id); + + NTFSContext ctx = { 0 }; + ctx.silent = app_ctx->silent; + ctx.verbose = app_ctx->verbose; + ctx.apm3_decrypt = false; + + if (ntfs_init_stream(&ctx, stream, output_dir)) { + int vhd_type = ntfs_detect_vhd_type(&ctx); + if (vhd_type == VHD_TYPE_DIFFERENCING && !app_ctx->parent_file) { + PRINT(app_ctx, "diff VHD, use -p\n"); + ntfs_close(&ctx); + result = ERR_OK; + goto cleanup; + } + + VERBOSE(app_ctx, "MFT=%llu c=%u\n", + (unsigned long long)ctx.total_mft_records, ctx.bytes_per_cluster); + + ctx.silent = true; + + if (ntfs_extract_all(&ctx)) { + extraction_success = true; + ctx.silent = app_ctx->silent; + + if (ctx.pending_vhd_count > 0) { + bool is_orphan = false; + ntfs_extract_pending_vhds(&ctx, app_ctx->silent, app_ctx->verbose, &is_orphan); + } + + app_ctx->total_files_extracted += ctx.files_extracted; + app_ctx->total_bytes_extracted += ctx.extracted_bytes; + + if (info.is_apm3 && ctx.pending_opt_count > 0) { + int pending_count = ctx.pending_opt_count; + + for (int i = 0; i < pending_count; i++) { + const PendingOpt* opt = ntfs_get_pending_opt(&ctx, i); + if (!opt) continue; + + VERBOSE(app_ctx, "opt:%s\n", opt->filename); + + uint8_t bootid_enc[96]; + RunSource run_src = {0}; + run_src.ntfs_ctx = &ctx; + run_src.run_count = (opt->run_count < MAX_DATA_RUNS) ? opt->run_count : MAX_DATA_RUNS; + run_src.file_size = opt->file_size; + for (int r = 0; r < run_src.run_count; r++) { + run_src.runs[r].offset = opt->runs[r].offset; + run_src.runs[r].length = opt->runs[r].length; + } + + if (!stream_read_raw(&ctx, run_src.runs, run_src.run_count, + run_src.file_size, 0, bootid_enc, 96)) { + fprintf(stderr, "optrd\n"); + continue; + } + + uint8_t bootid_dec[96]; + memcpy(bootid_dec, bootid_enc, 96); + AES_ctx bootid_aes; + AES_init_ctx_iv(&bootid_aes, BOOTID_KEY, BOOTID_IV); + AES_CBC_decrypt_buffer(&bootid_aes, bootid_dec, 96); + + BootId inner_bootid; + memcpy(&inner_bootid, bootid_dec, sizeof(BootId)); + + if (inner_bootid.container_type != CONTAINER_TYPE_OPTION) continue; + + char inner_game_id[5]; + memcpy(inner_game_id, inner_bootid.game_id, 4); + inner_game_id[4] = '\0'; + + uint64_t inner_data_offset = inner_bootid.header_block_count * inner_bootid.block_size; + uint64_t inner_data_size = (inner_bootid.block_count - inner_bootid.header_block_count) * inner_bootid.block_size; + + uint8_t first_page[PAGE_SIZE]; + if (!stream_read_raw(&ctx, run_src.runs, run_src.run_count, + run_src.file_size, inner_data_offset, first_page, PAGE_SIZE)) { + fprintf(stderr, "pgrd\n"); + continue; + } + + uint8_t derived_key[16], derived_iv[16]; + uint8_t inner_iv[16]; + bool is_inner_apm3 = false; + + if (key_derive(inner_game_id, derived_key, derived_iv)) { + uint8_t page_iv[16]; + uint8_t test_decrypt[16]; + + iv_page(0, derived_iv, page_iv); + memcpy(test_decrypt, first_page, 16); + AES_ctx test_ctx; + AES_init_ctx_iv(&test_ctx, derived_key, page_iv); + AES_CBC_decrypt_buffer(&test_ctx, test_decrypt, 16); + + if (memcmp(test_decrypt, NTFS_HEADER, 8) == 0) { + is_inner_apm3 = true; + memcpy(inner_iv, derived_iv, 16); + } + } + + if (!is_inner_apm3) { + memcpy(derived_key, OPTION_KEY, 16); + if (!iv_file(derived_key, EXFAT_HEADER, first_page, inner_iv)) continue; + } + + char inner_output_dir[MAX_PATH_LENGTH]; + char opt_basename[MAX_FILENAME_LENGTH]; + strncpy(opt_basename, opt->filename, sizeof(opt_basename) - 1); + opt_basename[sizeof(opt_basename) - 1] = '\0'; + char* ext = strrchr(opt_basename, '.'); + if (ext) *ext = '\0'; + path_join(inner_output_dir, sizeof(inner_output_dir), output_dir, opt_basename); + + RunSource* inner_run_src = malloc(sizeof(RunSource)); + if (!inner_run_src) continue; + memset(inner_run_src, 0, sizeof(RunSource)); + + inner_run_src->ntfs_ctx = &ctx; + inner_run_src->run_count = (opt->run_count < MAX_DATA_RUNS) ? opt->run_count : MAX_DATA_RUNS; + inner_run_src->file_size = opt->file_size; + for (int r = 0; r < inner_run_src->run_count; r++) { + inner_run_src->runs[r].offset = opt->runs[r].offset; + inner_run_src->runs[r].length = opt->runs[r].length; + } + + DecryptStream* inner_stream = malloc(sizeof(DecryptStream)); + if (!inner_stream) { + free(inner_run_src); + continue; + } + + if (!stream_init_from_runs(inner_stream, inner_run_src, derived_key, inner_iv)) { + free(inner_run_src); + free(inner_stream); + continue; + } + + inner_stream->data_offset = inner_data_offset; + inner_stream->data_size = inner_data_size; + + if (is_inner_apm3) { + NTFSContext inner_ntfs = {0}; + inner_ntfs.silent = app_ctx->silent; + inner_ntfs.verbose = app_ctx->verbose; + + if (ntfs_init_stream(&inner_ntfs, inner_stream, inner_output_dir)) { + inner_ntfs.silent = true; + + if (ntfs_extract_all(&inner_ntfs)) { + inner_ntfs.silent = app_ctx->silent; + + bool is_orphan = false; + if (inner_ntfs.pending_vhd_count > 0) { + ntfs_extract_pending_vhds(&inner_ntfs, app_ctx->silent, app_ctx->verbose, &is_orphan); + } + + if (is_orphan) { + VERBOSE(app_ctx, " orphan\n"); + RMDIR(inner_output_dir); + } else { + VERBOSE(app_ctx, " inner ok\n"); + app_ctx->total_files_extracted += inner_ntfs.files_extracted; + app_ctx->total_bytes_extracted += inner_ntfs.extracted_bytes; + } + } + ntfs_close(&inner_ntfs); + } + } else { + ExfatContext inner_exfat; + if (exfat_init_stream(&inner_exfat, inner_stream)) { + inner_exfat.silent = app_ctx->silent; + inner_exfat.verbose = app_ctx->verbose; + + if (exfat_extract_all(&inner_exfat, inner_output_dir)) { + VERBOSE(app_ctx, " inner ok\n"); + app_ctx->total_files_extracted += inner_exfat.files_extracted; + app_ctx->total_bytes_extracted += inner_exfat.extracted_bytes; + } + exfat_close(&inner_exfat); + } + } + + free(inner_run_src); + free(inner_stream); + } + + ntfs_close(&ctx); + } else { + ntfs_close(&ctx); + } + } + else { + fprintf(stderr, "ntfs\n"); + ntfs_close(&ctx); + } + } + else { + fprintf(stderr, "ntfsi\n"); + } + } + else { + NTFSContext ctx = { 0 }; + ctx.silent = app_ctx->silent; + ctx.verbose = app_ctx->verbose; + if (ntfs_init_stream(&ctx, stream, output_dir)) { + int vhd_type = ntfs_detect_vhd_type(&ctx); + if (vhd_type == VHD_TYPE_DIFFERENCING && !app_ctx->parent_file) { + PRINT(app_ctx, "diff VHD, use -p\n"); + ntfs_close(&ctx); + result = ERR_OK; + goto cleanup; + } + + VERBOSE(app_ctx, "MFT=%llu c=%u\n", + (unsigned long long)ctx.total_mft_records, ctx.bytes_per_cluster); + + ctx.silent = true; + + if (ntfs_extract_all(&ctx)) { + extraction_success = true; + ctx.silent = app_ctx->silent; + + if (ctx.pending_vhd_count > 0) { + bool is_orphan = false; + ntfs_extract_pending_vhds(&ctx, app_ctx->silent, app_ctx->verbose, &is_orphan); + } + + app_ctx->total_files_extracted += ctx.files_extracted; + app_ctx->total_bytes_extracted += ctx.extracted_bytes; + } + else { + fprintf(stderr, "ntfs\n"); + } + ntfs_close(&ctx); + } + else { + fprintf(stderr, "ntfsi\n"); + } + } + + if (extraction_success) { + result = ERR_OK; + } + +cleanup: + if (file) fclose(file); + free(read_buffer); + free(stream); + return result; +} + +#ifdef PLATFORM_WINDOWS + +static bool env_has_prefix(const WCHAR* env, const WCHAR* prefix) { + while (*prefix) { + if (*env != *prefix) return false; + env++; prefix++; + } + return true; +} + +static bool wchar_iequals(const WCHAR* a, const WCHAR* b) { + while (*a && *b) { + WCHAR ca = *a, cb = *b; + if (ca >= 'A' && ca <= 'Z') ca += 32; + if (cb >= 'A' && cb <= 'Z') cb += 32; + if (ca != cb) return false; + a++; b++; + } + return *a == *b; +} + +static bool get_parent_process_name(WCHAR* out_name, size_t max_chars) { + out_name[0] = 0; + + PROCESS_BASIC_INFORMATION pbi; + ULONG ret_len; + #define NtCurrentProcess() ((HANDLE)(intptr_t)-1) + NTSTATUS status = NtQueryInformationProcess(NtCurrentProcess(), ProcessBasicInformation, &pbi, sizeof(pbi), &ret_len); + if (!NT_SUCCESS(status)) return false; + + uintptr_t parent_pid = pbi.InheritedFromUniqueProcessId; + if (parent_pid == 0) return false; + + ULONG buf_size = 1024 * 1024; + uint8_t* buf = NULL; + + #define STATUS_INFO_LENGTH_MISMATCH ((NTSTATUS)0xC0000004L) + for (int attempt = 0; attempt < 3; attempt++) { + buf = lib_malloc(buf_size); + if (!buf) return false; + + status = NtQuerySystemInformation(SystemProcessInformation, buf, buf_size, &ret_len); + if (NT_SUCCESS(status)) break; + + lib_free(buf); + buf = NULL; + + if (status == STATUS_INFO_LENGTH_MISMATCH) { + buf_size = ret_len + 65536; + } else { + return false; + } + } + + if (!buf) return false; + + SYSTEM_PROCESS_INFORMATION* proc = (SYSTEM_PROCESS_INFORMATION*)buf; + bool found = false; + + while (1) { + if (proc->UniqueProcessId == parent_pid && proc->ImageName.Buffer) { + WCHAR* name = proc->ImageName.Buffer; + WCHAR* last_slash = name; + for (WCHAR* p = name; *p; p++) { + if (*p == '\\' || *p == '/') last_slash = p + 1; + } + size_t i = 0; + while (last_slash[i] && i < max_chars - 1) { + out_name[i] = last_slash[i]; + i++; + } + out_name[i] = 0; + found = true; + break; + } + if (proc->NextEntryOffset == 0) break; + proc = (SYSTEM_PROCESS_INFORMATION*)((uint8_t*)proc + proc->NextEntryOffset); + } + + lib_free(buf); + return found; +} + +static bool from_explorer(void) { + WCHAR parent_name[260]; + if (get_parent_process_name(parent_name, 260)) { + static const WCHAR cmd[] = {'c','m','d','.','e','x','e',0}; + static const WCHAR powershell[] = {'p','o','w','e','r','s','h','e','l','l','.','e','x','e',0}; + static const WCHAR pwsh[] = {'p','w','s','h','.','e','x','e',0}; + static const WCHAR wt[] = {'W','i','n','d','o','w','s','T','e','r','m','i','n','a','l','.','e','x','e',0}; + static const WCHAR code[] = {'C','o','d','e','.','e','x','e',0}; + static const WCHAR conhost[] = {'c','o','n','h','o','s','t','.','e','x','e',0}; + + if (wchar_iequals(parent_name, cmd)) return false; + if (wchar_iequals(parent_name, powershell)) return false; + if (wchar_iequals(parent_name, pwsh)) return false; + if (wchar_iequals(parent_name, wt)) return false; + if (wchar_iequals(parent_name, code)) return false; + if (wchar_iequals(parent_name, conhost)) return false; + + static const WCHAR explorer[] = {'e','x','p','l','o','r','e','r','.','e','x','e',0}; + if (wchar_iequals(parent_name, explorer)) return true; + } + + PEB* peb = lib_get_peb(); + RTL_USER_PROCESS_PARAMETERS* params = peb->ProcessParameters; + WCHAR* env = params->Environment; + if (!env) return true; + + static const WCHAR prompt[] = {'P','R','O','M','P','T','=',0}; + static const WCHAR wt_session[] = {'W','T','_','S','E','S','S','I','O','N','=',0}; + static const WCHAR term_prog[] = {'T','E','R','M','_','P','R','O','G','R','A','M','=',0}; + + WCHAR* scan = env; + while (*scan) { + if (env_has_prefix(scan, prompt)) return false; + if (env_has_prefix(scan, wt_session)) return false; + if (env_has_prefix(scan, term_prog)) return false; + while (*scan) scan++; + scan++; + } + + return true; +} + +#else + +static bool from_explorer(void) { + return false; +} + +#endif + +static void show_usage(void) { + puts("unsegaREBORN [flags] \n-o dir -n -w -p parent -s -v -vn"); +} + +#ifdef PLATFORM_WINDOWS +__declspec(dllimport) NTSTATUS __stdcall NtDelayExecution(uint8_t Alertable, LARGE_INTEGER* DelayInterval); +#endif + +static void wait_for_enter(void) { +#ifdef PLATFORM_WINDOWS + extern HANDLE lib_stdin_handle; + if (!lib_stdin_handle || lib_stdin_handle == INVALID_HANDLE_VALUE) return; + + char buf[16]; + IO_STATUS_BLOCK iosb = {0}; + NtReadFile(lib_stdin_handle, NULL, NULL, NULL, &iosb, buf, 1, NULL, NULL); +#else + char c; + lib_fread(&c, 1, 1, stdin); +#endif +} + +static void show_info(void) { + printf("unsegaREBORN %s\ndrag files or use flags\nkeys: keys.inc\nenter...", VERSION); + fflush(stdout); + wait_for_enter(); +} + +int lib_main(int argc, char** argv) { + AppContext app_ctx = {0}; + app_ctx.extract_fs = true; + app_ctx.start_time = time(NULL); + + int start_index = 1; + + if (argc < 2) { + if (from_explorer()) { + show_info(); + } else { + show_usage(); + } + return 0; + } + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-vn") == 0) { + printf("unsegaREBORN %s\n", VERSION); + return 0; + } + if (strcmp(argv[i], "-h") == 0) { + show_usage(); + return 0; + } + } + + const char* input_files[256]; + int input_file_count = 0; + + for (int i = start_index; i < argc && input_file_count < 256; i++) { + const char* a = argv[i]; + if (a[0] == '-') { + if (strcmp(a, "-n") == 0) app_ctx.extract_fs = false; + else if (strcmp(a, "-s") == 0) app_ctx.silent = true; + else if (strcmp(a, "-v") == 0) app_ctx.verbose = true; + else if (strcmp(a, "-w") == 0) app_ctx.write_intermediate = true; + else if (strcmp(a, "-o") == 0 && i + 1 < argc) app_ctx.output_dir = argv[++i]; + else if (strcmp(a, "-p") == 0 && i + 1 < argc) app_ctx.parent_file = argv[++i]; + continue; + } + input_files[input_file_count++] = a; + } + + if (input_file_count == 0) { fprintf(stderr, "no files\n"); return 1; } + if (!key_any()) fprintf(stderr, "no keys\n"); + + bool any_failed = false; + for (int i = 0; i < input_file_count; ++i) { + const char* file_path = input_files[i]; + if (do_file(&app_ctx, file_path) == ERR_OK) { + if (app_ctx.extract_fs && app_ctx.output_filename) { + char output_dir[MAX_PATH_LENGTH]; + + const char* basename = strrchr(app_ctx.output_filename, '/'); + if (!basename) basename = strrchr(app_ctx.output_filename, '\\'); + basename = basename ? basename + 1 : app_ctx.output_filename; + + char basename_no_ext[MAX_PATH_LENGTH]; + strncpy(basename_no_ext, basename, sizeof(basename_no_ext) - 1); + basename_no_ext[sizeof(basename_no_ext) - 1] = '\0'; + char* ext = strrchr(basename_no_ext, '.'); + if (ext) *ext = '\0'; + + if (app_ctx.output_dir) { + path_join(output_dir, sizeof(output_dir), app_ctx.output_dir, basename_no_ext); + } else { + strncpy(output_dir, app_ctx.output_filename, sizeof(output_dir) - 1); + output_dir[sizeof(output_dir) - 1] = '\0'; + ext = strrchr(output_dir, '.'); + if (ext) *ext = '\0'; + } + + bool extraction_success = false; + + if (strstr(app_ctx.output_filename, ".exfat") != NULL) { + ExfatContext ctx; + if (exfat_init(&ctx, app_ctx.output_filename)) { + ctx.silent = app_ctx.silent; + ctx.verbose = app_ctx.verbose; + VERBOSE(&app_ctx, "exfat c=%u f=%u\n", ctx.bytes_per_cluster, ctx.fat_length_bytes); + if (exfat_extract_all(&ctx, output_dir)) { + extraction_success = true; + app_ctx.total_files_extracted += ctx.files_extracted; + app_ctx.total_bytes_extracted += ctx.extracted_bytes; + } + exfat_close(&ctx); + } + } + else if (strstr(app_ctx.output_filename, ".ntfs") != NULL) { + NTFSContext ctx = { 0 }; + ctx.silent = app_ctx.silent; + ctx.verbose = app_ctx.verbose; + if (ntfs_init(&ctx, app_ctx.output_filename, output_dir)) { + int vhd_type = ntfs_detect_vhd_type(&ctx); + if (vhd_type == VHD_TYPE_DIFFERENCING && !app_ctx.parent_file) { + PRINT(&app_ctx, "\ndiff VHD, use -p\n"); + ntfs_close(&ctx); + } else { + VERBOSE(&app_ctx, "ntfs MFT=%llu c=%u\n", + (unsigned long long)ctx.total_mft_records, ctx.bytes_per_cluster); + + if (ntfs_extract_all(&ctx)) { + extraction_success = true; + app_ctx.total_files_extracted += ctx.files_extracted; + app_ctx.total_bytes_extracted += ctx.extracted_bytes; + + char vhd_path[MAX_PATH_LENGTH]; + int highest_vhd = -1; + + for (int vhd_num = 0; vhd_num < 10; vhd_num++) { + snprintf(vhd_path, sizeof(vhd_path), "%s%sinternal_%d.vhd", + output_dir, PATH_SEPARATOR, vhd_num); + + FILE* test = FOPEN(vhd_path, "rb"); + if (!test) continue; + fclose(test); + highest_vhd = vhd_num; + } + + if (highest_vhd >= 0) { + snprintf(vhd_path, sizeof(vhd_path), "%s%sinternal_%d.vhd", + output_dir, PATH_SEPARATOR, highest_vhd); + + char vhd_output_dir[MAX_PATH_LENGTH]; + snprintf(vhd_output_dir, sizeof(vhd_output_dir), "%s%scontents", + output_dir, PATH_SEPARATOR); + + NTFSContext vhd_ctx = { 0 }; + vhd_ctx.silent = app_ctx.silent; + vhd_ctx.verbose = app_ctx.verbose; + + if (ntfs_init(&vhd_ctx, vhd_path, vhd_output_dir)) { + VERBOSE(&app_ctx, "vhd MFT=%llu c=%u\n", + (unsigned long long)vhd_ctx.total_mft_records, vhd_ctx.bytes_per_cluster); + if (ntfs_extract_all(&vhd_ctx)) { + app_ctx.total_files_extracted += vhd_ctx.files_extracted; + app_ctx.total_bytes_extracted += vhd_ctx.extracted_bytes; + } + ntfs_close(&vhd_ctx); + } + } + + do_inner_opts(&app_ctx, output_dir); + } + ntfs_close(&ctx); + } + } + } + else { + fprintf(stderr, "fs:%s\n", app_ctx.output_filename); + } + + free(app_ctx.output_filename); + app_ctx.output_filename = NULL; + } + } + else { + fprintf(stderr, "fail:%s\n", file_path); + any_failed = true; + } + } + + if (!app_ctx.silent && app_ctx.total_files_extracted > 0) { + char size_buf[32]; + fmt_size(app_ctx.total_bytes_extracted, size_buf, sizeof(size_buf)); + printf("\n%llu files %s %ds\n", + (unsigned long long)app_ctx.total_files_extracted, size_buf, + (int)difftime(time(NULL), app_ctx.start_time)); + } + + if (app_ctx.output_filename) { + free(app_ctx.output_filename); + } + + return any_failed ? 1 : 0; +} diff --git a/src/ntfs.c b/src/ntfs.c new file mode 100644 index 0000000..c3a740d --- /dev/null +++ b/src/ntfs.c @@ -0,0 +1,1669 @@ +#include "ntfs.h" +#include "exfat.h" +#include "progress.h" +#define BUFFER_SIZE (1024 * 1024) + +uint32_t g_dir_cache[DIR_CACHE_SIZE]; +bool g_dir_cache_init; + +static bool vhd_read(VHDContext* ctx, void* buffer, uint64_t offset, size_t size); +static bool vhd_init_internal(VHDContext* ctx, const char* filename, uint32_t depth); +static void vhd_close(VHDContext* ctx); +static bool read_bytes_from_runs(NTFSContext* ctx, const DataRun* runs, int run_count, + uint64_t file_size, uint64_t read_offset, void* buffer, size_t read_size); + +static uint32_t swap32(uint32_t value) { + return ((value & 0xFF000000) >> 24) | + ((value & 0x00FF0000) >> 8) | + ((value & 0x0000FF00) << 8) | + ((value & 0x000000FF) << 24); +} + +static uint64_t swap64(uint64_t value) { + return ((value & 0xFF00000000000000ULL) >> 56) | + ((value & 0x00FF000000000000ULL) >> 40) | + ((value & 0x0000FF0000000000ULL) >> 24) | + ((value & 0x000000FF00000000ULL) >> 8) | + ((value & 0x00000000FF000000ULL) << 8) | + ((value & 0x0000000000FF0000ULL) << 24) | + ((value & 0x000000000000FF00ULL) << 40) | + ((value & 0x00000000000000FFULL) << 56); +} + +static bool ntfs_read(NTFSContext* ctx, void* buffer, uint64_t offset, size_t size) { + if (ctx->stream) { + return stream_read(ctx->stream, buffer, offset, size); + } + if (ctx->is_vhd) { + return vhd_read(&ctx->vhd, buffer, offset, size); + } + if (ctx->raw_file_pos != offset) { + if (FSEEKO(ctx->raw.fp, offset, SEEK_SET) != 0) { + return false; + } + } + if (fread(buffer, 1, size, ctx->raw.fp) != size) { + return false; + } + ctx->raw_file_pos = offset + size; + return true; +} + +static bool apply_mft_fixups(const NTFSContext* ctx, uint8_t* record_buffer, size_t record_size) { + if (!record_buffer || record_size < sizeof(MFTRecordHeader)) { + return false; + } + + MFTRecordHeader* header = (MFTRecordHeader*)record_buffer; + uint16_t usa_offset = header->usa_offset; + uint16_t usa_count = header->usa_count; + + if (usa_offset == 0 || usa_count < 2) { + return true; + } + + size_t max_entries_available = 0; + if ((size_t)usa_offset < record_size) { + max_entries_available = (record_size - (size_t)usa_offset) / sizeof(uint16_t); + } + if (max_entries_available < 2) { + return true; + } + if (usa_count > (uint16_t)max_entries_available) { + usa_count = (uint16_t)max_entries_available; + } + + uint16_t* usa = (uint16_t*)(record_buffer + usa_offset); + uint16_t num_sectors = usa_count - 1; + uint16_t sector_size = (num_sectors > 0) ? (uint16_t)(record_size / num_sectors) : 512; + if (sector_size == 0) sector_size = 512; + + for (uint16_t i = 1; i <= num_sectors; i++) { + size_t tail_offset = (size_t)i * sector_size - 2; + if (tail_offset + 2 > record_size) continue; + uint16_t* tail = (uint16_t*)(record_buffer + tail_offset); + *tail = usa[i]; + } + + return true; +} + +static bool read_file_info(NTFSContext* ctx, uint64_t ref_number, FileInfo* info) { + memset(info, 0, sizeof(FileInfo)); + + uint64_t mft_offset = ctx->mft_offset + (ref_number * ctx->mft_record_size); + + if (!ctx->lookup_buffer) { + ctx->lookup_buffer = malloc(ctx->mft_record_size); + if (!ctx->lookup_buffer) return false; + } + uint8_t* record_buffer = ctx->lookup_buffer; + + bool success = false; + if (ntfs_read(ctx, record_buffer, mft_offset, ctx->mft_record_size)) { + if (!apply_mft_fixups(ctx, record_buffer, ctx->mft_record_size)) { + return false; + } + const MFTRecordHeader* record = (const MFTRecordHeader*)record_buffer; + + if (memcmp(record->magic, "FILE", 4) != 0 || !(record->flags & MFT_RECORD_IN_USE)) { + return false; + } + + uint32_t attrs_offset = record->attrs_offset; + uint32_t bytes_used = record->bytes_used; + if (attrs_offset >= ctx->mft_record_size || bytes_used > ctx->mft_record_size) { + return false; + } + bytes_used = min(bytes_used, ctx->mft_record_size); + + info->is_directory = (record->flags & MFT_RECORD_IS_DIRECTORY) != 0; + + const uint8_t* record_end = record_buffer + bytes_used; + const uint8_t* attr = record_buffer + attrs_offset; + + while (attr + sizeof(AttributeHeader) <= record_end) { + const AttributeHeader* header = (const AttributeHeader*)attr; + + if (header->type == 0xFFFFFFFF || header->length == 0) { + break; + } + if (header->length < sizeof(AttributeHeader) || attr + header->length > record_end) { + break; + } + + if (header->type == FILE_NAME_ATTR && !header->non_resident) { + uint16_t value_offset = header->data.resident.value_offset; + uint32_t value_length = header->data.resident.value_length; + if (value_offset + value_length > header->length) break; + + const FileNameAttribute* fname = (const FileNameAttribute*)(attr + value_offset); + if ((const uint8_t*)fname + 66 > attr + header->length) break; + + if (fname->namespace != 2) { + fs_name_to_utf8(fname->name, fname->name_length, info->name, sizeof(info->name)); + info->parent_ref = fname->parent_directory & 0xFFFFFFFFFFFF; + info->valid = true; + success = true; + break; + } + } + + attr += header->length; + } + } + + return success; +} + +static inline size_t hash_ref(uint64_t ref, size_t capacity) { + ref ^= ref >> 33; + ref *= 0xff51afd7ed558ccdULL; + ref ^= ref >> 33; + ref *= 0xc4ceb9fe1a85ec53ULL; + ref ^= ref >> 33; + return ref % capacity; +} + +static bool init_directory_cache(DirectoryCache* cache) { + cache->capacity = DIR_CACHE_INITIAL_SIZE; + cache->count = 0; + cache->entries = calloc(cache->capacity, sizeof(DirectoryEntry)); + if (!cache->entries) return false; + + uint64_t hash = 5 % cache->capacity; + cache->entries[hash].ref_number = 5; + cache->entries[hash].path[0] = '\0'; + cache->entries[hash].occupied = true; + cache->count = 1; + return true; +} + +static void free_directory_cache(DirectoryCache* cache) { + free(cache->entries); + cache->entries = NULL; + cache->capacity = 0; + cache->count = 0; +} + +static bool resize_directory_cache(DirectoryCache* cache) { + size_t new_capacity = cache->capacity * 2; + DirectoryEntry* new_entries = calloc(new_capacity, sizeof(DirectoryEntry)); + if (!new_entries) return false; + + for (size_t i = 0; i < cache->capacity; i++) { + if (cache->entries[i].occupied) { + size_t idx = hash_ref(cache->entries[i].ref_number, new_capacity); + while (new_entries[idx].occupied) { + idx = (idx + 1) % new_capacity; + } + new_entries[idx] = cache->entries[i]; + } + } + + free(cache->entries); + cache->entries = new_entries; + cache->capacity = new_capacity; + return true; +} + +static bool add_directory_to_cache(DirectoryCache* cache, uint64_t ref_number, const char* path) { + if (cache->count * 10 > cache->capacity * 7) { + if (!resize_directory_cache(cache)) return false; + } + + size_t idx = hash_ref(ref_number, cache->capacity); + while (cache->entries[idx].occupied) { + if (cache->entries[idx].ref_number == ref_number) { + strncpy(cache->entries[idx].path, path, MAX_PATH_LENGTH - 1); + cache->entries[idx].path[MAX_PATH_LENGTH - 1] = '\0'; + return true; + } + idx = (idx + 1) % cache->capacity; + } + + cache->entries[idx].ref_number = ref_number; + strncpy(cache->entries[idx].path, path, MAX_PATH_LENGTH - 1); + cache->entries[idx].path[MAX_PATH_LENGTH - 1] = '\0'; + cache->entries[idx].occupied = true; + cache->count++; + return true; +} + +static const char* get_cached_path(DirectoryCache* cache, uint64_t ref_number) { + size_t idx = hash_ref(ref_number, cache->capacity); + while (cache->entries[idx].occupied) { + if (cache->entries[idx].ref_number == ref_number) { + return cache->entries[idx].path; + } + idx = (idx + 1) % cache->capacity; + } + return NULL; +} + +static bool build_path_impl(NTFSContext* ctx, uint64_t ref_number, char* buffer, size_t buffer_size, int depth) { + if (depth > NTFS_MAX_RECURSION_DEPTH) { + return false; + } + + if (ref_number == 5) { + buffer[0] = '\0'; + return true; + } + + const char* cached_path = get_cached_path(&ctx->dir_cache, ref_number); + if (cached_path) { + strncpy(buffer, cached_path, buffer_size - 1); + buffer[buffer_size - 1] = '\0'; + return true; + } + + FileInfo info; + if (!read_file_info(ctx, ref_number, &info) || !info.valid) { + return false; + } + + if (info.name[0] == '$') { + return false; + } + + char parent_path[MAX_PATH_LENGTH]; + if (!build_path_impl(ctx, info.parent_ref, parent_path, sizeof(parent_path), depth + 1)) { + return false; + } + + if (parent_path[0] == '\0') { + strncpy(buffer, info.name, buffer_size - 1); + } + else { + snprintf(buffer, buffer_size, "%s%s%s", parent_path, PATH_SEPARATOR, info.name); + } + buffer[buffer_size - 1] = '\0'; + + if (info.is_directory) { + add_directory_to_cache(&ctx->dir_cache, ref_number, buffer); + } + + return true; +} + +static bool build_path_recursively(NTFSContext* ctx, uint64_t ref_number, char* buffer, size_t buffer_size) { + return build_path_impl(ctx, ref_number, buffer, buffer_size, 0); +} + +static void get_full_path(NTFSContext* ctx, uint64_t parent_ref, const char* name, + char* out_path, size_t out_size) { + char parent_path[MAX_PATH_LENGTH]; + + if (!build_path_recursively(ctx, parent_ref, parent_path, sizeof(parent_path))) { + snprintf(out_path, out_size, "%s%s%s", + ctx->base_path, + PATH_SEPARATOR, + name); + return; + } + + if (parent_path[0] == '\0') { + snprintf(out_path, out_size, "%s%s%s", + ctx->base_path, + PATH_SEPARATOR, + name); + } + else { + snprintf(out_path, out_size, "%s%s%s%s%s", + ctx->base_path, + PATH_SEPARATOR, + parent_path, + PATH_SEPARATOR, + name); + } +} + +static bool extract_data_from_runs(NTFSContext* ctx, const DataRun* runs, int run_count, + uint64_t data_size, FILE* out_file) { + if (!ctx->file_buffer) return false; + + uint64_t total_written = 0; + bool success = true; + + for (int i = 0; i < run_count && total_written < data_size; i++) { + uint64_t cluster_offset = ctx->data_start_offset + + (runs[i].offset * ctx->bytes_per_cluster); + uint64_t length = runs[i].length * ctx->bytes_per_cluster; + + if (length > data_size - total_written) { + length = data_size - total_written; + } + + uint64_t remaining = length; + while (remaining > 0 && success) { + size_t to_read = (remaining > BUFFER_SIZE) ? BUFFER_SIZE : (size_t)remaining; + + if (!ntfs_read(ctx, ctx->file_buffer, cluster_offset, to_read)) { + success = false; + break; + } + + if (FWRITE_DIRECT(out_file, ctx->file_buffer, to_read) != to_read) { + success = false; + break; + } + + cluster_offset += to_read; + remaining -= to_read; + total_written += to_read; + + ctx->extracted_bytes += to_read; + if (ctx->progress) { + progress_update((Progress*)ctx->progress, ctx->extracted_bytes); + } + } + } + + return success; +} + +static bool read_bytes_from_runs(NTFSContext* ctx, const DataRun* runs, int run_count, + uint64_t file_size, uint64_t read_offset, void* buffer, size_t read_size) { + if (read_offset + read_size > file_size) { + return false; + } + + uint64_t current_file_pos = 0; + uint8_t* out = (uint8_t*)buffer; + size_t bytes_remaining = read_size; + + for (int i = 0; i < run_count && bytes_remaining > 0; i++) { + uint64_t run_bytes = runs[i].length * ctx->bytes_per_cluster; + uint64_t run_start = current_file_pos; + uint64_t run_end = run_start + run_bytes; + + if (read_offset < run_end && read_offset + read_size > run_start) { + uint64_t start_in_run = (read_offset > run_start) ? (read_offset - run_start) : 0; + uint64_t end_in_run = ((read_offset + read_size) < run_end) ? + (read_offset + read_size - run_start) : run_bytes; + size_t chunk_size = (size_t)(end_in_run - start_in_run); + + uint64_t disk_offset = ctx->data_start_offset + + (runs[i].offset * ctx->bytes_per_cluster) + start_in_run; + + if (!ntfs_read(ctx, out, disk_offset, chunk_size)) { + return false; + } + + out += chunk_size; + bytes_remaining -= chunk_size; + } + + current_file_pos = run_end; + } + + return bytes_remaining == 0; +} + +static int parse_data_runs(const uint8_t* run_list, size_t run_list_len, DataRun* runs, int max_runs) { + int count = 0; + uint64_t offset_base = 0; + const uint8_t* p = run_list; + const uint8_t* end = run_list + run_list_len; + + while (p < end && *p != 0 && count < max_runs) { + uint8_t header = *p++; + int length_size = header & 0xF; + int offset_size = header >> 4; + + if (length_size == 0) break; + if (p + length_size + offset_size > end) break; + + uint64_t length = 0; + for (int i = 0; i < length_size; i++) { + length |= ((uint64_t)*p++) << (i * 8); + } + + int64_t offset = 0; + if (offset_size > 0) { + for (int i = 0; i < offset_size; i++) { + offset |= ((uint64_t)*p++) << (i * 8); + } + if (offset & ((uint64_t)1 << ((offset_size * 8) - 1))) { + offset |= ~((uint64_t)(1ULL << (offset_size * 8)) - 1); + } + } + + offset_base += offset; + runs[count].offset = offset_base; + runs[count].length = length; + count++; + } + + return count; +} + +static int parse_internal_vhd_number(const char* filename) { + if (strncmp(filename, "internal_", 9) != 0) return -1; + + size_t len = strlen(filename); + if (len < 13 || strcmp(filename + len - 4, ".vhd") != 0) return -1; + + char num_str[8] = {0}; + size_t num_len = len - 13; + if (num_len == 0 || num_len > 7) return -1; + + memcpy(num_str, filename + 9, num_len); + for (size_t i = 0; i < num_len; i++) { + if (num_str[i] < '0' || num_str[i] > '9') return -1; + } + return atoi(num_str); +} + +static bool store_pending_vhd(NTFSContext* ctx, const MFTRecordHeader* record, int vhd_number) { + if (ctx->pending_vhd_count >= MAX_PENDING_VHDS) return false; + + const uint8_t* attr = (const uint8_t*)record + record->attrs_offset; + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + + if (header->type == 0xFFFFFFFF || header->length == 0) break; + + if (header->type == DATA_ATTR && header->name_length == 0 && header->non_resident) { + PendingVHD* pending = &ctx->pending_vhds[ctx->pending_vhd_count]; + const uint8_t* run_list = attr + header->data.non_resident.mapping_pairs_offset; + size_t run_list_len = header->length - header->data.non_resident.mapping_pairs_offset; + pending->run_count = parse_data_runs(run_list, run_list_len, pending->runs, MAX_DATA_RUNS); + pending->file_size = header->data.non_resident.data_size; + pending->vhd_number = vhd_number; + ctx->pending_vhd_count++; + return true; + } + attr += header->length; + } + return false; +} + +static bool store_pending_opt(NTFSContext* ctx, const char* filename, const MFTRecordHeader* record) { + if (ctx->pending_opt_count >= MAX_PENDING_OPTS) return false; + + const uint8_t* attr = (const uint8_t*)record + record->attrs_offset; + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + + if (header->type == 0xFFFFFFFF || header->length == 0) break; + + if (header->type == DATA_ATTR && header->name_length == 0 && header->non_resident) { + PendingOpt* pending = &ctx->pending_opts[ctx->pending_opt_count]; + const uint8_t* run_list = attr + header->data.non_resident.mapping_pairs_offset; + size_t run_list_len = header->length - header->data.non_resident.mapping_pairs_offset; + pending->run_count = parse_data_runs(run_list, run_list_len, pending->runs, MAX_DATA_RUNS); + pending->file_size = header->data.non_resident.data_size; + pending->data_offset = ctx->data_start_offset; + strncpy(pending->filename, filename, MAX_FILENAME_LENGTH - 1); + pending->filename[MAX_FILENAME_LENGTH - 1] = '\0'; + ctx->pending_opt_count++; + return true; + } + attr += header->length; + } + return false; +} + +static bool extract_file(NTFSContext* ctx, const MFTRecordHeader* record, + const char* full_path, const char* filename, uint64_t modification_time, uint64_t access_time) { + + const char* ext = strrchr(filename, '.'); + bool is_opt_file = ext && strcmp(ext, ".opt") == 0; + + if (ctx->stream) { + int vhd_num = parse_internal_vhd_number(filename); + if (vhd_num >= 0) { + return store_pending_vhd(ctx, record, vhd_num); + } + if (is_opt_file) { + return store_pending_opt(ctx, filename, record); + } + } + + uint64_t file_size = 0; + const uint8_t* data_attr = NULL; + const uint8_t* attr = (const uint8_t*)record + record->attrs_offset; + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + if (header->type == 0xFFFFFFFF || header->length == 0) break; + if (header->type == DATA_ATTR && header->name_length == 0) { + data_attr = attr; + if (header->non_resident) { + file_size = header->data.non_resident.data_size; + } else { + file_size = header->data.resident.value_length; + } + break; + } + attr += header->length; + } + if (!data_attr) return true; + + char parent_path[MAX_PATH_LENGTH]; + strncpy(parent_path, full_path, sizeof(parent_path) - 1); + parent_path[sizeof(parent_path) - 1] = '\0'; + + char* last_separator = strrchr(parent_path, PATH_SEPARATOR[0]); + if (last_separator) { + *last_separator = '\0'; + if (strcmp(parent_path, ctx->last_dir) != 0) { + create_directories(parent_path); + strncpy(ctx->last_dir, parent_path, MAX_PATH_LENGTH - 1); + ctx->last_dir[MAX_PATH_LENGTH - 1] = '\0'; + } + } + + FILE* out_file; + if (file_size >= 65536) { + out_file = FOPEN_PREALLOC(full_path, file_size); + } else { + out_file = FOPEN(full_path, "wb"); + } + if (!out_file) { + return false; + } + + bool success = false; + const AttributeHeader* header = (const AttributeHeader*)data_attr; + + if (header->non_resident) { + DataRun runs[256]; + const uint8_t* run_list = data_attr + header->data.non_resident.mapping_pairs_offset; + size_t run_list_len = header->length - header->data.non_resident.mapping_pairs_offset; + int run_count = parse_data_runs(run_list, run_list_len, runs, 256); + + success = extract_data_from_runs(ctx, runs, run_count, file_size, out_file); + } + else { + const uint8_t* data = data_attr + header->data.resident.value_offset; + uint32_t len = header->data.resident.value_length; + success = (FWRITE_DIRECT(out_file, data, len) == len); + if (success) { + ctx->extracted_bytes += len; + if (ctx->progress) { + progress_update((Progress*)ctx->progress, ctx->extracted_bytes); + } + } + } + + if (success && (modification_time != 0 || access_time != 0)) { +#ifdef _WIN32 + set_file_times_handle(out_file, modification_time, access_time); +#endif + } + + fclose(out_file); + if (!success) { + REMOVE(full_path); + } + else { + ctx->files_extracted++; +#ifndef _WIN32 + if (modification_time != 0 || access_time != 0) { + set_file_times(full_path, modification_time, access_time); + } +#endif + } + return success; +} + +static bool process_mft_record(NTFSContext* ctx, const uint8_t* record_data) { + const MFTRecordHeader* record = (const MFTRecordHeader*)record_data; + + if (memcmp(record->magic, "FILE", 4) != 0 || !(record->flags & MFT_RECORD_IN_USE)) { + return true; + } + + uint32_t attrs_offset = record->attrs_offset; + uint32_t bytes_used = record->bytes_used; + if (attrs_offset >= ctx->mft_record_size || bytes_used > ctx->mft_record_size) { + return true; + } + bytes_used = min(bytes_used, ctx->mft_record_size); + + char filename[MAX_FILENAME_LENGTH]; + uint64_t parent_ref = 0; + uint64_t modification_time = 0; + uint64_t access_time = 0; + bool got_filename = false; + bool is_directory = (record->flags & MFT_RECORD_IS_DIRECTORY) != 0; + uint64_t record_num = record->record_number & 0xFFFFFFFFFFFF; + + const uint8_t* record_end = record_data + bytes_used; + const uint8_t* attr = record_data + attrs_offset; + + while (attr + sizeof(AttributeHeader) <= record_end) { + const AttributeHeader* header = (const AttributeHeader*)attr; + + if (header->type == 0xFFFFFFFF || header->length == 0) { + break; + } + if (header->length < sizeof(AttributeHeader) || attr + header->length > record_end) { + break; + } + + if (header->type == FILE_NAME_ATTR && !header->non_resident) { + uint16_t value_offset = header->data.resident.value_offset; + uint32_t value_length = header->data.resident.value_length; + if (value_offset + value_length > header->length) break; + + const FileNameAttribute* fname = (const FileNameAttribute*)(attr + value_offset); + if ((const uint8_t*)fname + 66 > attr + header->length) break; + + if (fname->namespace != 2) { + fs_name_to_utf8(fname->name, fname->name_length, filename, sizeof(filename)); + parent_ref = fname->parent_directory & 0xFFFFFFFFFFFF; + modification_time = fname->modification_time; + access_time = fname->access_time; + got_filename = true; + break; + } + } + + attr += header->length; + } + + if (!got_filename) { + return true; + } + + if (filename[0] == '$') { + return true; + } + + if (!is_safe_path(filename)) { + return true; + } + + + + char full_path[MAX_PATH_LENGTH]; + get_full_path(ctx, parent_ref, filename, full_path, sizeof(full_path)); + + if (is_directory) { + if (!create_directories(full_path)) { + return false; + } + + if (modification_time != 0) { + DeferredDirTime* dirs = (DeferredDirTime*)ctx->deferred_dirs; + if (ctx->deferred_count >= ctx->deferred_capacity) { + uint32_t new_cap = ctx->deferred_capacity ? ctx->deferred_capacity * 2 : 256; + DeferredDirTime* new_buf = realloc(dirs, new_cap * sizeof(DeferredDirTime)); + if (new_buf) { + ctx->deferred_dirs = new_buf; + ctx->deferred_capacity = new_cap; + dirs = new_buf; + } + } + if (ctx->deferred_count < ctx->deferred_capacity) { + DeferredDirTime* d = &dirs[ctx->deferred_count++]; + STRCPY_S(d->path, sizeof(d->path), full_path); + d->mtime = modification_time; + d->atime = access_time; + } + } + + const char* relative_path = full_path + strlen(ctx->base_path); + while (*relative_path == PATH_SEPARATOR[0]) relative_path++; + + if (!add_directory_to_cache(&ctx->dir_cache, record_num, relative_path)) { + return false; + } + return true; + } + + return extract_file(ctx, record, full_path, filename, modification_time, access_time); +} + +static bool vhd_raw_read(VHDContext* ctx, void* buffer, uint64_t offset, size_t size) { + if (ctx->run_source) { + NTFSContext* ntfs = (NTFSContext*)ctx->run_source->ntfs_ctx; + return read_bytes_from_runs(ntfs, ctx->run_source->runs, ctx->run_source->run_count, + ctx->run_source->file_size, offset, buffer, size); + } + if (FSEEKO(ctx->fp, offset, SEEK_SET) != 0) { + return false; + } + return fread(buffer, 1, size, ctx->fp) == size; +} + +static uint64_t vhd_get_size(VHDContext* ctx) { + if (ctx->run_source) { + return ctx->run_source->file_size; + } + int64_t current = FTELLO(ctx->fp); + FSEEKO(ctx->fp, 0, SEEK_END); + int64_t size = FTELLO(ctx->fp); + FSEEKO(ctx->fp, current, SEEK_SET); + return (uint64_t)size; +} + +static bool vhd_read_dynamic_block(VHDContext* ctx, uint8_t* buf, uint64_t offset, size_t size, bool is_differencing) { + uint64_t block_size = ctx->dyn_header.block_size; + + while (size > 0) { + uint32_t block_idx = (uint32_t)(offset / block_size); + uint32_t block_offset = (uint32_t)(offset % block_size); + + if (block_idx >= ctx->dyn_header.max_bat_entries) { + return false; + } + + uint32_t bat_entry = ctx->bat[block_idx]; + size_t chunk = (size < (block_size - block_offset)) ? + size : (size_t)(block_size - block_offset); + + if (bat_entry == VHD_BAT_ENTRY_RESERVED) { + if (is_differencing && ctx->parent) { + if (!vhd_read(ctx->parent, buf, offset, chunk)) { + return false; + } + } else { + memset(buf, 0, chunk); + } + } else { + if (!ctx->block_cached || ctx->cached_block_idx != block_idx) { + uint64_t sector_file_offset = ((uint64_t)bat_entry) * VHD_SECTOR_SIZE; + + if (!vhd_raw_read(ctx, ctx->sector_bitmap, sector_file_offset, ctx->sector_bitmap_size)) { + return false; + } + + if (!vhd_raw_read(ctx, ctx->block_buffer, sector_file_offset + ctx->sector_bitmap_size, block_size)) { + return false; + } + + ctx->cached_block_idx = block_idx; + ctx->block_cached = true; + } + + if (is_differencing && ctx->parent) { + uint32_t start_sector = block_offset / VHD_SECTOR_SIZE; + uint32_t end_offset = block_offset + (uint32_t)chunk; + uint32_t end_sector = (end_offset + VHD_SECTOR_SIZE - 1) / VHD_SECTOR_SIZE; + + for (uint32_t s = start_sector; s < end_sector; s++) { + uint32_t byte_idx = s / 8; + uint32_t bit_idx = 7 - (s % 8); + bool sector_present = (ctx->sector_bitmap[byte_idx] >> bit_idx) & 1; + + uint32_t sector_start_in_block = s * VHD_SECTOR_SIZE; + uint32_t sector_end_in_block = sector_start_in_block + VHD_SECTOR_SIZE; + + uint32_t copy_start = (sector_start_in_block < block_offset) ? block_offset : sector_start_in_block; + uint32_t copy_end = (sector_end_in_block > end_offset) ? end_offset : sector_end_in_block; + + if (copy_start >= copy_end) continue; + + size_t copy_len = copy_end - copy_start; + size_t buf_offset = copy_start - block_offset; + + if (sector_present) { + memcpy(buf + buf_offset, ctx->block_buffer + copy_start, copy_len); + } else { + uint64_t parent_offset = (uint64_t)block_idx * block_size + copy_start; + if (!vhd_read(ctx->parent, buf + buf_offset, parent_offset, copy_len)) { + return false; + } + } + } + } else { + memcpy(buf, ctx->block_buffer + block_offset, chunk); + } + } + + buf += chunk; + offset += chunk; + size -= chunk; + } + return true; +} + +static bool vhd_read(VHDContext* ctx, void* buffer, uint64_t offset, size_t size) { + if (ctx->footer.disk_type == VHD_TYPE_FIXED) { + return vhd_raw_read(ctx, buffer, offset, size); + } + else if (ctx->footer.disk_type == VHD_TYPE_DYNAMIC) { + return vhd_read_dynamic_block(ctx, (uint8_t*)buffer, offset, size, false); + } + else if (ctx->footer.disk_type == VHD_TYPE_DIFFERENCING) { + return vhd_read_dynamic_block(ctx, (uint8_t*)buffer, offset, size, true); + } + return false; +} + +static void extract_base_dir(const char* filepath, char* base_dir, size_t base_dir_size) { + strncpy(base_dir, filepath, base_dir_size - 1); + base_dir[base_dir_size - 1] = '\0'; + + char* last_sep = NULL; + for (char* p = base_dir; *p; p++) { + if (*p == '/' || *p == '\\') { + last_sep = p; + } + } + if (last_sep) { + *(last_sep + 1) = '\0'; + } else { + base_dir[0] = '\0'; + } +} + +static const char* extract_filename(const char* path) { + const char* last_slash = strrchr(path, '/'); + const char* last_backslash = strrchr(path, '\\'); + const char* filename = path; + if (last_slash && last_slash > filename) filename = last_slash + 1; + if (last_backslash && last_backslash > filename - 1) filename = last_backslash + 1; + return filename; +} + +static bool try_parent_path(const char* base_dir, const char* filename, char* parent_path, size_t path_size) { + snprintf(parent_path, path_size, "%s%s", base_dir, filename); + FILE* test = FOPEN(parent_path, "rb"); + if (test) { + fclose(test); + return true; + } + return false; +} + +static bool resolve_parent_path(VHDContext* ctx, char* parent_path, size_t path_size) { + char parent_filename[MAX_FILENAME_LENGTH] = "internal_0.vhd"; + + for (int i = 0; i < 8; i++) { + VHDParentLocator* loc = &ctx->dyn_header.parent_loc[i]; + uint32_t code = swap32(loc->platform_code); + uint32_t data_len = swap32(loc->platform_data_length); + uint64_t data_offset = swap64(loc->platform_data_offset); + + if (code == 0 || data_len == 0 || data_len > 2048) continue; + + uint8_t* path_data = malloc(data_len + 2); + if (!path_data) continue; + + if (!vhd_raw_read(ctx, path_data, data_offset, data_len)) { + free(path_data); + continue; + } + path_data[data_len] = 0; + path_data[data_len + 1] = 0; + + char locator_path[MAX_PATH_LENGTH]; + size_t utf16_len = data_len / 2; + utf16_to_utf8((const uint16_t*)path_data, (int)utf16_len, locator_path, sizeof(locator_path)); + free(path_data); + + const char* filename = extract_filename(locator_path); + if (filename[0] != '\0') { + strncpy(parent_filename, filename, sizeof(parent_filename) - 1); + break; + } + } + + if (try_parent_path(ctx->base_dir, parent_filename, parent_path, path_size)) { + return true; + } + + parent_path[0] = '\0'; + return false; +} + +static void vhd_close(VHDContext* ctx) { + if (!ctx) return; + + if (ctx->parent) { + vhd_close(ctx->parent); + free(ctx->parent); + ctx->parent = NULL; + } + + if (ctx->fp) { + fclose(ctx->fp); + ctx->fp = NULL; + } + free(ctx->run_source); + ctx->run_source = NULL; + free(ctx->bat); + free(ctx->sector_bitmap); + free(ctx->block_buffer); + ctx->bat = NULL; + ctx->sector_bitmap = NULL; + ctx->block_buffer = NULL; +} + +static bool vhd_init_dynamic_header(VHDContext* ctx) { + if (!vhd_raw_read(ctx, &ctx->dyn_header, ctx->footer.data_offset, sizeof(VHDDynamicHeader))) { + return false; + } + + if (memcmp(ctx->dyn_header.cookie, VHD_DYNAMIC_COOKIE, strlen(VHD_DYNAMIC_COOKIE)) != 0) { + return false; + } + + ctx->dyn_header.data_offset = swap64(ctx->dyn_header.data_offset); + ctx->dyn_header.bat_offset = swap64(ctx->dyn_header.bat_offset); + ctx->dyn_header.head_vers = swap32(ctx->dyn_header.head_vers); + ctx->dyn_header.max_bat_entries = swap32(ctx->dyn_header.max_bat_entries); + ctx->dyn_header.block_size = swap32(ctx->dyn_header.block_size); + ctx->dyn_header.parent_timestamp = swap32(ctx->dyn_header.parent_timestamp); + + size_t bat_size = (size_t)ctx->dyn_header.max_bat_entries * sizeof(uint32_t); + + if (bat_size == 0 || bat_size > (1ULL << 30)) { + return false; + } + + ctx->bat = malloc(bat_size); + if (!ctx->bat) { + return false; + } + + if (!vhd_raw_read(ctx, ctx->bat, ctx->dyn_header.bat_offset, bat_size)) { + return false; + } + + for (uint32_t i = 0; i < ctx->dyn_header.max_bat_entries; i++) { + ctx->bat[i] = swap32(ctx->bat[i]); + } + + ctx->sector_bitmap_size = (ctx->dyn_header.block_size / VHD_SECTOR_SIZE + 7) / 8; + ctx->sector_bitmap = malloc(ctx->sector_bitmap_size); + ctx->block_buffer = malloc(ctx->dyn_header.block_size); + + if (!ctx->sector_bitmap || !ctx->block_buffer) { + return false; + } + + return true; +} + +static bool vhd_read_footer(VHDContext* ctx) { + uint64_t file_size = vhd_get_size(ctx); + if (file_size < VHD_FOOTER_SIZE) { + return false; + } + + if (!vhd_raw_read(ctx, &ctx->footer, file_size - VHD_FOOTER_SIZE, sizeof(VHDFooter))) { + return false; + } + + if (memcmp(ctx->footer.cookie, VHD_COOKIE, strlen(VHD_COOKIE)) != 0) { + return false; + } + + ctx->footer.features = swap32(ctx->footer.features); + ctx->footer.version = swap32(ctx->footer.version); + ctx->footer.data_offset = swap64(ctx->footer.data_offset); + ctx->footer.timestamp = swap32(ctx->footer.timestamp); + ctx->footer.creator_app = swap32(ctx->footer.creator_app); + ctx->footer.creator_ver = swap32(ctx->footer.creator_ver); + ctx->footer.creator_os = swap32(ctx->footer.creator_os); + ctx->footer.original_size = swap64(ctx->footer.original_size); + ctx->footer.current_size = swap64(ctx->footer.current_size); + ctx->footer.cylinder = ((ctx->footer.cylinder >> 8) | (ctx->footer.cylinder << 8)); + ctx->footer.disk_type = swap32(ctx->footer.disk_type); + ctx->footer.checksum = swap32(ctx->footer.checksum); + return true; +} + +static bool vhd_init_common(VHDContext* ctx, const char* base_dir, uint32_t depth) { + if (!vhd_read_footer(ctx)) { + return false; + } + + if (ctx->footer.disk_type == VHD_TYPE_DYNAMIC) { + if (!vhd_init_dynamic_header(ctx)) { + return false; + } + } + else if (ctx->footer.disk_type == VHD_TYPE_DIFFERENCING) { + if (!vhd_init_dynamic_header(ctx)) { + return false; + } + + char parent_path[MAX_PATH_LENGTH]; + if (resolve_parent_path(ctx, parent_path, sizeof(parent_path))) { + ctx->parent = malloc(sizeof(VHDContext)); + if (ctx->parent) { + if (!vhd_init_internal(ctx->parent, parent_path, depth + 1)) { + free(ctx->parent); + ctx->parent = NULL; + return false; + } + } + } else { + return false; + } + } + return true; +} + +static bool vhd_init_internal(VHDContext* ctx, const char* filename, uint32_t depth) { + memset(ctx, 0, sizeof(VHDContext)); + ctx->depth = depth; + + if (depth > VHD_MAX_CHAIN_DEPTH) { + return false; + } + + extract_base_dir(filename, ctx->base_dir, sizeof(ctx->base_dir)); + + ctx->fp = FOPEN(filename, "rb"); + if (!ctx->fp) { + return false; + } + + if (!vhd_init_common(ctx, ctx->base_dir, depth)) { + vhd_close(ctx); + return false; + } + + return true; +} + +static bool vhd_init(VHDContext* ctx, const char* filename) { + return vhd_init_internal(ctx, filename, 0); +} + +static bool vhd_init_from_runs(VHDContext* ctx, NTFSContext* ntfs, const DataRun* runs, + int run_count, uint64_t file_size, const char* base_dir) { + memset(ctx, 0, sizeof(VHDContext)); + + ctx->run_source = malloc(sizeof(VHDRunSource)); + if (!ctx->run_source) { + return false; + } + + ctx->run_source->ntfs_ctx = ntfs; + ctx->run_source->run_count = (run_count < MAX_DATA_RUNS) ? run_count : MAX_DATA_RUNS; + ctx->run_source->file_size = file_size; + ctx->run_source->data_start_offset = ntfs->data_start_offset; + ctx->run_source->bytes_per_cluster = ntfs->bytes_per_cluster; + memcpy(ctx->run_source->runs, runs, ctx->run_source->run_count * sizeof(DataRun)); + strncpy(ctx->base_dir, base_dir, sizeof(ctx->base_dir) - 1); + + if (!vhd_init_common(ctx, base_dir, 0)) { + vhd_close(ctx); + return false; + } + return true; +} + +static bool ntfs_setup_mft(NTFSContext* ctx, uint64_t ntfs_offset) { + ctx->data_start_offset = ntfs_offset; + + if (!ntfs_read(ctx, &ctx->boot, ntfs_offset, sizeof(NTFSBootSector))) { + return false; + } + + ctx->bytes_per_sector = ctx->boot.bytes_per_sector; + ctx->bytes_per_cluster = (uint32_t)ctx->boot.bytes_per_sector * ctx->boot.sectors_per_cluster; + ctx->mft_offset = ntfs_offset + (ctx->boot.mft_cluster_number * ctx->bytes_per_cluster); + + if (ctx->boot.clusters_per_mft_record > 0) { + ctx->mft_record_size = ctx->boot.clusters_per_mft_record * ctx->bytes_per_cluster; + } else { + ctx->mft_record_size = 1U << (-ctx->boot.clusters_per_mft_record); + } + + ctx->file_buffer = malloc(BUFFER_SIZE); + if (!ctx->file_buffer) return false; + ctx->last_dir[0] = '\0'; + + uint8_t* mft_record = malloc(ctx->mft_record_size); + if (!mft_record) return false; + + if (!ntfs_read(ctx, mft_record, ctx->mft_offset, ctx->mft_record_size) || + !apply_mft_fixups(ctx, mft_record, ctx->mft_record_size)) { + free(mft_record); + return false; + } + + const MFTRecordHeader* record = (const MFTRecordHeader*)mft_record; + if (memcmp(record->magic, "FILE", 4) != 0) { + free(mft_record); + return false; + } + + const uint8_t* attr = mft_record + record->attrs_offset; + while (attr < mft_record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + if (header->type == 0xFFFFFFFF || header->length == 0) break; + if (header->type == DATA_ATTR && header->name_length == 0) { + if (header->non_resident) { + ctx->mft_data_size = header->data.non_resident.data_size; + ctx->total_mft_records = ctx->mft_data_size / ctx->mft_record_size; + } + break; + } + attr += header->length; + } + + free(mft_record); + return true; +} + +bool ntfs_init(NTFSContext* ctx, const char* path, const char* extract_path) { + bool silent = ctx->silent; + bool verbose = ctx->verbose; + + memset(ctx, 0, sizeof(NTFSContext)); + + ctx->silent = silent; + ctx->verbose = verbose; + strncpy(ctx->base_path, extract_path, sizeof(ctx->base_path) - 1); + + if (!init_directory_cache(&ctx->dir_cache)) { + return false; + } + + FILE* fp = FOPEN(path, "rb"); + if (!fp) { + free_directory_cache(&ctx->dir_cache); + return false; + } + + if (FSEEKO(fp, -512, SEEK_END) == 0) { + char signature[9] = { 0 }; + if (fread(signature, 1, 8, fp) == 8 && memcmp(signature, VHD_COOKIE, 8) == 0) { + fclose(fp); + ctx->is_vhd = true; + if (!vhd_init(&ctx->vhd, path)) { + free_directory_cache(&ctx->dir_cache); + return false; + } + } + else { + rewind(fp); + ctx->is_vhd = false; + ctx->raw.fp = fp; + } + } + + uint64_t ntfs_offset = 0; + bool found_ntfs = false; + + if (ctx->is_vhd) { + uint8_t sector[VHD_SECTOR_SIZE]; + if (ntfs_read(ctx, sector, 0, VHD_SECTOR_SIZE)) { + if (sector[0x1FE] == 0x55 && sector[0x1FF] == 0xAA) { + for (int i = 0; i < 4; i++) { + const uint8_t* part = sector + 0x1BE + (i * 16); + if (part[4] == NTFS_PARTITION_TYPE) { + uint32_t start_sector = + (uint32_t)part[8] | + ((uint32_t)part[9] << 8) | + ((uint32_t)part[10] << 16) | + ((uint32_t)part[11] << 24); + ntfs_offset = (uint64_t)start_sector * VHD_SECTOR_SIZE; + + if (ntfs_read(ctx, sector, ntfs_offset, VHD_SECTOR_SIZE) && + memcmp(sector + 3, NTFS_SIGNATURE, 8) == 0) { + found_ntfs = true; + break; + } + } + } + } + } + + if (!found_ntfs) { + const uint64_t offsets[] = { 0, 0x100000, 0x200000, 0x400000, 0x800000, 0 }; + for (int i = 0; offsets[i]; i++) { + if (ntfs_read(ctx, sector, offsets[i], VHD_SECTOR_SIZE) && + memcmp(sector + 3, NTFS_SIGNATURE, 8) == 0) { + ntfs_offset = offsets[i]; + found_ntfs = true; + break; + } + } + } + } + else { + uint8_t boot[512]; + if (ntfs_read(ctx, boot, 0, sizeof(boot)) && + boot[0] == 0xEB && boot[1] == 0x52 && boot[2] == 0x90 && + memcmp(boot + 3, NTFS_SIGNATURE, 8) == 0) { + found_ntfs = true; + } + } + + if (!found_ntfs) { + ntfs_close(ctx); + return false; + } + + if (!ntfs_setup_mft(ctx, ntfs_offset)) { + ntfs_close(ctx); + return false; + } + return true; +} + +bool ntfs_init_stream(NTFSContext* ctx, DecryptStream* stream, const char* extract_path) { + bool saved_verbose = ctx->verbose; + bool saved_silent = ctx->silent; + memset(ctx, 0, sizeof(NTFSContext)); + ctx->verbose = saved_verbose; + ctx->silent = saved_silent; + strncpy(ctx->base_path, extract_path, sizeof(ctx->base_path) - 1); + ctx->stream = stream; + + if (!init_directory_cache(&ctx->dir_cache)) { + return false; + } + + uint64_t ntfs_offset = 0; + bool found_ntfs = false; + + uint8_t boot[512]; + if (ntfs_read(ctx, boot, 0, sizeof(boot)) && + boot[0] == 0xEB && boot[1] == 0x52 && boot[2] == 0x90 && + memcmp(boot + 3, NTFS_SIGNATURE, 8) == 0) { + found_ntfs = true; + } + + if (!found_ntfs) { + free_directory_cache(&ctx->dir_cache); + return false; + } + + if (!ntfs_setup_mft(ctx, ntfs_offset)) { + free_directory_cache(&ctx->dir_cache); + return false; + } + return true; +} + +static uint64_t get_file_data_size(NTFSContext* ctx, const uint8_t* record_data) { + const MFTRecordHeader* record = (const MFTRecordHeader*)record_data; + + if (memcmp(record->magic, "FILE", 4) != 0 || !(record->flags & MFT_RECORD_IN_USE)) { + return 0; + } + + if (record->flags & MFT_RECORD_IS_DIRECTORY) { + return 0; + } + + char filename[MAX_FILENAME_LENGTH]; + bool got_filename = false; + + const uint8_t* attr = (const uint8_t*)record + record->attrs_offset; + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + if (header->type == 0xFFFFFFFF || header->length == 0) break; + + if (header->type == FILE_NAME_ATTR && !header->non_resident) { + const FileNameAttribute* fname = + (const FileNameAttribute*)(attr + header->data.resident.value_offset); + if (fname->namespace != 2) { + fs_name_to_utf8(fname->name, fname->name_length, filename, sizeof(filename)); + got_filename = true; + break; + } + } + attr += header->length; + } + + if (!got_filename || filename[0] == '$' || !is_safe_path(filename)) { + return 0; + } + + attr = (const uint8_t*)record + record->attrs_offset; + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + if (header->type == 0xFFFFFFFF || header->length == 0) break; + + if (header->type == DATA_ATTR && header->name_length == 0) { + if (header->non_resident) { + return header->data.non_resident.data_size; + } else { + return header->data.resident.value_length; + } + } + attr += header->length; + } + + return 0; +} + +static void count_total_bytes(NTFSContext* ctx) { + ctx->total_bytes = 0; + + uint8_t* record_buffer = malloc(ctx->mft_record_size); + if (!record_buffer) return; + + uint64_t current_offset = ctx->mft_offset; + uint64_t total_records = ctx->total_mft_records; + + ctx->raw_file_pos = 0; + + for (uint64_t i = 0; i < total_records; i++) { + if (!ntfs_read(ctx, record_buffer, current_offset, ctx->mft_record_size)) { + break; + } + + if (!apply_mft_fixups(ctx, record_buffer, ctx->mft_record_size)) { + current_offset += ctx->mft_record_size; + continue; + } + + ctx->total_bytes += get_file_data_size(ctx, record_buffer); + current_offset += ctx->mft_record_size; + } + + free(record_buffer); +} + +int ntfs_detect_vhd_type(NTFSContext* ctx) { + uint8_t* record_buffer = malloc(ctx->mft_record_size); + if (!record_buffer) return -1; + + uint64_t current_offset = ctx->mft_offset; + uint64_t total_records = ctx->total_mft_records; + int detected_type = -1; + + ctx->raw_file_pos = 0; + + for (uint64_t i = 0; i < total_records && detected_type == -1; i++) { + if (!ntfs_read(ctx, record_buffer, current_offset, ctx->mft_record_size)) { + current_offset += ctx->mft_record_size; + continue; + } + + if (!apply_mft_fixups(ctx, record_buffer, ctx->mft_record_size)) { + current_offset += ctx->mft_record_size; + continue; + } + + const MFTRecordHeader* record = (const MFTRecordHeader*)record_buffer; + if (memcmp(record->magic, "FILE", 4) != 0 || !(record->flags & MFT_RECORD_IN_USE)) { + current_offset += ctx->mft_record_size; + continue; + } + + if (record->flags & MFT_RECORD_IS_DIRECTORY) { + current_offset += ctx->mft_record_size; + continue; + } + + char filename[MAX_FILENAME_LENGTH] = {0}; + const uint8_t* attr = (const uint8_t*)record + record->attrs_offset; + + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + if (header->type == 0xFFFFFFFF || header->length == 0) break; + + if (header->type == FILE_NAME_ATTR && !header->non_resident) { + const FileNameAttribute* fname = + (const FileNameAttribute*)(attr + header->data.resident.value_offset); + if (fname->namespace != 2) { + fs_name_to_utf8(fname->name, fname->name_length, filename, sizeof(filename)); + break; + } + } + attr += header->length; + } + + if (strncmp(filename, "internal_", 9) == 0 && + strstr(filename, ".vhd") != NULL) { + + attr = (const uint8_t*)record + record->attrs_offset; + while (attr < (const uint8_t*)record + record->bytes_used) { + const AttributeHeader* header = (const AttributeHeader*)attr; + if (header->type == 0xFFFFFFFF || header->length == 0) break; + + if (header->type == DATA_ATTR && header->name_length == 0 && header->non_resident) { + uint64_t file_size = header->data.non_resident.data_size; + + if (file_size >= 512) { + DataRun runs[256]; + const uint8_t* run_list = attr + header->data.non_resident.mapping_pairs_offset; + size_t run_list_len = header->length - header->data.non_resident.mapping_pairs_offset; + int run_count = parse_data_runs(run_list, run_list_len, runs, 256); + + uint8_t footer[512]; + if (read_bytes_from_runs(ctx, runs, run_count, file_size, + file_size - 512, footer, 512)) { + if (memcmp(footer, "conectix", 8) == 0) { + detected_type = (footer[60] << 24) | (footer[61] << 16) | + (footer[62] << 8) | footer[63]; + } + } + } + break; + } + attr += header->length; + } + } + + current_offset += ctx->mft_record_size; + } + + free(record_buffer); + return detected_type; +} + +bool ntfs_extract_all(NTFSContext* ctx) { + if (!create_directories(ctx->base_path)) return false; + + uint8_t* record_buffer = malloc(ctx->mft_record_size); + if (!record_buffer) return false; + + if (!ctx->is_vhd) { + count_total_bytes(ctx); + } + ctx->extracted_bytes = 0; + + if (ctx->verbose && !ctx->silent && ctx->total_bytes > 0) { + printf("%llu B\n", (unsigned long long)ctx->total_bytes); + } + + Progress progress; + if (!ctx->silent && ctx->total_bytes > 0) { + progress_init(&progress, ctx->total_bytes); + ctx->progress = &progress; + } + + uint64_t current_offset = ctx->mft_offset; + uint64_t total_records = ctx->total_mft_records; + + ctx->raw_file_pos = 0; + + time_t start_time = time(NULL); + time_t last_update = start_time; + uint64_t last_bytes = 0; + + for (uint64_t i = 0; i < total_records; i++) { + if (!ctx->silent && !ctx->progress && ctx->is_vhd) { + time_t now = time(NULL); + if (now != last_update) { + int64_t elapsed = (int64_t)difftime(now, last_update); + uint64_t speed_mb = (elapsed > 0) ? (ctx->extracted_bytes - last_bytes) / (uint64_t)elapsed / (1024 * 1024) : 0; + uint64_t total_mb_w = ctx->extracted_bytes / (1024 * 1024); + uint64_t total_mb_f = (ctx->extracted_bytes % (1024 * 1024)) * 100 / (1024 * 1024); + printf("\r%llu.%02llu MB %llu f %llu MB/s ", + (unsigned long long)total_mb_w, (unsigned long long)total_mb_f, + (unsigned long long)ctx->files_extracted, (unsigned long long)speed_mb); + fflush(stdout); + last_update = now; + last_bytes = ctx->extracted_bytes; + } + } + + if (!ntfs_read(ctx, record_buffer, current_offset, ctx->mft_record_size)) break; + + if (!apply_mft_fixups(ctx, record_buffer, ctx->mft_record_size)) break; + + const MFTRecordHeader* record = (const MFTRecordHeader*)record_buffer; + if (memcmp(record->magic, "FILE", 4) == 0) { + process_mft_record(ctx, record_buffer); + } + + current_offset += ctx->mft_record_size; + } + + if (!ctx->silent && !ctx->progress && ctx->is_vhd) { + uint64_t total_mb_w = ctx->extracted_bytes / (1024 * 1024); + uint64_t total_mb_f = (ctx->extracted_bytes % (1024 * 1024)) * 100 / (1024 * 1024); + printf("\r%llu.%02llu MB %llu f\n", + (unsigned long long)total_mb_w, (unsigned long long)total_mb_f, + (unsigned long long)ctx->files_extracted); + } + + if (ctx->progress) { + progress_finish(&progress); + ctx->progress = NULL; + } + + if (ctx->pending_vhd_count == 0 && ctx->deferred_dirs) { + DeferredDirTime* dirs = (DeferredDirTime*)ctx->deferred_dirs; + for (uint32_t i = ctx->deferred_count; i > 0; i--) { + set_dir_times(dirs[i - 1].path, dirs[i - 1].mtime, dirs[i - 1].atime); + } + } + free(ctx->deferred_dirs); + ctx->deferred_dirs = NULL; + ctx->deferred_count = 0; + ctx->deferred_capacity = 0; + + free(record_buffer); + return true; +} + +bool ntfs_extract_pending_vhds(NTFSContext* ctx, bool silent, bool verbose, bool* is_orphan) { + if (is_orphan) *is_orphan = false; + if (ctx->pending_vhd_count == 0) return true; + + int highest_vhd = -1; + PendingVHD* highest = NULL; + for (int i = 0; i < ctx->pending_vhd_count; i++) { + if (ctx->pending_vhds[i].vhd_number > highest_vhd) { + highest_vhd = ctx->pending_vhds[i].vhd_number; + highest = &ctx->pending_vhds[i]; + } + } + + if (!highest) return true; + + VHDContext vhd_ctx = {0}; + if (!vhd_init_from_runs(&vhd_ctx, ctx, highest->runs, highest->run_count, + highest->file_size, ctx->base_path)) { + if (is_orphan) *is_orphan = true; + return true; + } + + if (vhd_ctx.footer.disk_type == VHD_TYPE_DIFFERENCING) { + if (is_orphan) *is_orphan = true; + vhd_close(&vhd_ctx); + return true; + } + + NTFSContext inner_ctx = {0}; + inner_ctx.silent = silent; + inner_ctx.verbose = verbose; + inner_ctx.is_vhd = true; + inner_ctx.vhd = vhd_ctx; + strncpy(inner_ctx.base_path, ctx->base_path, sizeof(inner_ctx.base_path) - 1); + + if (!init_directory_cache(&inner_ctx.dir_cache)) { + vhd_close(&inner_ctx.vhd); + return false; + } + + uint8_t boot[512]; + bool read_ok = vhd_read(&inner_ctx.vhd, boot, 0, sizeof(boot)); + + uint64_t ntfs_offset = 0; + bool found_ntfs = false; + + if (read_ok && boot[0] == 0xEB && boot[1] == 0x52 && boot[2] == 0x90 && + memcmp(boot + 3, NTFS_SIGNATURE, 8) == 0) { + found_ntfs = true; + } + + if (!found_ntfs && read_ok && boot[510] == 0x55 && boot[511] == 0xAA) { + for (int i = 0; i < 4; i++) { + uint8_t* entry = boot + 446 + (i * 16); + uint8_t ptype = entry[4]; + uint32_t lba_start = entry[8] | (entry[9] << 8) | (entry[10] << 16) | (entry[11] << 24); + + if (ptype == NTFS_PARTITION_TYPE && lba_start > 0) { + ntfs_offset = (uint64_t)lba_start * 512; + if (vhd_read(&inner_ctx.vhd, boot, ntfs_offset, sizeof(boot)) && + boot[0] == 0xEB && boot[1] == 0x52 && boot[2] == 0x90 && + memcmp(boot + 3, NTFS_SIGNATURE, 8) == 0) { + found_ntfs = true; + break; + } + } + } + } + + if (!found_ntfs) { + free_directory_cache(&inner_ctx.dir_cache); + vhd_close(&inner_ctx.vhd); + return false; + } + + if (!ntfs_setup_mft(&inner_ctx, ntfs_offset)) { + free_directory_cache(&inner_ctx.dir_cache); + vhd_close(&inner_ctx.vhd); + return false; + } + + if (verbose && !silent) { + printf(" MFT=%llu clust=%u\n", + (unsigned long long)inner_ctx.total_mft_records, inner_ctx.bytes_per_cluster); + } + + bool success = ntfs_extract_all(&inner_ctx); + + ctx->files_extracted += inner_ctx.files_extracted; + ctx->extracted_bytes += inner_ctx.extracted_bytes; + + ntfs_close(&inner_ctx); + + DeferredDirTime* dirs = (DeferredDirTime*)ctx->deferred_dirs; + for (uint32_t i = ctx->deferred_count; i > 0; i--) { + DeferredDirTime* d = &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 success; +} + +int ntfs_get_pending_opt_count(NTFSContext* ctx) { + return ctx ? ctx->pending_opt_count : 0; +} + +const PendingOpt* ntfs_get_pending_opt(NTFSContext* ctx, int index) { + if (!ctx || index < 0 || index >= ctx->pending_opt_count) return NULL; + return &ctx->pending_opts[index]; +} + +void ntfs_close(NTFSContext* ctx) { + if (ctx->stream) { + } + else if (ctx->is_vhd) { + vhd_close(&ctx->vhd); + } + else { + if (ctx->raw.fp) { + fclose(ctx->raw.fp); + } + } + free(ctx->file_buffer); + free_directory_cache(&ctx->dir_cache); + free(ctx->lookup_buffer); + memset(ctx, 0, sizeof(NTFSContext)); +} diff --git a/src/stream.c b/src/stream.c new file mode 100644 index 0000000..9fd638a --- /dev/null +++ b/src/stream.c @@ -0,0 +1,158 @@ +#include "stream.h" +#include "crypto.h" +#include "common.h" +#include "ntfs.h" + +static bool read_from_runs_internal(void* ntfs_ctx, const DataRun* runs, int run_count, + uint64_t file_size, uint64_t read_offset, void* buffer, size_t read_size); + +bool stream_init(DecryptStream* ds, FILE* fp, uint64_t data_offset, + uint64_t data_size, const uint8_t key[16], const uint8_t iv[16]) { + if (!ds || !fp) return false; + + memset(ds, 0, sizeof(DecryptStream)); + ds->fp = fp; + ds->parent_stream = NULL; + ds->run_source = NULL; + ds->data_offset = data_offset; + ds->data_size = data_size; + memcpy(ds->key, key, 16); + memcpy(ds->file_iv, iv, 16); + ds->cached_page_offset = (uint64_t)-1; + ds->file_pos = (uint64_t)-1; + + AES_init_ctx_iv(&ds->aes_ctx, key, iv); + + return true; +} + +bool stream_init_from_runs(DecryptStream* ds, RunSource* source, + const uint8_t key[16], const uint8_t iv[16]) { + if (!ds || !source) return false; + + memset(ds, 0, sizeof(DecryptStream)); + ds->fp = NULL; + ds->parent_stream = NULL; + ds->run_source = source; + ds->data_offset = 0; + ds->data_size = source->file_size; + memcpy(ds->key, key, 16); + memcpy(ds->file_iv, iv, 16); + ds->cached_page_offset = (uint64_t)-1; + ds->file_pos = (uint64_t)-1; + + AES_init_ctx_iv(&ds->aes_ctx, key, iv); + + return true; +} + +static bool read_from_runs_internal(void* ntfs_ctx, const DataRun* runs, int run_count, + uint64_t file_size, uint64_t read_offset, void* buffer, size_t read_size) { + if (read_offset + read_size > file_size) { + return false; + } + + NTFSContext* ctx = (NTFSContext*)ntfs_ctx; + uint64_t current_file_pos = 0; + uint8_t* out = (uint8_t*)buffer; + size_t bytes_remaining = read_size; + + for (int i = 0; i < run_count && bytes_remaining > 0; i++) { + uint64_t run_bytes = runs[i].length * ctx->bytes_per_cluster; + uint64_t run_start = current_file_pos; + uint64_t run_end = run_start + run_bytes; + + if (read_offset < run_end && read_offset + read_size > run_start) { + uint64_t start_in_run = (read_offset > run_start) ? (read_offset - run_start) : 0; + uint64_t end_in_run = ((read_offset + read_size) < run_end) ? + (read_offset + read_size - run_start) : run_bytes; + size_t chunk_size = (size_t)(end_in_run - start_in_run); + + uint64_t disk_offset = ctx->data_start_offset + + (runs[i].offset * ctx->bytes_per_cluster) + start_in_run; + + if (!stream_read(ctx->stream, out, disk_offset, chunk_size)) { + return false; + } + + out += chunk_size; + bytes_remaining -= chunk_size; + } + + current_file_pos = run_end; + } + + return bytes_remaining == 0; +} + +bool stream_read_raw(void* ntfs_ctx, const DataRun* runs, int run_count, + uint64_t file_size, uint64_t offset, void* buffer, size_t size) { + return read_from_runs_internal(ntfs_ctx, runs, run_count, file_size, offset, buffer, size); +} + +bool stream_read(DecryptStream* ds, void* buffer, uint64_t offset, size_t size) { + if (!ds || !buffer || size == 0) return false; + if (offset > ds->data_size) return false; + if ((uint64_t)size > ds->data_size - offset) return false; + + uint8_t* out = (uint8_t*)buffer; + size_t bytes_read = 0; + + while (bytes_read < size) { + uint64_t current_offset = offset + bytes_read; + uint64_t page_offset = (current_offset / DECRYPT_PAGE_SIZE) * DECRYPT_PAGE_SIZE; + size_t offset_in_page = (size_t)(current_offset % DECRYPT_PAGE_SIZE); + + if (ds->cached_page_offset != page_offset) { + size_t page_bytes_available = (size_t)(ds->data_size - page_offset); + size_t read_size = (page_bytes_available > DECRYPT_PAGE_SIZE) + ? DECRYPT_PAGE_SIZE : page_bytes_available; + + if (ds->parent_stream) { + if (!stream_read(ds->parent_stream, ds->page_buffer, page_offset, read_size)) { + return false; + } + } else if (ds->run_source) { + uint64_t run_offset = ds->data_offset + page_offset; + if (!read_from_runs_internal(ds->run_source->ntfs_ctx, ds->run_source->runs, + ds->run_source->run_count, ds->run_source->file_size, + run_offset, ds->page_buffer, read_size)) { + return false; + } + } else { + uint64_t file_pos_wanted = ds->data_offset + page_offset; + if (ds->file_pos != file_pos_wanted) { + if (FSEEKO(ds->fp, file_pos_wanted, SEEK_SET) != 0) return false; + } + if (fread(ds->page_buffer, 1, read_size, ds->fp) != read_size) return false; + ds->file_pos = file_pos_wanted + read_size; + } + + if (read_size < DECRYPT_PAGE_SIZE) { + memset(ds->page_buffer + read_size, 0, DECRYPT_PAGE_SIZE - read_size); + } + + uint8_t page_iv[16]; + iv_page(page_offset, ds->file_iv, page_iv); + + AES_ctx_set_iv(&ds->aes_ctx, page_iv); + AES_CBC_decrypt_buffer(&ds->aes_ctx, ds->page_buffer, DECRYPT_PAGE_SIZE); + + ds->cached_page_offset = page_offset; + } + + size_t bytes_in_page = DECRYPT_PAGE_SIZE - offset_in_page; + size_t bytes_remaining = size - bytes_read; + size_t copy_size = (bytes_in_page < bytes_remaining) ? bytes_in_page : bytes_remaining; + + uint64_t max_valid = ds->data_size - current_offset; + if (copy_size > max_valid) copy_size = (size_t)max_valid; + + memcpy(out + bytes_read, ds->page_buffer + offset_in_page, copy_size); + bytes_read += copy_size; + + if (copy_size == 0) break; + } + + return bytes_read == size; +}