Zero-copy FTP/HTTP Daemon compatible with all POSIX systems
| 1 | /* |
| 2 | * Feature-test macros must appear before any system include. |
| 3 | * _FILE_OFFSET_BITS=64: enables fseeko/ftello on 32-bit hosts. |
| 4 | * _POSIX_C_SOURCE=200809L: exposes fseeko, fdopen, strcasecmp etc. in strict C99 mode. |
| 5 | */ |
| 6 | #ifndef _FILE_OFFSET_BITS |
| 7 | # define _FILE_OFFSET_BITS 64 |
| 8 | #endif |
| 9 | #ifndef _POSIX_C_SOURCE |
| 10 | # define _POSIX_C_SOURCE 200809L |
| 11 | #endif |
| 12 | |
| 13 | /* |
| 14 | * exfat_unpacker — exFAT filesystem image parser |
| 15 | * Copyright (C) 2025 seregonwar |
| 16 | * |
| 17 | * This program is free software: you can redistribute it and/or modify |
| 18 | * it under the terms of the GNU General Public License as published by |
| 19 | * the Free Software Foundation, either version 3 of the License, or |
| 20 | * (at your option) any later version. |
| 21 | * |
| 22 | * This program is distributed in the hope that it will be useful, |
| 23 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 24 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 25 | * GNU General Public License for more details. |
| 26 | * |
| 27 | * You should have received a copy of the GNU General Public License |
| 28 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
| 29 | * |
| 30 | * exfat_unpacker.c — read-only exFAT filesystem image parser |
| 31 | * |
| 32 | * Fixes applied over the original version: |
| 33 | * 1. Boot sector: volume_flags uint32→uint16; bytes_per_sector_shift now |
| 34 | * correctly at 0x6C instead of 0x6E. |
| 35 | * 2. File entry struct trimmed to exactly 32 bytes; phantom fields removed. |
| 36 | * 3. Filename entry: name_length/name_hash moved to stream extension where |
| 37 | * they actually live; out-of-bounds reads eliminated. |
| 38 | * 4. exfat_read_directory: follows the cluster chain (was reading only the |
| 39 | * first cluster, silently truncating large directories). |
| 40 | * 5. exfat_parse_file_entry: gets NameLength from stream extension; |
| 41 | * accumulates chars across multiple filename entries; respects the |
| 42 | * secondary_count to avoid walking beyond the entry set. |
| 43 | * 6. File extraction: uses a single stack buffer (EXFAT_IO_CHUNK_SIZE) |
| 44 | * instead of per-cluster malloc; respects the NoFatChain flag. |
| 45 | * 7. Added exfat_extract_to_buffer() for in-memory extraction. |
| 46 | * 8. fseeko() used for large-file offsets (> 2 GiB). |
| 47 | */ |
| 48 | |
| 49 | #include "exfat_unpacker.h" |
| 50 | #include <errno.h> |
| 51 | #if defined(_MSC_VER) |
| 52 | # include <io.h> |
| 53 | # include <fcntl.h> |
| 54 | # define dup _dup |
| 55 | # define fdopen _fdopen |
| 56 | # define close _close |
| 57 | # define write _write |
| 58 | # define fseeko _fseeki64 |
| 59 | # define ftello _ftelli64 |
| 60 | # define lseek64 _lseeki64 |
| 61 | #else |
| 62 | # include <unistd.h> /* write(), close() */ |
| 63 | # define lseek64 lseek |
| 64 | #endif |
| 65 | |
| 66 | /* ── Boot sector ────────────────────────────────────────────────────────── */ |
| 67 | |
| 68 | int exfat_read_boot_sector(exfat_context_t *ctx) { |
| 69 | if (!ctx || !ctx->image_file) |
| 70 | return -1; |
| 71 | |
| 72 | if (fseeko(ctx->image_file, 0, SEEK_SET) != 0) { |
| 73 | perror("[exFAT] fseeko boot sector"); |
| 74 | return -1; |
| 75 | } |
| 76 | if (fread(&ctx->boot_sector, sizeof(exfat_boot_sector_t), 1, |
| 77 | ctx->image_file) != 1) { |
| 78 | perror("[exFAT] fread boot sector"); |
| 79 | return -1; |
| 80 | } |
| 81 | if (exfat_validate_boot_sector(&ctx->boot_sector) != 0) |
| 82 | return -1; |
| 83 | |
| 84 | /* |
| 85 | * Derive working constants. |
| 86 | * Shift values are validated by exfat_validate_boot_sector(). |
| 87 | */ |
| 88 | ctx->bytes_per_sector = 1U << ctx->boot_sector.bytes_per_sector_shift; |
| 89 | ctx->sectors_per_cluster = 1U << ctx->boot_sector.sectors_per_cluster_shift; |
| 90 | ctx->bytes_per_cluster = ctx->bytes_per_sector * ctx->sectors_per_cluster; |
| 91 | ctx->cluster_heap_offset_bytes = |
| 92 | (uint64_t)ctx->boot_sector.cluster_heap_offset * ctx->bytes_per_sector; |
| 93 | |
| 94 | return 0; |
| 95 | } |
| 96 | |
| 97 | int exfat_validate_boot_sector(const exfat_boot_sector_t *boot) { |
| 98 | if (!boot) |
| 99 | return -1; |
| 100 | |
| 101 | /* FileSystemName must be "EXFAT " (with 3 trailing spaces) */ |
| 102 | if (memcmp(boot->fs_name, EXFAT_FS_NAME, EXFAT_FS_NAME_LEN) != 0) { |
| 103 | fprintf(stderr, "[exFAT] invalid filesystem name\n"); |
| 104 | return -1; |
| 105 | } |
| 106 | if (boot->boot_signature != EXFAT_BOOT_SIGNATURE) { |
| 107 | fprintf(stderr, "[exFAT] invalid boot signature: 0x%04X\n", |
| 108 | boot->boot_signature); |
| 109 | return -1; |
| 110 | } |
| 111 | /* Per spec §3.1.13: BytesPerSectorShift in [9, 12] */ |
| 112 | if (boot->bytes_per_sector_shift < 9 || boot->bytes_per_sector_shift > 12) { |
| 113 | fprintf(stderr, "[exFAT] BytesPerSectorShift=%u out of range [9,12]\n", |
| 114 | boot->bytes_per_sector_shift); |
| 115 | return -1; |
| 116 | } |
| 117 | /* Per spec §3.1.14: BytesPerSectorShift + SectorsPerClusterShift <= 25 */ |
| 118 | if ((unsigned)boot->bytes_per_sector_shift + |
| 119 | (unsigned)boot->sectors_per_cluster_shift > 25U) { |
| 120 | fprintf(stderr, |
| 121 | "[exFAT] shift sum %u+%u > 25 (cluster too large)\n", |
| 122 | boot->bytes_per_sector_shift, |
| 123 | boot->sectors_per_cluster_shift); |
| 124 | return -1; |
| 125 | } |
| 126 | return 0; |
| 127 | } |
| 128 | |
| 129 | void exfat_print_boot_sector(const exfat_boot_sector_t *boot) { |
| 130 | if (!boot) return; |
| 131 | printf("=== exFAT Boot Sector ===\n"); |
| 132 | printf(" FileSystemName : %.8s\n", boot->fs_name); |
| 133 | printf(" FatOffset : %u sectors\n", boot->fat_offset); |
| 134 | printf(" FatLength : %u sectors\n", boot->fat_length); |
| 135 | printf(" ClusterHeapOffset : %u sectors\n", boot->cluster_heap_offset); |
| 136 | printf(" TotalClusters : %u\n", boot->total_clusters); |
| 137 | printf(" RootDirFirstCluster : %u\n", boot->root_dir_first_cluster); |
| 138 | printf(" VolumeSerialNumber : 0x%08X\n", boot->volume_serial); |
| 139 | printf(" FileSystemRevision : 0x%04X\n", boot->fs_revision); |
| 140 | printf(" BytesPerSectorShift : %u (= %u bytes/sector)\n", |
| 141 | boot->bytes_per_sector_shift, 1U << boot->bytes_per_sector_shift); |
| 142 | printf(" SectorsPerClusterShift: %u (= %u sectors/cluster)\n", |
| 143 | boot->sectors_per_cluster_shift, |
| 144 | 1U << boot->sectors_per_cluster_shift); |
| 145 | printf(" NumberOfFats : %u\n", boot->num_fats); |
| 146 | printf(" PercentInUse : %u%%\n", boot->percent_in_use); |
| 147 | printf("\n"); |
| 148 | } |
| 149 | |
| 150 | /* ── FAT ────────────────────────────────────────────────────────────────── */ |
| 151 | |
| 152 | int exfat_read_fat(exfat_context_t *ctx) { |
| 153 | if (!ctx || !ctx->image_file) |
| 154 | return -1; |
| 155 | |
| 156 | uint64_t fat_offset_bytes = |
| 157 | (uint64_t)ctx->boot_sector.fat_offset * ctx->bytes_per_sector; |
| 158 | uint64_t fat_size_bytes = |
| 159 | (uint64_t)ctx->boot_sector.fat_length * ctx->bytes_per_sector; |
| 160 | |
| 161 | /* Sanity: FAT must cover at least total_clusters+2 entries */ |
| 162 | size_t min_entries = (size_t)ctx->boot_sector.total_clusters + 2U; |
| 163 | size_t available = (size_t)(fat_size_bytes / sizeof(uint32_t)); |
| 164 | if (available < min_entries) { |
| 165 | fprintf(stderr, |
| 166 | "[exFAT] FAT too small: %zu entries, need %zu\n", |
| 167 | available, min_entries); |
| 168 | return -1; |
| 169 | } |
| 170 | |
| 171 | ctx->fat_table = (uint32_t *)malloc(fat_size_bytes); |
| 172 | if (!ctx->fat_table) { |
| 173 | perror("[exFAT] malloc FAT"); |
| 174 | return -1; |
| 175 | } |
| 176 | ctx->fat_entries = available; |
| 177 | |
| 178 | if (fseeko(ctx->image_file, (off_t)fat_offset_bytes, SEEK_SET) != 0) { |
| 179 | perror("[exFAT] fseeko FAT"); |
| 180 | free(ctx->fat_table); |
| 181 | ctx->fat_table = NULL; |
| 182 | return -1; |
| 183 | } |
| 184 | if (fread(ctx->fat_table, fat_size_bytes, 1, ctx->image_file) != 1) { |
| 185 | perror("[exFAT] fread FAT"); |
| 186 | free(ctx->fat_table); |
| 187 | ctx->fat_table = NULL; |
| 188 | return -1; |
| 189 | } |
| 190 | return 0; |
| 191 | } |
| 192 | |
| 193 | uint32_t exfat_get_next_cluster(const exfat_context_t *ctx, uint32_t cluster) { |
| 194 | if (!ctx || !ctx->fat_table) |
| 195 | return EXFAT_FAT_END_OF_CHAIN; |
| 196 | if (cluster >= (uint32_t)ctx->fat_entries) |
| 197 | return EXFAT_FAT_END_OF_CHAIN; |
| 198 | return ctx->fat_table[cluster]; |
| 199 | } |
| 200 | |
| 201 | void exfat_free_fat(exfat_context_t *ctx) { |
| 202 | if (ctx && ctx->fat_table) { |
| 203 | free(ctx->fat_table); |
| 204 | ctx->fat_table = NULL; |
| 205 | ctx->fat_entries = 0; |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | /* ── Cluster I/O ────────────────────────────────────────────────────────── */ |
| 210 | |
| 211 | uint64_t exfat_get_cluster_offset(const exfat_context_t *ctx, uint32_t cluster) { |
| 212 | if (!ctx) return 0; |
| 213 | return ctx->cluster_heap_offset_bytes + |
| 214 | (uint64_t)(cluster - 2U) * ctx->bytes_per_cluster; |
| 215 | } |
| 216 | |
| 217 | ssize_t exfat_read_cluster(exfat_context_t *ctx, uint32_t cluster, |
| 218 | uint8_t *buffer, size_t size) { |
| 219 | if (!ctx || !ctx->image_file || !buffer) |
| 220 | return -1; |
| 221 | if (!exfat_is_valid_cluster(cluster)) { |
| 222 | fprintf(stderr, "[exFAT] invalid cluster %u\n", cluster); |
| 223 | return -1; |
| 224 | } |
| 225 | uint64_t offset = exfat_get_cluster_offset(ctx, cluster); |
| 226 | size_t bytes_to_read = (size < ctx->bytes_per_cluster) ? |
| 227 | size : ctx->bytes_per_cluster; |
| 228 | |
| 229 | if (fseeko(ctx->image_file, (off_t)offset, SEEK_SET) != 0) { |
| 230 | perror("[exFAT] fseeko cluster"); |
| 231 | return -1; |
| 232 | } |
| 233 | size_t got = fread(buffer, 1, bytes_to_read, ctx->image_file); |
| 234 | return (ssize_t)got; |
| 235 | } |
| 236 | |
| 237 | /* ── Directory parsing ──────────────────────────────────────────────────── */ |
| 238 | |
| 239 | int exfat_utf16_to_utf8(const uint16_t *utf16, int nchars, |
| 240 | char *utf8, size_t utf8_size) { |
| 241 | if (!utf16 || !utf8 || utf8_size == 0) |
| 242 | return -1; |
| 243 | |
| 244 | size_t out = 0; |
| 245 | for (int i = 0; i < nchars && out < utf8_size - 1U; i++) { |
| 246 | uint16_t ch = utf16[i]; |
| 247 | if (ch == 0) break; |
| 248 | |
| 249 | if (ch < 0x80U) { |
| 250 | utf8[out++] = (char)ch; |
| 251 | } else if (ch < 0x800U) { |
| 252 | if (out + 2 >= utf8_size) break; |
| 253 | utf8[out++] = (char)(0xC0U | (ch >> 6)); |
| 254 | utf8[out++] = (char)(0x80U | (ch & 0x3FU)); |
| 255 | } else { |
| 256 | if (out + 3 >= utf8_size) break; |
| 257 | utf8[out++] = (char)(0xE0U | (ch >> 12)); |
| 258 | utf8[out++] = (char)(0x80U | ((ch >> 6) & 0x3FU)); |
| 259 | utf8[out++] = (char)(0x80U | (ch & 0x3FU)); |
| 260 | } |
| 261 | } |
| 262 | utf8[out] = '\0'; |
| 263 | return 0; |
| 264 | } |
| 265 | |
| 266 | /* |
| 267 | * Parse one complete directory entry set (0x85 + secondaries). |
| 268 | * |
| 269 | * @param data flat buffer containing one or more 32-byte entries |
| 270 | * @param offset byte offset of the 0x85 entry within data[] |
| 271 | * @param data_len total valid bytes in data[] |
| 272 | * @param info output |
| 273 | * |
| 274 | * @return total bytes consumed by this entry set (= (1+secondary_count)*32), |
| 275 | * or -1 if the entry is not a 0x85 file/dir entry or data is short. |
| 276 | */ |
| 277 | int exfat_parse_file_entry(const uint8_t *data, size_t offset, size_t data_len, |
| 278 | exfat_file_info_t *info) { |
| 279 | if (!data || !info || offset >= data_len) |
| 280 | return -1; |
| 281 | |
| 282 | const exfat_file_entry_t *fe = |
| 283 | (const exfat_file_entry_t *)(data + offset); |
| 284 | |
| 285 | if (fe->entry_type != EXFAT_ENTRY_TYPE_FILE_DIR) |
| 286 | return -1; |
| 287 | |
| 288 | uint8_t secondary_count = fe->secondary_count; |
| 289 | size_t total_bytes = (size_t)(1 + secondary_count) * EXFAT_ENTRY_SIZE; |
| 290 | |
| 291 | if (offset + total_bytes > data_len) |
| 292 | return -1; /* entry set extends beyond buffer */ |
| 293 | |
| 294 | memset(info, 0, sizeof(exfat_file_info_t)); |
| 295 | info->attributes = fe->file_attributes; |
| 296 | info->create_time = fe->create_time; |
| 297 | info->last_modified_time= fe->last_modified_time; |
| 298 | info->last_accessed_time= fe->last_accessed_time; |
| 299 | info->is_directory = exfat_is_directory(fe->file_attributes); |
| 300 | |
| 301 | /* Walk secondary entries ─────────────────────────────────────────── */ |
| 302 | int name_chars_collected = 0; /* chars accumulated so far */ |
| 303 | int name_length = 0; /* from stream extension */ |
| 304 | char name_buf[EXFAT_MAX_FILENAME_LEN * 4 + 1]; /* worst-case UTF-8 */ |
| 305 | size_t name_buf_used = 0; |
| 306 | |
| 307 | for (int s = 0; s < (int)secondary_count; s++) { |
| 308 | const uint8_t *sec = data + offset + (size_t)(s + 1) * EXFAT_ENTRY_SIZE; |
| 309 | uint8_t stype = sec[0]; |
| 310 | |
| 311 | if (stype == EXFAT_ENTRY_TYPE_STREAM_EXT) { |
| 312 | const exfat_stream_ext_t *sx = (const exfat_stream_ext_t *)sec; |
| 313 | info->first_cluster = sx->first_cluster; |
| 314 | info->data_length = sx->data_length; |
| 315 | info->no_fat_chain = (sx->flags & EXFAT_FLAG_NO_FAT_CHAIN) ? 1 : 0; |
| 316 | name_length = (int)sx->name_length; /* total chars */ |
| 317 | |
| 318 | } else if (stype == EXFAT_ENTRY_TYPE_FILENAME) { |
| 319 | const exfat_filename_entry_t *fn = |
| 320 | (const exfat_filename_entry_t *)sec; |
| 321 | |
| 322 | /* |
| 323 | * Each filename entry carries up to 15 UTF-16LE code units. |
| 324 | * Collect until we reach name_length chars (from stream ext). |
| 325 | * This correctly handles filenames longer than 15 chars that |
| 326 | * span multiple filename entries. |
| 327 | */ |
| 328 | int chars_remaining = name_length - name_chars_collected; |
| 329 | if (chars_remaining <= 0) |
| 330 | break; |
| 331 | |
| 332 | int chars_here = (chars_remaining < 15) ? chars_remaining : 15; |
| 333 | |
| 334 | /* |
| 335 | * Copy the packed uint16_t array to a local aligned buffer before |
| 336 | * passing to exfat_utf16_to_utf8(). Taking the address of a packed |
| 337 | * struct member directly causes -Waddress-of-packed-member and |
| 338 | * potential UB on architectures that require aligned 16-bit reads. |
| 339 | */ |
| 340 | uint16_t name_copy[15]; |
| 341 | memcpy(name_copy, fn->file_name, (size_t)chars_here * sizeof(uint16_t)); |
| 342 | |
| 343 | char chunk[15 * 4 + 1]; |
| 344 | exfat_utf16_to_utf8(name_copy, chars_here, |
| 345 | chunk, sizeof(chunk)); |
| 346 | |
| 347 | size_t chunk_len = strlen(chunk); |
| 348 | if (name_buf_used + chunk_len < sizeof(name_buf)) { |
| 349 | memcpy(name_buf + name_buf_used, chunk, chunk_len); |
| 350 | name_buf_used += chunk_len; |
| 351 | name_chars_collected += chars_here; |
| 352 | } |
| 353 | } |
| 354 | } |
| 355 | |
| 356 | name_buf[name_buf_used] = '\0'; |
| 357 | /* Copy into the fixed-size filename field; snprintf ensures NUL */ |
| 358 | snprintf(info->filename, sizeof(info->filename), "%s", name_buf); |
| 359 | |
| 360 | return (int)total_bytes; |
| 361 | } |
| 362 | |
| 363 | /* |
| 364 | * Read all directory entries from a cluster chain. |
| 365 | * |
| 366 | * FIX: the original only read the first cluster. This implementation |
| 367 | * follows the FAT chain (or advances contiguously for NoFatChain dirs) |
| 368 | * until the chain ends or max_entries is reached. |
| 369 | * |
| 370 | * @note Directories themselves never set NoFatChain in practice, but we |
| 371 | * handle it for correctness. |
| 372 | */ |
| 373 | int exfat_read_directory(exfat_context_t *ctx, uint32_t first_cluster, |
| 374 | exfat_file_info_t *entries, int max_entries) { |
| 375 | if (!ctx || !entries || max_entries <= 0) |
| 376 | return -1; |
| 377 | |
| 378 | uint8_t *cluster_buf = (uint8_t *)malloc(ctx->bytes_per_cluster); |
| 379 | if (!cluster_buf) { |
| 380 | perror("[exFAT] malloc directory buffer"); |
| 381 | return -1; |
| 382 | } |
| 383 | |
| 384 | int entry_count = 0; |
| 385 | uint32_t cluster = first_cluster; |
| 386 | |
| 387 | while (exfat_is_valid_cluster(cluster) && entry_count < max_entries) { |
| 388 | ssize_t got = exfat_read_cluster(ctx, cluster, cluster_buf, |
| 389 | ctx->bytes_per_cluster); |
| 390 | if (got <= 0) |
| 391 | break; |
| 392 | |
| 393 | size_t offset = 0; |
| 394 | while (offset + EXFAT_ENTRY_SIZE <= (size_t)got && |
| 395 | entry_count < max_entries) { |
| 396 | |
| 397 | uint8_t etype = cluster_buf[offset]; |
| 398 | |
| 399 | if (etype == EXFAT_ENTRY_TYPE_EOD) |
| 400 | goto done; /* end-of-directory — stop scanning */ |
| 401 | |
| 402 | if (etype != EXFAT_ENTRY_TYPE_FILE_DIR) { |
| 403 | offset += EXFAT_ENTRY_SIZE; |
| 404 | continue; |
| 405 | } |
| 406 | |
| 407 | int consumed = exfat_parse_file_entry( |
| 408 | cluster_buf, offset, (size_t)got, |
| 409 | &entries[entry_count]); |
| 410 | |
| 411 | if (consumed <= 0) { |
| 412 | offset += EXFAT_ENTRY_SIZE; |
| 413 | continue; |
| 414 | } |
| 415 | |
| 416 | /* Only count entries with a non-empty filename */ |
| 417 | if (entries[entry_count].filename[0] != '\0') |
| 418 | entry_count++; |
| 419 | |
| 420 | offset += (size_t)consumed; |
| 421 | } |
| 422 | |
| 423 | /* Advance to next cluster */ |
| 424 | uint32_t next = exfat_get_next_cluster(ctx, cluster); |
| 425 | if (exfat_is_end_of_chain(next)) |
| 426 | break; |
| 427 | cluster = next; |
| 428 | } |
| 429 | |
| 430 | done: |
| 431 | free(cluster_buf); |
| 432 | return entry_count; |
| 433 | } |
| 434 | |
| 435 | /* ── File extraction helpers ────────────────────────────────────────────── */ |
| 436 | |
| 437 | /* |
| 438 | * Core read loop shared by all three extraction functions. |
| 439 | * |
| 440 | * Reads the file described by `info` in EXFAT_IO_CHUNK_SIZE blocks and |
| 441 | * calls write_cb(write_arg, buf, len) for each. Returns 0 on success. |
| 442 | * |
| 443 | * NoFatChain support: if info->no_fat_chain is set, advance clusters |
| 444 | * sequentially instead of following the FAT. |
| 445 | */ |
| 446 | typedef int (*exfat_write_cb_t)(void *arg, const uint8_t *buf, size_t len); |
| 447 | |
| 448 | static int exfat_read_file_data(exfat_context_t *ctx, |
| 449 | const exfat_file_info_t *info, |
| 450 | exfat_write_cb_t write_cb, void *write_arg) { |
| 451 | uint8_t io_buf[EXFAT_IO_CHUNK_SIZE]; /* fixed stack buffer, no malloc */ |
| 452 | uint64_t remaining = info->data_length; |
| 453 | uint32_t cluster = info->first_cluster; |
| 454 | |
| 455 | if (!exfat_is_valid_cluster(cluster) && remaining > 0) { |
| 456 | fprintf(stderr, "[exFAT] invalid first cluster %u\n", cluster); |
| 457 | return -1; |
| 458 | } |
| 459 | |
| 460 | while (remaining > 0 && exfat_is_valid_cluster(cluster)) { |
| 461 | uint64_t cluster_off = exfat_get_cluster_offset(ctx, cluster); |
| 462 | |
| 463 | /* How much of this cluster do we still need? */ |
| 464 | uint64_t want = (remaining < ctx->bytes_per_cluster) ? |
| 465 | remaining : ctx->bytes_per_cluster; |
| 466 | |
| 467 | /* Read in EXFAT_IO_CHUNK_SIZE pieces within the cluster */ |
| 468 | uint64_t cluster_done = 0; |
| 469 | while (cluster_done < want) { |
| 470 | uint64_t chunk = want - cluster_done; |
| 471 | if (chunk > EXFAT_IO_CHUNK_SIZE) |
| 472 | chunk = EXFAT_IO_CHUNK_SIZE; |
| 473 | |
| 474 | uint64_t abs_off = cluster_off + cluster_done; |
| 475 | if (fseeko(ctx->image_file, (off_t)abs_off, SEEK_SET) != 0) { |
| 476 | perror("[exFAT] fseeko file data"); |
| 477 | return -1; |
| 478 | } |
| 479 | size_t got = fread(io_buf, 1, (size_t)chunk, ctx->image_file); |
| 480 | if (got == 0) |
| 481 | return -1; /* short read */ |
| 482 | |
| 483 | if (write_cb(write_arg, io_buf, got) != 0) |
| 484 | return -1; |
| 485 | |
| 486 | cluster_done += got; |
| 487 | } |
| 488 | |
| 489 | remaining -= want; |
| 490 | if (remaining == 0) |
| 491 | break; |
| 492 | |
| 493 | /* Advance to next cluster */ |
| 494 | if (info->no_fat_chain) { |
| 495 | cluster++; /* contiguous layout */ |
| 496 | } else { |
| 497 | uint32_t next = exfat_get_next_cluster(ctx, cluster); |
| 498 | if (exfat_is_end_of_chain(next)) { |
| 499 | if (remaining > 0) |
| 500 | fprintf(stderr, "[exFAT] chain ended with %llu bytes left\n", |
| 501 | (unsigned long long)remaining); |
| 502 | break; |
| 503 | } |
| 504 | cluster = next; |
| 505 | } |
| 506 | } |
| 507 | return 0; |
| 508 | } |
| 509 | |
| 510 | /* write_cb for FILE* output */ |
| 511 | static int write_cb_file(void *arg, const uint8_t *buf, size_t len) { |
| 512 | FILE *f = (FILE *)arg; |
| 513 | return (fwrite(buf, 1, len, f) == len) ? 0 : -1; |
| 514 | } |
| 515 | |
| 516 | /* write_cb for fd output */ |
| 517 | static int write_cb_fd(void *arg, const uint8_t *buf, size_t len) { |
| 518 | int fd = *(int *)arg; |
| 519 | while (len > 0) { |
| 520 | ssize_t n = write(fd, buf, len); |
| 521 | if (n <= 0) { perror("[exFAT] write"); return -1; } |
| 522 | buf += n; |
| 523 | len -= (size_t)n; |
| 524 | } |
| 525 | return 0; |
| 526 | } |
| 527 | |
| 528 | /* write_cb for in-memory output */ |
| 529 | typedef struct { uint8_t *buf; size_t pos; size_t cap; } mem_ctx_t; |
| 530 | static int write_cb_mem(void *arg, const uint8_t *buf, size_t len) { |
| 531 | mem_ctx_t *m = (mem_ctx_t *)arg; |
| 532 | if (m->pos + len > m->cap) { |
| 533 | size_t can_copy = m->cap - m->pos; |
| 534 | memcpy(m->buf + m->pos, buf, can_copy); |
| 535 | m->pos += can_copy; |
| 536 | return -1; /* buffer full */ |
| 537 | } |
| 538 | memcpy(m->buf + m->pos, buf, len); |
| 539 | m->pos += len; |
| 540 | return 0; |
| 541 | } |
| 542 | |
| 543 | int exfat_extract_file(exfat_context_t *ctx, const exfat_file_info_t *info, |
| 544 | const char *output_path) { |
| 545 | if (!ctx || !info || !output_path) |
| 546 | return -1; |
| 547 | FILE *f = fopen(output_path, "wb"); |
| 548 | if (!f) { perror("[exFAT] fopen output"); return -1; } |
| 549 | int rc = exfat_read_file_data(ctx, info, write_cb_file, f); |
| 550 | fclose(f); |
| 551 | return rc; |
| 552 | } |
| 553 | |
| 554 | int exfat_extract_file_fd(exfat_context_t *ctx, const exfat_file_info_t *info, |
| 555 | int output_fd) { |
| 556 | if (!ctx || !info || output_fd < 0) |
| 557 | return -1; |
| 558 | return exfat_read_file_data(ctx, info, write_cb_fd, &output_fd); |
| 559 | } |
| 560 | |
| 561 | /* |
| 562 | * Extract file content directly into a caller-provided buffer. |
| 563 | * |
| 564 | * @return number of bytes written on success, -1 on error (including |
| 565 | * truncation when buf_size < info->data_length). |
| 566 | */ |
| 567 | ssize_t exfat_extract_to_buffer(exfat_context_t *ctx, |
| 568 | const exfat_file_info_t *info, |
| 569 | uint8_t *buf, size_t buf_size) { |
| 570 | if (!ctx || !info || !buf || buf_size == 0) |
| 571 | return -1; |
| 572 | if (info->data_length > buf_size) { |
| 573 | fprintf(stderr, "[exFAT] buffer too small: need %llu, have %zu\n", |
| 574 | (unsigned long long)info->data_length, buf_size); |
| 575 | return -1; |
| 576 | } |
| 577 | mem_ctx_t m = { buf, 0, buf_size }; |
| 578 | if (exfat_read_file_data(ctx, info, write_cb_mem, &m) != 0) |
| 579 | return -1; |
| 580 | return (ssize_t)m.pos; |
| 581 | } |
| 582 | |
| 583 | /* ── Context lifecycle ──────────────────────────────────────────────────── */ |
| 584 | |
| 585 | int exfat_init(exfat_context_t *ctx, const char *image_path) { |
| 586 | if (!ctx || !image_path) |
| 587 | return -1; |
| 588 | |
| 589 | memset(ctx, 0, sizeof(exfat_context_t)); |
| 590 | ctx->image_file = fopen(image_path, "rb"); |
| 591 | if (!ctx->image_file) { |
| 592 | perror("[exFAT] fopen image"); |
| 593 | return -1; |
| 594 | } |
| 595 | if (exfat_read_boot_sector(ctx) != 0 || exfat_read_fat(ctx) != 0) { |
| 596 | exfat_cleanup(ctx); |
| 597 | return -1; |
| 598 | } |
| 599 | return 0; |
| 600 | } |
| 601 | |
| 602 | void exfat_cleanup(exfat_context_t *ctx) { |
| 603 | if (!ctx) return; |
| 604 | if (ctx->image_file) { |
| 605 | fclose(ctx->image_file); |
| 606 | ctx->image_file = NULL; |
| 607 | } |
| 608 | exfat_free_fat(ctx); |
| 609 | } |
| 610 | |
| 611 | /* ── Utility ────────────────────────────────────────────────────────────── */ |
| 612 | |
| 613 | void exfat_print_file_info(const exfat_file_info_t *info) { |
| 614 | if (!info) return; |
| 615 | char sz[32]; |
| 616 | exfat_format_size(info->data_length, sz, sizeof(sz)); |
| 617 | printf("%s %-40s %10s cluster=%u%s\n", |
| 618 | info->is_directory ? "DIR " : "FILE", |
| 619 | info->filename, sz, info->first_cluster, |
| 620 | info->no_fat_chain ? " [contiguous]" : ""); |
| 621 | } |
| 622 | |
| 623 | time_t exfat_dos_time_to_unix(uint32_t dos_time) { |
| 624 | struct tm t; |
| 625 | memset(&t, 0, sizeof(t)); |
| 626 | t.tm_sec = (int)((dos_time & 0x1FU) * 2); |
| 627 | t.tm_min = (int)((dos_time >> 5) & 0x3FU); |
| 628 | t.tm_hour = (int)((dos_time >> 11) & 0x1FU); |
| 629 | t.tm_mday = (int)((dos_time >> 16) & 0x1FU); |
| 630 | t.tm_mon = (int)(((dos_time >> 21) & 0x0FU) - 1U); |
| 631 | t.tm_year = (int)(((dos_time >> 25) & 0x7FU) + 80U); |
| 632 | t.tm_isdst = -1; |
| 633 | return mktime(&t); |
| 634 | } |
| 635 | |
| 636 | void exfat_format_size(uint64_t size, char *buf, size_t buf_size) { |
| 637 | if (!buf || buf_size == 0) return; |
| 638 | static const char *units[] = { "B", "KB", "MB", "GB", "TB" }; |
| 639 | double d = (double)size; |
| 640 | int u = 0; |
| 641 | while (d >= 1024.0 && u < 4) { d /= 1024.0; u++; } |
| 642 | snprintf(buf, buf_size, "%.2f %s", d, units[u]); |
| 643 | } |
| 644 |