| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | /* | ||
| 2 | * mpq-api.c -- public archive, file and block operations. | ||
| 3 | * | ||
| 4 | * Copyright (c) 2003-2026 Maik Broemme <mbroemme@libmpq.org> | ||
| 5 | * | ||
| 6 | * This file is free software; you can redistribute it and/or modify | ||
| 7 | * it under the terms of the GNU Lesser General Public License as published by | ||
| 8 | * the Free Software Foundation; either version 2.1 of the License, or | ||
| 9 | * (at your option) any later version. | ||
| 10 | * | ||
| 11 | * This file is distributed in the hope that it will be useful, | ||
| 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 14 | * GNU Lesser General Public License for more details. | ||
| 15 | * | ||
| 16 | * You should have received a copy of the GNU Lesser General Public License | ||
| 17 | * along with this file; if not, see <https://www.gnu.org/licenses/>. | ||
| 18 | */ | ||
| 19 | |||
| 20 | #ifdef HAVE_CONFIG_H | ||
| 21 | #include "config.h" | ||
| 22 | #endif | ||
| 23 | |||
| 24 | #include "mpq-compression.h" | ||
| 25 | #include "mpq-crypto.h" | ||
| 26 | #include "mpq-endian.h" | ||
| 27 | #include "mpq-internal.h" | ||
| 28 | #include "mpq-platform.h" | ||
| 29 | #include "mpq-reader.h" | ||
| 30 | #include "mpq-writer.h" | ||
| 31 | #include <libmpq/mpq.h> | ||
| 32 | |||
| 33 | #include <errno.h> | ||
| 34 | #include <fcntl.h> | ||
| 35 | #include <stdlib.h> | ||
| 36 | #include <string.h> | ||
| 37 | #include <sys/stat.h> | ||
| 38 | |||
| 39 | /* Error strings indexed by the negated libmpq error code. */ | ||
| 40 | static const char *libmpq_error_strings[] = { "success", | ||
| 41 | "open error on file", | ||
| 42 | "close error on file", | ||
| 43 | "lseek error on file", | ||
| 44 | "read error on file", | ||
| 45 | "write error on file", | ||
| 46 | "memory allocation error", | ||
| 47 | "format error", | ||
| 48 | "init() wasn't called", | ||
| 49 | "buffer size is too small", | ||
| 50 | "archive, file, or block does not exist", | ||
| 51 | "we don't know the decryption seed", | ||
| 52 | "error on unpacking file" }; | ||
| 53 | |||
| 54 | /* Verify that a file payload subrange is both internally consistent and | ||
| 55 | * contained in the physical backing file captured when the archive opened. | ||
| 56 | * Sector offsets and block-table sizes are archive-controlled, so this check | ||
| 57 | * must happen before using either value for allocation or stream reads. */ | ||
| 58 | static int32_t | ||
| 59 | 1365 | libmpq__validate_payload_range( | |
| 60 | const mpq_archive_s *mpq_archive, uint32_t block_table_index, uint64_t relative_offset, | ||
| 61 | uint64_t size | ||
| 62 | ) | ||
| 63 | { | ||
| 64 | uint64_t payload_offset; | ||
| 65 | uint64_t absolute_offset; | ||
| 66 | |||
| 67 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1365 times.
|
1365 | if (mpq_archive->archive_offset < 0) { |
| 68 | ✗ | return LIBMPQ_ERROR_FORMAT; | |
| 69 | } | ||
| 70 | |||
| 71 | 1365 | payload_offset = ((uint64_t)mpq_archive->mpq_block_ex[block_table_index].offset_high << 32) | | |
| 72 | 1365 | mpq_archive->mpq_block[block_table_index].offset; | |
| 73 | 1365 | absolute_offset = (uint64_t)mpq_archive->archive_offset; | |
| 74 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1365 times.
|
1365 | if (payload_offset > UINT64_MAX - absolute_offset) { |
| 75 | ✗ | return LIBMPQ_ERROR_FORMAT; | |
| 76 | } | ||
| 77 | 1365 | absolute_offset += payload_offset; | |
| 78 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1365 times.
|
1365 | if (relative_offset > UINT64_MAX - absolute_offset) { |
| 79 | ✗ | return LIBMPQ_ERROR_FORMAT; | |
| 80 | } | ||
| 81 | 1365 | absolute_offset += relative_offset; | |
| 82 |
1/2✓ Branch 0 taken 1365 times.
✗ Branch 1 not taken.
|
1365 | if (absolute_offset > mpq_archive->file_size || |
| 83 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1365 times.
|
1365 | size > mpq_archive->file_size - absolute_offset) { |
| 84 | ✗ | return LIBMPQ_ERROR_READ; | |
| 85 | } | ||
| 86 | |||
| 87 | 1365 | return LIBMPQ_SUCCESS; | |
| 88 | } | ||
| 89 | |||
| 90 | /* Return the configured libmpq package version. | ||
| 91 | * The returned pointer refers to immutable library storage and remains valid | ||
| 92 | * for the lifetime of the process. */ | ||
| 93 | const char * | ||
| 94 | 2 | libmpq__version(void) | |
| 95 | { | ||
| 96 | 2 | return VERSION; | |
| 97 | } | ||
| 98 | |||
| 99 | /* Translate a libmpq return code into a static diagnostic string. | ||
| 100 | * Valid codes index an internal immutable table; invalid positive or out-of- | ||
| 101 | * range negative values return NULL instead of reading outside that table. */ | ||
| 102 | const char * | ||
| 103 | 15 | libmpq__strerror(int32_t return_code) | |
| 104 | { | ||
| 105 | |||
| 106 | /* Only negative libmpq error codes and zero are valid table indexes. */ | ||
| 107 |
2/2✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1 times.
|
15 | if (-return_code < 0 || |
| 108 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 13 times.
|
14 | (size_t)-return_code >= sizeof(libmpq_error_strings) / sizeof(libmpq_error_strings[0])) |
| 109 | 2 | return NULL; | |
| 110 | |||
| 111 | /* Return the static string owned by the library. */ | ||
| 112 | 13 | return libmpq_error_strings[-return_code]; | |
| 113 | } | ||
| 114 | |||
| 115 | /* Create an MPQ archive through the internal writer implementation. | ||
| 116 | * This public facade preserves the stable API while keeping archive layout | ||
| 117 | * and file-table construction in the writer module. */ | ||
| 118 | int32_t | ||
| 119 | 70 | libmpq__archive_create( | |
| 120 | mpq_archive_s **out, const char *path, const mpq_archive_create_options_s *options | ||
| 121 | ) | ||
| 122 | { | ||
| 123 | 70 | return libmpq__writer_archive_create(out, path, options); | |
| 124 | } | ||
| 125 | |||
| 126 | /* Begin a streamed file through the internal writer implementation. | ||
| 127 | * The returned opaque writer owns the in-progress file state until finish or | ||
| 128 | * an error closes the stream. */ | ||
| 129 | int32_t | ||
| 130 | 13 | libmpq__file_begin( | |
| 131 | mpq_archive_s *archive, const char *name, libmpq__off_t size, const mpq_file_options_s *options, | ||
| 132 | mpq_writer_s **out | ||
| 133 | ) | ||
| 134 | { | ||
| 135 | 13 | return libmpq__writer_file_begin(archive, name, size, options, out); | |
| 136 | } | ||
| 137 | |||
| 138 | /* Write one input range through the internal writer implementation. | ||
| 139 | * The writer validates the declared file size and buffers or flushes sectors | ||
| 140 | * according to the selected storage and compression options. */ | ||
| 141 | int32_t | ||
| 142 | 15 | libmpq__file_write(mpq_writer_s *writer, const uint8_t *buffer, libmpq__off_t size) | |
| 143 | { | ||
| 144 | 15 | return libmpq__writer_file_write(writer, buffer, size); | |
| 145 | } | ||
| 146 | |||
| 147 | /* Finish a streamed file through the internal writer implementation. | ||
| 148 | * Finalization verifies that all declared bytes were supplied and publishes | ||
| 149 | * the completed file entry in the archive tables. */ | ||
| 150 | int32_t | ||
| 151 | 8 | libmpq__file_finish(mpq_writer_s *writer) | |
| 152 | { | ||
| 153 | 8 | return libmpq__writer_file_finish(writer); | |
| 154 | } | ||
| 155 | |||
| 156 | /* Add an in-memory file through the internal writer implementation. | ||
| 157 | * The convenience call performs begin, write, and finish operations while | ||
| 158 | * retaining the same validation and compression behavior as streaming. */ | ||
| 159 | int32_t | ||
| 160 | 565 | libmpq__file_add( | |
| 161 | mpq_archive_s *archive, const char *name, const uint8_t *data, libmpq__off_t size, | ||
| 162 | const mpq_file_options_s *options | ||
| 163 | ) | ||
| 164 | { | ||
| 165 | 565 | return libmpq__writer_file_add(archive, name, data, size, options); | |
| 166 | } | ||
| 167 | |||
| 168 | /* Add a filesystem file through the internal writer implementation. | ||
| 169 | * The source is read in bounded chunks, so callers need not load the complete | ||
| 170 | * file into memory before archive creation begins. */ | ||
| 171 | int32_t | ||
| 172 | 5 | libmpq__file_add_path( | |
| 173 | mpq_archive_s *archive, const char *name, const char *source, const mpq_file_options_s *options | ||
| 174 | ) | ||
| 175 | { | ||
| 176 | 5 | return libmpq__writer_file_add_path(archive, name, source, options); | |
| 177 | } | ||
| 178 | |||
| 179 | /* Open an MPQ archive from a path and optional embedded archive offset. | ||
| 180 | * A sentinel offset enables embedded-header scanning, while an explicit offset | ||
| 181 | * restricts parsing to the requested archive location. */ | ||
| 182 | int32_t | ||
| 183 | 74 | libmpq__archive_open( | |
| 184 | mpq_archive_s **mpq_archive, const char *mpq_filename, libmpq__off_t archive_offset | ||
| 185 | ) | ||
| 186 | { | ||
| 187 | 74 | return libmpq__reader_archive_open_path(mpq_archive, mpq_filename, archive_offset); | |
| 188 | } | ||
| 189 | |||
| 190 | /* Reopen an archive with independent file I/O, metadata, and lazy caches. | ||
| 191 | * Cloning rejects writer handles and verifies the source path still identifies | ||
| 192 | * the same file before reparsing it into a separate archive object. */ | ||
| 193 | int32_t | ||
| 194 | 3 | libmpq__archive_clone(mpq_archive_s **clone, mpq_archive_s *source) | |
| 195 | { | ||
| 196 | struct stat file_status; | ||
| 197 | |||
| 198 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
|
3 | if (clone == NULL) |
| 199 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 200 | 2 | *clone = NULL; | |
| 201 | |||
| 202 |
4/6✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1 times.
|
2 | if (source == NULL || source->filename == NULL || source->write_mode) |
| 203 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 204 | |||
| 205 | #if !defined(_WIN32) && !defined(_WIN64) | ||
| 206 | |||
| 207 | /* Reject cloning after the backing path has been replaced or removed. */ | ||
| 208 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (source->file_identity_valid) { |
| 209 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
|
1 | if (stat(source->filename, &file_status) < 0) |
| 210 | ✗ | return errno == ENOENT ? LIBMPQ_ERROR_EXIST : LIBMPQ_ERROR_OPEN; | |
| 211 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if ((uint64_t)file_status.st_dev != source->file_device || |
| 212 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | (uint64_t)file_status.st_ino != source->file_inode) |
| 213 | ✗ | return LIBMPQ_ERROR_EXIST; | |
| 214 | } | ||
| 215 | #endif | ||
| 216 | |||
| 217 | 1 | return libmpq__reader_archive_open_path(clone, source->filename, source->archive_offset); | |
| 218 | } | ||
| 219 | |||
| 220 | /* Close the archive file and release all metadata tables allocated during archive open. | ||
| 221 | * Writer handles are finalized before their archive storage is freed, while | ||
| 222 | * reader-side cached block offsets are released entry by entry. */ | ||
| 223 | int32_t | ||
| 224 | 129 | libmpq__archive_close(mpq_archive_s *mpq_archive) | |
| 225 | { | ||
| 226 | uint32_t i; | ||
| 227 | 129 | int32_t result = LIBMPQ_SUCCESS; | |
| 228 | |||
| 229 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 128 times.
|
129 | if (mpq_archive == NULL) |
| 230 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 231 | |||
| 232 |
2/2✓ Branch 0 taken 65 times.
✓ Branch 1 taken 63 times.
|
128 | if (mpq_archive->write_mode) { |
| 233 | |||
| 234 | /* Writer closure must serialize tables before releasing writer storage. */ | ||
| 235 | 65 | result = libmpq__writer_finalize(mpq_archive); | |
| 236 |
2/6✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 65 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
|
65 | if (mpq_archive->fp != NULL && fclose(mpq_archive->fp) < 0 && result == LIBMPQ_SUCCESS) |
| 237 | ✗ | result = LIBMPQ_ERROR_CLOSE; | |
| 238 |
2/2✓ Branch 0 taken 3010 times.
✓ Branch 1 taken 65 times.
|
3075 | for (i = 0; i < mpq_archive->write_capacity; i++) |
| 239 |
1/2✓ Branch 0 taken 3010 times.
✗ Branch 1 not taken.
|
3010 | free(mpq_archive->write_names ? mpq_archive->write_names[i] : NULL); |
| 240 | 65 | free(mpq_archive->write_names); | |
| 241 | 65 | free(mpq_archive->write_locales); | |
| 242 | 65 | free(mpq_archive->write_platforms); | |
| 243 | 65 | free(mpq_archive->mpq_hash); | |
| 244 | 65 | free(mpq_archive->mpq_block); | |
| 245 | 65 | free(mpq_archive->mpq_block_ex); | |
| 246 | 65 | free(mpq_archive->filename); | |
| 247 | 65 | free(mpq_archive); | |
| 248 | 65 | return result; | |
| 249 | } | ||
| 250 | |||
| 251 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 63 times.
|
63 | if ((fclose(mpq_archive->fp)) < 0) { |
| 252 | |||
| 253 | /* Keep the handle intact so the caller may retry closing it. */ | ||
| 254 | ✗ | return LIBMPQ_ERROR_CLOSE; | |
| 255 | } | ||
| 256 | |||
| 257 |
2/2✓ Branch 0 taken 2016 times.
✓ Branch 1 taken 63 times.
|
2079 | for (i = 0; i < mpq_archive->mpq_header.block_table_count; i++) { |
| 258 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 2016 times.
|
2016 | if (mpq_archive->mpq_file[i] != NULL) { |
| 259 | ✗ | free(mpq_archive->mpq_file[i]->packed_offset); | |
| 260 | ✗ | free(mpq_archive->mpq_file[i]); | |
| 261 | } | ||
| 262 | } | ||
| 263 | |||
| 264 | 63 | free(mpq_archive->mpq_map); | |
| 265 | 63 | free(mpq_archive->mpq_file); | |
| 266 | 63 | free(mpq_archive->mpq_hash); | |
| 267 | 63 | free(mpq_archive->mpq_block); | |
| 268 | 63 | free(mpq_archive->mpq_block_ex); | |
| 269 | 63 | free(mpq_archive->filename); | |
| 270 | 63 | free(mpq_archive); | |
| 271 | |||
| 272 | 63 | return LIBMPQ_SUCCESS; | |
| 273 | } | ||
| 274 | |||
| 275 | /* Return the sum of packed sizes for all files in the archive block table. | ||
| 276 | * The caller supplies an accumulator, allowing this query to preserve the | ||
| 277 | * library's existing additive API behavior. */ | ||
| 278 | int32_t | ||
| 279 | 1 | libmpq__archive_size_packed(mpq_archive_s *mpq_archive, libmpq__off_t *packed_size) | |
| 280 | { | ||
| 281 | |||
| 282 | /* Running total across all block-table entries. */ | ||
| 283 | uint32_t i; | ||
| 284 | |||
| 285 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
|
7 | for (i = 0; i < mpq_archive->files; i++) { |
| 286 | 6 | *packed_size += | |
| 287 | 6 | mpq_archive->mpq_block[mpq_archive->mpq_map[i].block_table_indices].packed_size; | |
| 288 | } | ||
| 289 | |||
| 290 | 1 | return LIBMPQ_SUCCESS; | |
| 291 | } | ||
| 292 | |||
| 293 | /* Return the sum of unpacked sizes for all files in the archive block table. | ||
| 294 | * Only live public file-map entries are counted; unused block-table capacity | ||
| 295 | * does not contribute to the reported total. */ | ||
| 296 | int32_t | ||
| 297 | 1 | libmpq__archive_size_unpacked(mpq_archive_s *mpq_archive, libmpq__off_t *unpacked_size) | |
| 298 | { | ||
| 299 | |||
| 300 | /* Running total across all block-table entries. */ | ||
| 301 | uint32_t i; | ||
| 302 | |||
| 303 |
2/2✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
|
7 | for (i = 0; i < mpq_archive->files; i++) { |
| 304 | 6 | *unpacked_size += | |
| 305 | 6 | mpq_archive->mpq_block[mpq_archive->mpq_map[i].block_table_indices].unpacked_size; | |
| 306 | } | ||
| 307 | |||
| 308 | 1 | return LIBMPQ_SUCCESS; | |
| 309 | } | ||
| 310 | |||
| 311 | /* Return the byte offset where the MPQ archive starts in the backing file. | ||
| 312 | * Embedded archives therefore report their discovered start rather than zero. */ | ||
| 313 | int32_t | ||
| 314 | 1 | libmpq__archive_offset(mpq_archive_s *mpq_archive, libmpq__off_t *offset) | |
| 315 | { | ||
| 316 | 1 | *offset = mpq_archive->archive_offset; | |
| 317 | |||
| 318 | 1 | return LIBMPQ_SUCCESS; | |
| 319 | } | ||
| 320 | |||
| 321 | /* Return the MPQ archive format version stored in the header. | ||
| 322 | * The internal zero-based version is converted to the public one-based API | ||
| 323 | * value before being written to the caller's output. */ | ||
| 324 | int32_t | ||
| 325 | 3 | libmpq__archive_version(mpq_archive_s *mpq_archive, uint32_t *version) | |
| 326 | { | ||
| 327 | 3 | *version = mpq_archive->mpq_header.version + 1; | |
| 328 | |||
| 329 | 3 | return LIBMPQ_SUCCESS; | |
| 330 | } | ||
| 331 | |||
| 332 | /* Return the number of valid file entries discovered while opening the archive. | ||
| 333 | * This is the compact public count, not the reserved block-table capacity. */ | ||
| 334 | int32_t | ||
| 335 | 3 | libmpq__archive_files(mpq_archive_s *mpq_archive, uint32_t *files) | |
| 336 | { | ||
| 337 | 3 | *files = mpq_archive->files; | |
| 338 | |||
| 339 | 3 | return LIBMPQ_SUCCESS; | |
| 340 | } | ||
| 341 | |||
| 342 | /* Return the packed size of a file entry by block-table number. | ||
| 343 | * The public file number is validated and translated through the compact map | ||
| 344 | * before reading the corresponding block-table entry. */ | ||
| 345 | int32_t | ||
| 346 | 3 | libmpq__file_size_packed( | |
| 347 | mpq_archive_s *mpq_archive, uint32_t file_number, libmpq__off_t *packed_size | ||
| 348 | ) | ||
| 349 | { | ||
| 350 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 2 times.
|
3 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 351 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 352 | } | ||
| 353 | |||
| 354 | 2 | *packed_size = | |
| 355 | 2 | mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].packed_size; | |
| 356 | |||
| 357 | 2 | return LIBMPQ_SUCCESS; | |
| 358 | } | ||
| 359 | |||
| 360 | /* Return the unpacked size of a file entry by block-table number. | ||
| 361 | * Invalid compact file numbers are rejected before any archive metadata is | ||
| 362 | * accessed. */ | ||
| 363 | int32_t | ||
| 364 | 606 | libmpq__file_size_unpacked( | |
| 365 | mpq_archive_s *mpq_archive, uint32_t file_number, libmpq__off_t *unpacked_size | ||
| 366 | ) | ||
| 367 | { | ||
| 368 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 605 times.
|
606 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 369 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 370 | } | ||
| 371 | |||
| 372 | 605 | *unpacked_size = | |
| 373 | 605 | mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].unpacked_size; | |
| 374 | |||
| 375 | 605 | return LIBMPQ_SUCCESS; | |
| 376 | } | ||
| 377 | |||
| 378 | /* Return the file data offset relative to the start of the archive. | ||
| 379 | * MPQ v2 high offset words are combined with the legacy low word to produce | ||
| 380 | * the complete offset visible through the public API. */ | ||
| 381 | int32_t | ||
| 382 | 576 | libmpq__file_offset(mpq_archive_s *mpq_archive, uint32_t file_number, libmpq__off_t *offset) | |
| 383 | { | ||
| 384 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 575 times.
|
576 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 385 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 386 | } | ||
| 387 | |||
| 388 | 575 | *offset = mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].offset + | |
| 389 | 575 | (((long long)mpq_archive | |
| 390 | 575 | ->mpq_block_ex[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 391 | 575 | .offset_high) | |
| 392 | 575 | << 32); | |
| 393 | |||
| 394 | 575 | return LIBMPQ_SUCCESS; | |
| 395 | } | ||
| 396 | |||
| 397 | /* Return the number of blocks needed to store the selected file. | ||
| 398 | * The reader distinguishes single-unit files from sectorized entries and | ||
| 399 | * applies the archive sector size for the latter. */ | ||
| 400 | int32_t | ||
| 401 | 578 | libmpq__file_blocks(mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t *blocks) | |
| 402 | { | ||
| 403 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 577 times.
|
578 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 404 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 405 | } | ||
| 406 | |||
| 407 | 577 | *blocks = libmpq__reader_count_file_blocks(mpq_archive, file_number); | |
| 408 | |||
| 409 | 577 | return LIBMPQ_SUCCESS; | |
| 410 | } | ||
| 411 | |||
| 412 | /* Report whether the selected file entry has the MPQ encrypted flag set. | ||
| 413 | * The result is normalized to the library's boolean convention after lookup. */ | ||
| 414 | int32_t | ||
| 415 | 946 | libmpq__file_encrypted(mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t *encrypted) | |
| 416 | { | ||
| 417 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 945 times.
|
946 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 418 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 419 | } | ||
| 420 | |||
| 421 | 945 | *encrypted = | |
| 422 | 945 | (mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags & | |
| 423 | LIBMPQ_FLAG_ENCRYPTED) != 0 | ||
| 424 | ? TRUE | ||
| 425 | 945 | : FALSE; | |
| 426 | |||
| 427 | 945 | return LIBMPQ_SUCCESS; | |
| 428 | } | ||
| 429 | |||
| 430 | /* Report whether the selected file entry has any MPQ compression flags set. | ||
| 431 | * This reports Blizzard multi-compression metadata, including its per-sector | ||
| 432 | * codec mask, rather than treating standalone PKWARE as multi-compressed. */ | ||
| 433 | int32_t | ||
| 434 | 948 | libmpq__file_compressed(mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t *compressed) | |
| 435 | { | ||
| 436 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 947 times.
|
948 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 437 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 438 | } | ||
| 439 | |||
| 440 | 947 | *compressed = | |
| 441 | 947 | (mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags & | |
| 442 | LIBMPQ_FLAG_COMPRESS_MULTI) != 0 | ||
| 443 | ? TRUE | ||
| 444 | 947 | : FALSE; | |
| 445 | |||
| 446 | 947 | return LIBMPQ_SUCCESS; | |
| 447 | } | ||
| 448 | |||
| 449 | /* Report whether the selected file entry uses PKWARE implosion. | ||
| 450 | * The flag query covers standalone implode storage as represented in the MPQ | ||
| 451 | * block entry and returns a normalized boolean result. */ | ||
| 452 | int32_t | ||
| 453 | 945 | libmpq__file_imploded(mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t *imploded) | |
| 454 | { | ||
| 455 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 944 times.
|
945 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 456 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 457 | } | ||
| 458 | |||
| 459 | 944 | *imploded = | |
| 460 | 944 | (mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags & | |
| 461 | LIBMPQ_FLAG_COMPRESS_PKZIP) != 0 | ||
| 462 | ? TRUE | ||
| 463 | 944 | : FALSE; | |
| 464 | |||
| 465 | 944 | return LIBMPQ_SUCCESS; | |
| 466 | } | ||
| 467 | |||
| 468 | /* Calculate the three Storm hashes used to identify an MPQ file name. | ||
| 469 | * Each output corresponds to a distinct hash-table phase used during MPQ | ||
| 470 | * name lookup and collision probing. */ | ||
| 471 | void | ||
| 472 | 4899 | libmpq__file_hash(const char *filename, uint32_t *hash1, uint32_t *hash2, uint32_t *hash3) | |
| 473 | { | ||
| 474 | 4899 | *hash1 = libmpq__crypto_hash_string(filename, 0x0); | |
| 475 | 4899 | *hash2 = libmpq__crypto_hash_string(filename, 0x100); | |
| 476 | 4899 | *hash3 = libmpq__crypto_hash_string(filename, 0x200); | |
| 477 | 4899 | } | |
| 478 | |||
| 479 | /* Resolve a precomputed MPQ file-name hash to a public file number. | ||
| 480 | * The first hash selects a slot and linear probing continues until the stored | ||
| 481 | * pair matches or the table wraps without finding the file. */ | ||
| 482 | int32_t | ||
| 483 | 577 | libmpq__file_number_from_hash( | |
| 484 | mpq_archive_s *mpq_archive, uint32_t hash1, uint32_t hash2, uint32_t hash3, uint32_t *number | ||
| 485 | ) | ||
| 486 | { | ||
| 487 | |||
| 488 | /* Hash table probe state and archive hash-table size. */ | ||
| 489 | uint32_t i; | ||
| 490 | uint32_t ht_count; | ||
| 491 | uint32_t block_table_index; | ||
| 492 | |||
| 493 | 577 | ht_count = mpq_archive->mpq_header.hash_table_count; | |
| 494 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 577 times.
|
577 | if (ht_count == 0) { |
| 495 | ✗ | return LIBMPQ_ERROR_EXIST; | |
| 496 | } | ||
| 497 | |||
| 498 | 577 | hash1 %= ht_count; | |
| 499 | |||
| 500 | /* The first hash selects the initial probe slot; collisions use linear probing. */ | ||
| 501 |
2/2✓ Branch 0 taken 697 times.
✓ Branch 1 taken 2 times.
|
699 | for (i = hash1; mpq_archive->mpq_hash[i].block_table_index != LIBMPQ_HASH_FREE; |
| 502 | 122 | i = (i + 1) % ht_count) { | |
| 503 |
3/4✓ Branch 0 taken 575 times.
✓ Branch 1 taken 122 times.
✓ Branch 2 taken 575 times.
✗ Branch 3 not taken.
|
697 | if (mpq_archive->mpq_hash[i].hash_a == hash2 && mpq_archive->mpq_hash[i].hash_b == hash3) { |
| 504 | 575 | block_table_index = mpq_archive->mpq_hash[i].block_table_index; | |
| 505 |
1/2✓ Branch 0 taken 575 times.
✗ Branch 1 not taken.
|
575 | if (block_table_index >= mpq_archive->mpq_header.block_table_count || |
| 506 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 575 times.
|
575 | (mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_EXISTS) == 0) { |
| 507 | ✗ | return LIBMPQ_ERROR_FORMAT; | |
| 508 | } | ||
| 509 | 575 | *number = block_table_index - mpq_archive->mpq_map[block_table_index].block_table_diff; | |
| 510 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 575 times.
|
575 | if (*number >= mpq_archive->files) { |
| 511 | ✗ | return LIBMPQ_ERROR_FORMAT; | |
| 512 | } | ||
| 513 | |||
| 514 | 575 | return LIBMPQ_SUCCESS; | |
| 515 | } | ||
| 516 | |||
| 517 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 122 times.
|
122 | if ((i + 1) % ht_count == hash1) { |
| 518 | ✗ | break; | |
| 519 | } | ||
| 520 | } | ||
| 521 | |||
| 522 | 2 | return LIBMPQ_ERROR_EXIST; | |
| 523 | } | ||
| 524 | |||
| 525 | /* Resolve an MPQ file name to its block-table number through the hash table. | ||
| 526 | * The name is hashed with all three Storm phases before the collision-aware | ||
| 527 | * lookup is delegated to the precomputed-hash helper. */ | ||
| 528 | int32_t | ||
| 529 | 576 | libmpq__file_number(mpq_archive_s *mpq_archive, const char *filename, uint32_t *number) | |
| 530 | { | ||
| 531 | uint32_t hash1; | ||
| 532 | uint32_t hash2; | ||
| 533 | uint32_t hash3; | ||
| 534 | |||
| 535 | 576 | libmpq__file_hash(filename, &hash1, &hash2, &hash3); | |
| 536 | 576 | return libmpq__file_number_from_hash(mpq_archive, hash1, hash2, hash3, number); | |
| 537 | } | ||
| 538 | |||
| 539 | /* Read a complete file by opening its block offset table and copying each block. | ||
| 540 | * The output buffer must hold the complete unpacked file, and cached offset | ||
| 541 | * state is closed on both successful and failed block reads. */ | ||
| 542 | int32_t | ||
| 543 | 574 | libmpq__file_read( | |
| 544 | mpq_archive_s *mpq_archive, uint32_t file_number, uint8_t *out_buf, libmpq__off_t out_size, | ||
| 545 | libmpq__off_t *transferred | ||
| 546 | ) | ||
| 547 | { | ||
| 548 | |||
| 549 | /* Block loop state and total bytes transferred to the caller. */ | ||
| 550 | uint32_t i; | ||
| 551 | 574 | uint32_t blocks = 0; | |
| 552 | 574 | int32_t result = 0; | |
| 553 | 574 | libmpq__off_t file_offset = 0; | |
| 554 | 574 | libmpq__off_t unpacked_size = 0; | |
| 555 | 574 | libmpq__off_t transferred_block = 0; | |
| 556 | 574 | libmpq__off_t transferred_total = 0; | |
| 557 | |||
| 558 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 573 times.
|
574 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 559 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 560 | } | ||
| 561 | |||
| 562 | 573 | libmpq__file_size_unpacked(mpq_archive, file_number, &unpacked_size); | |
| 563 | |||
| 564 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 572 times.
|
573 | if (unpacked_size > out_size) { |
| 565 | 1 | return LIBMPQ_ERROR_SIZE; | |
| 566 | } | ||
| 567 | |||
| 568 | 572 | libmpq__file_offset(mpq_archive, file_number, &file_offset); | |
| 569 | 572 | libmpq__file_blocks(mpq_archive, file_number, &blocks); | |
| 570 | |||
| 571 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 571 times.
|
572 | if ((result = libmpq__block_open_offset(mpq_archive, file_number)) < 0) { |
| 572 | 1 | return result; | |
| 573 | } | ||
| 574 | |||
| 575 | /* Read each block into its exact destination slice and maintain one total. */ | ||
| 576 |
2/2✓ Branch 0 taken 942 times.
✓ Branch 1 taken 571 times.
|
1513 | for (i = 0; i < blocks; i++) { |
| 577 | 942 | unpacked_size = 0; | |
| 578 | |||
| 579 | 942 | libmpq__block_size_unpacked(mpq_archive, file_number, i, &unpacked_size); | |
| 580 | |||
| 581 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 942 times.
|
942 | if ((result = libmpq__block_read( |
| 582 | mpq_archive, file_number, i, out_buf + transferred_total, unpacked_size, | ||
| 583 | &transferred_block | ||
| 584 | )) < 0) { | ||
| 585 | ✗ | libmpq__block_close_offset(mpq_archive, file_number); | |
| 586 | ✗ | return result; | |
| 587 | } | ||
| 588 | |||
| 589 | 942 | transferred_total += transferred_block; | |
| 590 | } | ||
| 591 | |||
| 592 | 571 | libmpq__block_close_offset(mpq_archive, file_number); | |
| 593 | |||
| 594 |
1/2✓ Branch 0 taken 571 times.
✗ Branch 1 not taken.
|
571 | if (transferred != NULL) { |
| 595 | 571 | *transferred = transferred_total; | |
| 596 | } | ||
| 597 | |||
| 598 | 571 | return LIBMPQ_SUCCESS; | |
| 599 | } | ||
| 600 | |||
| 601 | /* Open a file entry and cache its packed block offset table for block operations. | ||
| 602 | * Compressed entries load and decrypt their serialized offsets, while raw or | ||
| 603 | * single-unit entries receive synthesized offsets from block metadata. */ | ||
| 604 | int32_t | ||
| 605 | 576 | libmpq__block_open_offset(mpq_archive_s *mpq_archive, uint32_t file_number) | |
| 606 | { | ||
| 607 | |||
| 608 | /* Packed block table state, file seed and read status. */ | ||
| 609 | uint32_t blocks; | ||
| 610 | uint32_t i; | ||
| 611 | uint32_t block_table_index; | ||
| 612 | uint32_t packed_offset_count; | ||
| 613 | uint32_t packed_size; | ||
| 614 | 576 | int32_t result = 0; | |
| 615 | 576 | uint8_t *packed_data = NULL; | |
| 616 | |||
| 617 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 575 times.
|
576 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 618 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 619 | } | ||
| 620 | |||
| 621 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 574 times.
|
575 | if (mpq_archive->mpq_file[file_number]) { |
| 622 | |||
| 623 | /* Nested callers share the cached offsets through a reference count. */ | ||
| 624 | 1 | mpq_archive->mpq_file[file_number]->open_count++; | |
| 625 | 1 | return LIBMPQ_SUCCESS; | |
| 626 | } | ||
| 627 | |||
| 628 | 574 | block_table_index = mpq_archive->mpq_map[file_number].block_table_indices; | |
| 629 | 574 | blocks = libmpq__reader_count_file_blocks(mpq_archive, file_number); | |
| 630 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 574 times.
|
574 | if (blocks > UINT32_MAX / sizeof(uint32_t) - 2U) { |
| 631 | ✗ | return LIBMPQ_ERROR_FORMAT; | |
| 632 | } | ||
| 633 | 574 | packed_offset_count = blocks + 1; | |
| 634 | 574 | packed_size = sizeof(uint32_t) * packed_offset_count; | |
| 635 | |||
| 636 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 574 times.
|
574 | if ((mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_CRC) != 0) { |
| 637 | ✗ | packed_size += sizeof(uint32_t); | |
| 638 | } | ||
| 639 | |||
| 640 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 574 times.
|
574 | if ((mpq_archive->mpq_file[file_number] = calloc(1, sizeof(mpq_file_s))) == NULL) { |
| 641 | ✗ | result = LIBMPQ_ERROR_MALLOC; | |
| 642 | ✗ | goto error; | |
| 643 | } | ||
| 644 | |||
| 645 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 574 times.
|
574 | if ((mpq_archive->mpq_file[file_number]->packed_offset = calloc(1, packed_size)) == NULL) { |
| 646 | ✗ | result = LIBMPQ_ERROR_MALLOC; | |
| 647 | ✗ | goto error; | |
| 648 | } | ||
| 649 | |||
| 650 | 574 | mpq_archive->mpq_file[file_number]->packed_offset_count = packed_offset_count; | |
| 651 | 574 | mpq_archive->mpq_file[file_number]->open_count = 1; | |
| 652 | |||
| 653 | /* Compressed multi-sector files carry serialized offsets before their first | ||
| 654 | * payload, so load that table before any block can be read. */ | ||
| 655 |
2/2✓ Branch 0 taken 440 times.
✓ Branch 1 taken 134 times.
|
574 | if ((mpq_archive->mpq_block[block_table_index].flags & |
| 656 | 440 | (LIBMPQ_FLAG_COMPRESSED | LIBMPQ_FLAG_COMPRESS_PKZIP)) != 0 && | |
| 657 |
2/2✓ Branch 0 taken 422 times.
✓ Branch 1 taken 18 times.
|
440 | (mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_SINGLE) == 0) { |
| 658 |
2/4✓ Branch 0 taken 422 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 422 times.
|
844 | if (mpq_archive->mpq_block[block_table_index].packed_size < packed_size || |
| 659 | 422 | libmpq__validate_payload_range(mpq_archive, block_table_index, 0, packed_size) < 0) { | |
| 660 | ✗ | result = LIBMPQ_ERROR_FORMAT; | |
| 661 | ✗ | goto error; | |
| 662 | } | ||
| 663 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 422 times.
|
422 | if (fseeko( |
| 664 | mpq_archive->fp, | ||
| 665 | 422 | mpq_archive->mpq_block[block_table_index].offset + | |
| 666 | 422 | (((long long)mpq_archive->mpq_block_ex[block_table_index].offset_high) << 32) + | |
| 667 | 422 | mpq_archive->archive_offset, | |
| 668 | SEEK_SET | ||
| 669 | ) < 0) { | ||
| 670 | ✗ | result = LIBMPQ_ERROR_SEEK; | |
| 671 | ✗ | goto error; | |
| 672 | } | ||
| 673 | |||
| 674 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 422 times.
|
422 | if ((packed_data = malloc(packed_size)) == NULL) { |
| 675 | ✗ | result = LIBMPQ_ERROR_MALLOC; | |
| 676 | ✗ | goto error; | |
| 677 | } | ||
| 678 | |||
| 679 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 422 times.
|
422 | if (fread(packed_data, 1, packed_size, mpq_archive->fp) != packed_size) { |
| 680 | ✗ | result = LIBMPQ_ERROR_READ; | |
| 681 | ✗ | goto error; | |
| 682 | } | ||
| 683 | |||
| 684 | /* Some protected archives omit the encrypted flag; a wrong first offset exposes that. */ | ||
| 685 |
2/2✓ Branch 1 taken 111 times.
✓ Branch 2 taken 311 times.
|
422 | if (libmpq__load_le32(packed_data) != packed_size && |
| 686 |
1/2✓ Branch 1 taken 111 times.
✗ Branch 2 not taken.
|
111 | libmpq__load_le32(packed_data) != packed_size + 4) { |
| 687 | 111 | mpq_archive->mpq_block[block_table_index].flags |= LIBMPQ_FLAG_ENCRYPTED; | |
| 688 | } | ||
| 689 | |||
| 690 | /* The packed offset table uses seed - 1, so recover the file seed first. */ | ||
| 691 |
2/2✓ Branch 0 taken 111 times.
✓ Branch 1 taken 311 times.
|
422 | if (mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_ENCRYPTED) { |
| 692 | uint32_t seed; | ||
| 693 | |||
| 694 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 111 times.
|
111 | if (libmpq__crypto_derive_block_table_seed( |
| 695 | packed_data, packed_size, mpq_archive->block_size, &seed | ||
| 696 | ) < 0) { | ||
| 697 | ✗ | result = LIBMPQ_ERROR_DECRYPT; | |
| 698 | ✗ | goto error; | |
| 699 | } | ||
| 700 | 111 | mpq_archive->mpq_file[file_number]->seed = seed; | |
| 701 | 111 | mpq_archive->mpq_file[file_number]->seed_known = TRUE; | |
| 702 | |||
| 703 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 111 times.
|
111 | if (libmpq__crypto_decrypt_block( |
| 704 | 111 | packed_data, packed_size, mpq_archive->mpq_file[file_number]->seed - 1 | |
| 705 | ) < 0) { | ||
| 706 | ✗ | result = LIBMPQ_ERROR_DECRYPT; | |
| 707 | ✗ | goto error; | |
| 708 | } | ||
| 709 | |||
| 710 | /* A valid decrypted table starts with its own byte size. */ | ||
| 711 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 111 times.
|
111 | if (libmpq__load_le32(packed_data) != packed_size) { |
| 712 | ✗ | result = LIBMPQ_ERROR_DECRYPT; | |
| 713 | ✗ | goto error; | |
| 714 | } | ||
| 715 | } | ||
| 716 | |||
| 717 | 422 | libmpq__reader_decode_uint32_table( | |
| 718 | 422 | mpq_archive->mpq_file[file_number]->packed_offset, packed_data, | |
| 719 | packed_size / sizeof(uint32_t) | ||
| 720 | ); | ||
| 721 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 422 times.
|
422 | if (mpq_archive->mpq_file[file_number]->packed_offset[0] != packed_size) { |
| 722 | ✗ | result = LIBMPQ_ERROR_FORMAT; | |
| 723 | ✗ | goto error; | |
| 724 | } | ||
| 725 |
2/2✓ Branch 0 taken 736 times.
✓ Branch 1 taken 421 times.
|
1157 | for (i = 1; i < packed_offset_count; i++) { |
| 726 | 736 | if (mpq_archive->mpq_file[file_number]->packed_offset[i] < | |
| 727 |
1/2✓ Branch 0 taken 736 times.
✗ Branch 1 not taken.
|
736 | mpq_archive->mpq_file[file_number]->packed_offset[i - 1] || |
| 728 | 736 | mpq_archive->mpq_file[file_number]->packed_offset[i] > | |
| 729 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 735 times.
|
736 | mpq_archive->mpq_block[block_table_index].packed_size) { |
| 730 | 1 | result = LIBMPQ_ERROR_FORMAT; | |
| 731 | 1 | goto error; | |
| 732 | } | ||
| 733 | } | ||
| 734 | 421 | free(packed_data); | |
| 735 | 421 | packed_data = NULL; | |
| 736 | } else { | ||
| 737 | |||
| 738 | /* Raw sectorized files derive offsets directly from their fixed sector size. */ | ||
| 739 |
2/2✓ Branch 0 taken 96 times.
✓ Branch 1 taken 56 times.
|
152 | if ((mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags & |
| 740 | LIBMPQ_FLAG_SINGLE) == 0) { | ||
| 741 | |||
| 742 | /* Synthesize offsets for uncompressed multi-sector files. */ | ||
| 743 |
2/2✓ Branch 0 taken 250 times.
✓ Branch 1 taken 96 times.
|
346 | for (i = 0; i < packed_offset_count; i++) { |
| 744 |
2/2✓ Branch 0 taken 96 times.
✓ Branch 1 taken 154 times.
|
250 | if (i == blocks) { |
| 745 | 96 | mpq_archive->mpq_file[file_number]->packed_offset[i] = | |
| 746 | mpq_archive | ||
| 747 | 96 | ->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 748 | 96 | .unpacked_size; | |
| 749 | } else { | ||
| 750 | 154 | mpq_archive->mpq_file[file_number]->packed_offset[i] = | |
| 751 | 154 | i * mpq_archive->block_size; | |
| 752 | } | ||
| 753 | } | ||
| 754 | } else { | ||
| 755 | 56 | mpq_archive->mpq_file[file_number]->packed_offset[0] = 0; | |
| 756 | 56 | mpq_archive->mpq_file[file_number]->packed_offset[1] = | |
| 757 | 56 | mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 758 | 56 | .packed_size; | |
| 759 | } | ||
| 760 | } | ||
| 761 | |||
| 762 | /* Raw encrypted files have no encrypted offset table from which to derive a seed. */ | ||
| 763 |
2/2✓ Branch 0 taken 49 times.
✓ Branch 1 taken 524 times.
|
573 | if ((mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags & |
| 764 | (LIBMPQ_FLAG_ENCRYPTED | LIBMPQ_FLAG_COMPRESSED)) == LIBMPQ_FLAG_ENCRYPTED) { | ||
| 765 | uint8_t first_block[8]; | ||
| 766 | uint32_t first_offset; | ||
| 767 | uint32_t second_offset; | ||
| 768 | uint32_t first_size; | ||
| 769 | uint32_t seed; | ||
| 770 | |||
| 771 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 49 times.
|
49 | if (packed_offset_count < 2) { |
| 772 | ✗ | result = LIBMPQ_ERROR_FORMAT; | |
| 773 | ✗ | goto error; | |
| 774 | } | ||
| 775 | |||
| 776 | 49 | first_offset = mpq_archive->mpq_file[file_number]->packed_offset[0]; | |
| 777 | 49 | second_offset = mpq_archive->mpq_file[file_number]->packed_offset[1]; | |
| 778 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 49 times.
|
49 | if (second_offset < first_offset) { |
| 779 | ✗ | result = LIBMPQ_ERROR_FORMAT; | |
| 780 | ✗ | goto error; | |
| 781 | } | ||
| 782 | |||
| 783 | 49 | first_size = second_offset - first_offset; | |
| 784 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 49 times.
|
49 | if (first_size < sizeof(first_block)) { |
| 785 | ✗ | result = LIBMPQ_ERROR_DECRYPT; | |
| 786 | ✗ | goto error; | |
| 787 | } | ||
| 788 | |||
| 789 |
1/2✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
|
49 | if (fseeko( |
| 790 | mpq_archive->fp, | ||
| 791 | 49 | mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 792 | 49 | .offset + | |
| 793 | 49 | (((long long)mpq_archive | |
| 794 | 49 | ->mpq_block_ex[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 795 | 49 | .offset_high) | |
| 796 | 49 | << 32) + | |
| 797 | 49 | mpq_archive->archive_offset, | |
| 798 | SEEK_SET | ||
| 799 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 49 times.
|
49 | ) < 0 || |
| 800 | 49 | fread(first_block, 1, sizeof(first_block), mpq_archive->fp) != sizeof(first_block)) { | |
| 801 | ✗ | result = LIBMPQ_ERROR_READ; | |
| 802 | ✗ | goto error; | |
| 803 | } | ||
| 804 | |||
| 805 | /* Raw encrypted payloads require signature-based key recovery instead. */ | ||
| 806 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 49 times.
|
49 | if (libmpq__crypto_detect_file_key( |
| 807 | first_block, sizeof(first_block), | ||
| 808 | 49 | mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 809 | .unpacked_size, | ||
| 810 | &seed | ||
| 811 | ) < 0) { | ||
| 812 | ✗ | result = LIBMPQ_ERROR_DECRYPT; | |
| 813 | ✗ | goto error; | |
| 814 | } | ||
| 815 | |||
| 816 | 49 | mpq_archive->mpq_file[file_number]->seed = seed; | |
| 817 | 49 | mpq_archive->mpq_file[file_number]->seed_known = TRUE; | |
| 818 | } | ||
| 819 | |||
| 820 | 573 | return LIBMPQ_SUCCESS; | |
| 821 | |||
| 822 | 1 | error: | |
| 823 | |||
| 824 | 1 | free(packed_data); | |
| 825 | |||
| 826 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | if (mpq_archive->mpq_file[file_number] != NULL) { |
| 827 | 1 | free(mpq_archive->mpq_file[file_number]->packed_offset); | |
| 828 | 1 | free(mpq_archive->mpq_file[file_number]); | |
| 829 | 1 | mpq_archive->mpq_file[file_number] = NULL; | |
| 830 | } | ||
| 831 | |||
| 832 | 1 | return result; | |
| 833 | } | ||
| 834 | |||
| 835 | /* Release a cached block offset table when the last user closes it. | ||
| 836 | * Reference counting permits nested block operations while ensuring the cache | ||
| 837 | * is freed only after the final matching close. */ | ||
| 838 | int32_t | ||
| 839 | 575 | libmpq__block_close_offset(mpq_archive_s *mpq_archive, uint32_t file_number) | |
| 840 | { | ||
| 841 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 575 times.
|
575 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 842 | ✗ | return LIBMPQ_ERROR_EXIST; | |
| 843 | } | ||
| 844 | |||
| 845 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 574 times.
|
575 | if (mpq_archive->mpq_file[file_number] == NULL) { |
| 846 | 1 | return LIBMPQ_ERROR_OPEN; | |
| 847 | } | ||
| 848 | |||
| 849 | 574 | mpq_archive->mpq_file[file_number]->open_count--; | |
| 850 | |||
| 851 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 573 times.
|
574 | if (mpq_archive->mpq_file[file_number]->open_count != 0) { |
| 852 | |||
| 853 | /* Keep the cache alive until every matching open operation closes. */ | ||
| 854 | 1 | return LIBMPQ_SUCCESS; | |
| 855 | } | ||
| 856 | |||
| 857 | 573 | free(mpq_archive->mpq_file[file_number]->packed_offset); | |
| 858 | 573 | free(mpq_archive->mpq_file[file_number]); | |
| 859 | |||
| 860 | 573 | mpq_archive->mpq_file[file_number] = NULL; | |
| 861 | |||
| 862 | 573 | return LIBMPQ_SUCCESS; | |
| 863 | } | ||
| 864 | |||
| 865 | /* Return the unpacked size for one block of an opened file entry. | ||
| 866 | * Full sectors use the archive sector size, while the final sector is reduced | ||
| 867 | * to the remaining file bytes and single-unit files use their full size. */ | ||
| 868 | int32_t | ||
| 869 | 1890 | libmpq__block_size_unpacked( | |
| 870 | mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number, | ||
| 871 | libmpq__off_t *unpacked_size | ||
| 872 | ) | ||
| 873 | { | ||
| 874 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1889 times.
|
1890 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 875 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 876 | } | ||
| 877 | |||
| 878 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1888 times.
|
1889 | if (libmpq__reader_validate_block_number(mpq_archive, file_number, block_number) < 0) { |
| 879 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 880 | } | ||
| 881 | |||
| 882 |
1/2✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
|
1888 | if (mpq_archive->mpq_file[file_number] == NULL || |
| 883 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1888 times.
|
1888 | mpq_archive->mpq_file[file_number]->packed_offset == NULL) { |
| 884 | ✗ | return LIBMPQ_ERROR_OPEN; | |
| 885 | } | ||
| 886 | |||
| 887 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1888 times.
|
1888 | if (mpq_archive->mpq_file[file_number]->packed_offset_count <= block_number + 1) { |
| 888 | ✗ | return LIBMPQ_ERROR_EXIST; | |
| 889 | } | ||
| 890 | |||
| 891 |
2/2✓ Branch 0 taken 112 times.
✓ Branch 1 taken 1776 times.
|
1888 | if ((mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags & |
| 892 | LIBMPQ_FLAG_SINGLE) != 0) { | ||
| 893 | |||
| 894 | /* A single-unit entry has one logical block containing the whole file. */ | ||
| 895 | 112 | *unpacked_size = | |
| 896 | 112 | mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 897 | 112 | .unpacked_size; | |
| 898 | } | ||
| 899 | |||
| 900 |
2/2✓ Branch 0 taken 1776 times.
✓ Branch 1 taken 112 times.
|
1888 | if ((mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags & |
| 901 | LIBMPQ_FLAG_SINGLE) == 0) { | ||
| 902 | |||
| 903 | /* Every non-final sector is full-sized; only the tail uses a remainder. */ | ||
| 904 |
2/2✓ Branch 1 taken 756 times.
✓ Branch 2 taken 1020 times.
|
1776 | if (block_number < libmpq__reader_count_file_blocks(mpq_archive, file_number) - 1) { |
| 905 | 756 | *unpacked_size = mpq_archive->block_size; | |
| 906 | } else { | ||
| 907 | 1020 | *unpacked_size = | |
| 908 | 1020 | mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 909 | 1020 | .unpacked_size - | |
| 910 | 1020 | mpq_archive->block_size * block_number; | |
| 911 | } | ||
| 912 | } | ||
| 913 | |||
| 914 | 1888 | return LIBMPQ_SUCCESS; | |
| 915 | } | ||
| 916 | |||
| 917 | /* Read, decrypt and decompress one block from an opened file entry. | ||
| 918 | * The routine computes packed bounds, applies per-block encryption, selects | ||
| 919 | * raw or codec output, and reports the exact unpacked byte count. */ | ||
| 920 | int32_t | ||
| 921 | 945 | libmpq__block_read( | |
| 922 | mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number, uint8_t *out_buf, | ||
| 923 | libmpq__off_t out_size, libmpq__off_t *transferred | ||
| 924 | ) | ||
| 925 | { | ||
| 926 | |||
| 927 | /* Packed input buffer, size bookkeeping and block decryption state. */ | ||
| 928 | uint8_t *in_buf; | ||
| 929 | 945 | uint32_t seed = 0; | |
| 930 | 945 | uint32_t encrypted = 0; | |
| 931 | 945 | uint32_t compressed = 0; | |
| 932 | 945 | uint32_t imploded = 0; | |
| 933 | 945 | int32_t tb = 0; | |
| 934 | 945 | uint8_t use_out_buf = FALSE; | |
| 935 | 945 | libmpq__off_t block_offset = 0; | |
| 936 | 945 | libmpq__off_t in_size = 0; | |
| 937 | 945 | libmpq__off_t unpacked_size = 0; | |
| 938 | |||
| 939 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 945 times.
|
945 | if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) { |
| 940 | ✗ | return LIBMPQ_ERROR_EXIST; | |
| 941 | } | ||
| 942 | |||
| 943 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 944 times.
|
945 | if (libmpq__reader_validate_block_number(mpq_archive, file_number, block_number) < 0) { |
| 944 | 1 | return LIBMPQ_ERROR_EXIST; | |
| 945 | } | ||
| 946 | |||
| 947 |
1/2✓ Branch 0 taken 944 times.
✗ Branch 1 not taken.
|
944 | if (mpq_archive->mpq_file[file_number] == NULL || |
| 948 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 944 times.
|
944 | mpq_archive->mpq_file[file_number]->packed_offset == NULL) { |
| 949 | ✗ | return LIBMPQ_ERROR_OPEN; | |
| 950 | } | ||
| 951 | |||
| 952 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 944 times.
|
944 | if (mpq_archive->mpq_file[file_number]->packed_offset_count <= block_number + 1) { |
| 953 | ✗ | return LIBMPQ_ERROR_EXIST; | |
| 954 | } | ||
| 955 | |||
| 956 | 944 | libmpq__block_size_unpacked(mpq_archive, file_number, block_number, &unpacked_size); | |
| 957 | |||
| 958 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 943 times.
|
944 | if (unpacked_size > out_size) { |
| 959 | 1 | return LIBMPQ_ERROR_SIZE; | |
| 960 | } | ||
| 961 | |||
| 962 | /* Compute the absolute payload position from archive, file, and block offsets. | ||
| 963 | * The stored block offset is relative to the file payload start, not the | ||
| 964 | * beginning of the archive file. */ | ||
| 965 | 943 | if (mpq_archive->mpq_file[file_number]->packed_offset[block_number + 1] < | |
| 966 |
1/2✓ Branch 0 taken 943 times.
✗ Branch 1 not taken.
|
943 | mpq_archive->mpq_file[file_number]->packed_offset[block_number] || |
| 967 | 943 | mpq_archive->mpq_file[file_number]->packed_offset[block_number + 1] > | |
| 968 | 943 | mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 969 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 943 times.
|
943 | .packed_size) { |
| 970 | ✗ | return LIBMPQ_ERROR_FORMAT; | |
| 971 | } | ||
| 972 | 943 | in_size = mpq_archive->mpq_file[file_number]->packed_offset[block_number + 1] - | |
| 973 | 943 | mpq_archive->mpq_file[file_number]->packed_offset[block_number]; | |
| 974 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 943 times.
|
943 | if (libmpq__validate_payload_range( |
| 975 | 943 | mpq_archive, mpq_archive->mpq_map[file_number].block_table_indices, | |
| 976 | 943 | mpq_archive->mpq_file[file_number]->packed_offset[block_number], (uint64_t)in_size | |
| 977 | ) < 0) { | ||
| 978 | ✗ | return LIBMPQ_ERROR_READ; | |
| 979 | } | ||
| 980 | 943 | block_offset = | |
| 981 | 943 | mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].offset + | |
| 982 | 943 | (((long long)mpq_archive | |
| 983 | 943 | ->mpq_block_ex[mpq_archive->mpq_map[file_number].block_table_indices] | |
| 984 | 943 | .offset_high) | |
| 985 | 943 | << 32) + | |
| 986 | 943 | mpq_archive->mpq_file[file_number]->packed_offset[block_number]; | |
| 987 | |||
| 988 | 943 | libmpq__file_encrypted(mpq_archive, file_number, &encrypted); | |
| 989 | 943 | libmpq__file_compressed(mpq_archive, file_number, &compressed); | |
| 990 | 943 | libmpq__file_imploded(mpq_archive, file_number, &imploded); | |
| 991 | |||
| 992 | /* Raw unencrypted blocks can be read directly into the caller's buffer. */ | ||
| 993 |
7/8✓ Branch 0 taken 657 times.
✓ Branch 1 taken 286 times.
✓ Branch 2 taken 181 times.
✓ Branch 3 taken 476 times.
✓ Branch 4 taken 109 times.
✓ Branch 5 taken 72 times.
✓ Branch 6 taken 109 times.
✗ Branch 7 not taken.
|
943 | use_out_buf = !encrypted && !compressed && !imploded && in_size <= out_size; |
| 994 | |||
| 995 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 943 times.
|
943 | if (fseeko(mpq_archive->fp, block_offset + mpq_archive->archive_offset, SEEK_SET) < 0) { |
| 996 | ✗ | return LIBMPQ_ERROR_SEEK; | |
| 997 | } | ||
| 998 | |||
| 999 |
2/2✓ Branch 0 taken 109 times.
✓ Branch 1 taken 834 times.
|
943 | if (use_out_buf) { |
| 1000 | |||
| 1001 | /* Raw data can bypass a temporary allocation when no transform is needed. */ | ||
| 1002 | 109 | in_buf = out_buf; | |
| 1003 | } else { | ||
| 1004 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 834 times.
|
834 | if ((in_buf = calloc(1, in_size)) == NULL) { |
| 1005 | ✗ | return LIBMPQ_ERROR_MALLOC; | |
| 1006 | } | ||
| 1007 | } | ||
| 1008 | |||
| 1009 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 943 times.
|
943 | if (fread(in_buf, 1, (size_t)in_size, mpq_archive->fp) != (size_t)in_size) { |
| 1010 | ✗ | if (!use_out_buf) { | |
| 1011 | ✗ | free(in_buf); | |
| 1012 | } | ||
| 1013 | ✗ | return LIBMPQ_ERROR_READ; | |
| 1014 | } | ||
| 1015 | |||
| 1016 |
2/2✓ Branch 0 taken 286 times.
✓ Branch 1 taken 657 times.
|
943 | if (encrypted) { |
| 1017 | |||
| 1018 | /* Encrypted blocks use a seed derived from the file and block number. */ | ||
| 1019 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 286 times.
|
286 | if (libmpq__reader_get_block_seed(mpq_archive, file_number, block_number, &seed) < 0) { |
| 1020 | ✗ | if (!use_out_buf) { | |
| 1021 | ✗ | free(in_buf); | |
| 1022 | } | ||
| 1023 | ✗ | return LIBMPQ_ERROR_DECRYPT; | |
| 1024 | } | ||
| 1025 | |||
| 1026 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 286 times.
|
286 | if (libmpq__crypto_decrypt_block(in_buf, (uint32_t)in_size, seed) < 0) { |
| 1027 | ✗ | if (!use_out_buf) { | |
| 1028 | ✗ | free(in_buf); | |
| 1029 | } | ||
| 1030 | ✗ | return LIBMPQ_ERROR_DECRYPT; | |
| 1031 | } | ||
| 1032 | } | ||
| 1033 | |||
| 1034 | /* Blizzard multi-compression blocks declare their exact backend chain in the payload. */ | ||
| 1035 |
2/2✓ Branch 0 taken 613 times.
✓ Branch 1 taken 330 times.
|
943 | if (compressed) { |
| 1036 | |||
| 1037 | /* The payload's leading mask selects and orders its decompression stages. */ | ||
| 1038 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 613 times.
|
613 | if ((tb = libmpq__compression_decompress_block( |
| 1039 | in_buf, in_size, out_buf, out_size, LIBMPQ_FLAG_COMPRESS_MULTI | ||
| 1040 | )) < 0) { | ||
| 1041 | ✗ | if (!use_out_buf) { | |
| 1042 | ✗ | free(in_buf); | |
| 1043 | } | ||
| 1044 | ✗ | return LIBMPQ_ERROR_UNPACK; | |
| 1045 | } | ||
| 1046 | } | ||
| 1047 | |||
| 1048 | /* PKWARE-imploded blocks use the legacy explode decoder. */ | ||
| 1049 |
2/2✓ Branch 0 taken 138 times.
✓ Branch 1 taken 805 times.
|
943 | if (imploded) { |
| 1050 | |||
| 1051 | /* Standalone PKWARE payloads use the legacy decoder without a mask byte. */ | ||
| 1052 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 138 times.
|
138 | if ((tb = libmpq__compression_decompress_block( |
| 1053 | in_buf, in_size, out_buf, out_size, LIBMPQ_FLAG_COMPRESS_PKZIP | ||
| 1054 | )) < 0) { | ||
| 1055 | ✗ | if (!use_out_buf) { | |
| 1056 | ✗ | free(in_buf); | |
| 1057 | } | ||
| 1058 | ✗ | return LIBMPQ_ERROR_UNPACK; | |
| 1059 | } | ||
| 1060 | } | ||
| 1061 | |||
| 1062 |
3/4✓ Branch 0 taken 613 times.
✓ Branch 1 taken 330 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 613 times.
|
943 | if (compressed && imploded) { |
| 1063 | ✗ | if (!use_out_buf) { | |
| 1064 | ✗ | free(in_buf); | |
| 1065 | } | ||
| 1066 | ✗ | return LIBMPQ_ERROR_UNPACK; | |
| 1067 | } | ||
| 1068 | |||
| 1069 |
4/4✓ Branch 0 taken 330 times.
✓ Branch 1 taken 613 times.
✓ Branch 2 taken 192 times.
✓ Branch 3 taken 138 times.
|
943 | if (!compressed && !imploded) { |
| 1070 | |||
| 1071 | /* A raw block is copied only after encrypted and compressed paths are excluded. */ | ||
| 1072 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 192 times.
|
192 | if ((tb = libmpq__compression_decompress_block( |
| 1073 | in_buf, in_size, out_buf, out_size, LIBMPQ_FLAG_COMPRESS_NONE | ||
| 1074 | )) < 0) { | ||
| 1075 | ✗ | if (!use_out_buf) { | |
| 1076 | ✗ | free(in_buf); | |
| 1077 | } | ||
| 1078 | ✗ | return LIBMPQ_ERROR_UNPACK; | |
| 1079 | } | ||
| 1080 | } | ||
| 1081 | |||
| 1082 |
2/2✓ Branch 0 taken 834 times.
✓ Branch 1 taken 109 times.
|
943 | if (!use_out_buf) { |
| 1083 | 834 | free(in_buf); | |
| 1084 | } | ||
| 1085 | |||
| 1086 |
1/2✓ Branch 0 taken 943 times.
✗ Branch 1 not taken.
|
943 | if (transferred != NULL) { |
| 1087 | 943 | *transferred = tb; | |
| 1088 | } | ||
| 1089 | |||
| 1090 | 943 | return LIBMPQ_SUCCESS; | |
| 1091 | } | ||
| 1092 |