GCC Code Coverage Report


Directory: src/
File: src/mpq-reader.c
Date: 2026-10-06 13:47:41
Exec Total Coverage
Lines: 687 805 85.3%
Functions: 26 26 100.0%
Branches: 417 564 73.9%

Line Branch Exec Source
1 /*
2 * mpq-reader.c -- internal archive reader implementation.
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-archive.h"
25 #include "mpq-attributes.h"
26 #include "mpq-block.h"
27 #include "mpq-compression.h"
28 #include "mpq-crypto.h"
29 #include "mpq-endian.h"
30 #include "mpq-file.h"
31 #include "mpq-reader.h"
32 #include "mpq-source.h"
33
34 #include <stdlib.h>
35 #include <string.h>
36 #include <zlib.h>
37
38 /*
39 * Accumulate a serialized range without wrapping.
40 * Callers decide whether zero-length ranges are meaningful.
41 */
42 static int32_t
43 4508 extend_extent(uint64_t offset, uint64_t length, uint64_t *extent)
44 {
45 uint64_t end;
46
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4506 times.
4508 if (offset > UINT64_MAX - length)
47 2 return LIBMPQ_ERROR_FORMAT;
48 4506 end = offset + length;
49
2/2
✓ Branch 0 taken 3633 times.
✓ Branch 1 taken 873 times.
4506 if (end > *extent)
50 3633 *extent = end;
51 4506 return 0;
52 }
53
54 int32_t
55 388 libmpq__archive_required_extent(const mpq_archive_s *archive, uint64_t *size)
56 {
57 uint64_t extent;
58 uint64_t hash;
59 uint64_t block;
60 uint32_t i;
61
1/2
✓ Branch 0 taken 388 times.
✗ Branch 1 not taken.
388 if (size != NULL)
62 388 *size = 0;
63
2/4
✓ Branch 0 taken 388 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 388 times.
388 if (archive == NULL || size == NULL)
64 ✗ return LIBMPQ_ERROR_EXIST;
65
2/4
✓ Branch 0 taken 388 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 388 times.
✗ Branch 3 not taken.
388 if (archive->archive_offset < 0 || archive->mpq_header.version > LIBMPQ_ARCHIVE_VERSION_TWO ||
66
1/2
✓ Branch 0 taken 388 times.
✗ Branch 1 not taken.
388 (archive->mpq_header.block_table_count != 0 &&
67
2/4
✓ Branch 0 taken 388 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 388 times.
388 (archive->mpq_block == NULL || archive->mpq_block_ex == NULL)))
68 ✗ return LIBMPQ_ERROR_FORMAT;
69 388 extent = archive->mpq_header.header_size;
70
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 388 times.
388 if (extent <
71
2/2
✓ Branch 0 taken 105 times.
✓ Branch 1 taken 283 times.
388 LIBMPQ_HEADER_WIRE_SIZE + (archive->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_TWO
72 ? LIBMPQ_HEADER_EX_WIRE_SIZE
73 : 0u))
74 ✗ return LIBMPQ_ERROR_FORMAT;
75 388 hash = archive->mpq_header.hash_table_offset |
76 388 ((uint64_t)archive->mpq_header_ex.hash_table_offset_high << 32);
77 388 block = archive->mpq_header.block_table_offset |
78 388 ((uint64_t)archive->mpq_header_ex.block_table_offset_high << 32);
79
1/2
✓ Branch 0 taken 388 times.
✗ Branch 1 not taken.
388 if (extend_extent(
80 388 hash, (uint64_t)archive->mpq_header.hash_table_count * LIBMPQ_HASH_ENTRY_WIRE_SIZE,
81 &extent
82
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 388 times.
388 ) != 0 ||
83 388 extend_extent(
84 388 block, (uint64_t)archive->mpq_header.block_table_count * LIBMPQ_BLOCK_ENTRY_WIRE_SIZE,
85 &extent
86 ) != 0)
87 ✗ return LIBMPQ_ERROR_FORMAT;
88
4/4
✓ Branch 0 taken 104 times.
✓ Branch 1 taken 284 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 102 times.
492 if (archive->mpq_header_ex.extended_offset != 0 &&
89 104 extend_extent(
90 104 archive->mpq_header_ex.extended_offset,
91 104 (uint64_t)archive->mpq_header.block_table_count * LIBMPQ_BLOCK_EX_ENTRY_WIRE_SIZE,
92 &extent
93 ) != 0)
94 2 return LIBMPQ_ERROR_FORMAT;
95
2/2
✓ Branch 0 taken 19834 times.
✓ Branch 1 taken 386 times.
20220 for (i = 0; i < archive->mpq_header.block_table_count; ++i) {
96 19834 uint64_t offset =
97 19834 archive->mpq_block[i].offset | ((uint64_t)archive->mpq_block_ex[i].offset_high << 32);
98
2/2
✓ Branch 0 taken 3639 times.
✓ Branch 1 taken 16195 times.
19834 if ((archive->mpq_block[i].flags & LIBMPQ_FLAG_EXISTS) &&
99
3/4
✓ Branch 0 taken 3628 times.
✓ Branch 1 taken 11 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3628 times.
7267 archive->mpq_block[i].packed_size != 0 &&
100 3628 extend_extent(offset, archive->mpq_block[i].packed_size, &extent) != 0)
101 ✗ return LIBMPQ_ERROR_FORMAT;
102 }
103 386 *size = extent;
104 386 return 0;
105 }
106
107 int32_t
108 386 libmpq__archive_signature_extent(const mpq_archive_s *archive, uint64_t *size)
109 {
110 uint64_t required;
111 uint64_t extent;
112 int32_t result;
113
1/2
✓ Branch 0 taken 386 times.
✗ Branch 1 not taken.
386 if (size != NULL)
114 386 *size = 0;
115
2/4
✓ Branch 0 taken 386 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 386 times.
386 if (archive == NULL || size == NULL)
116 ✗ return LIBMPQ_ERROR_EXIST;
117 386 result = libmpq__archive_required_extent(archive, &required);
118
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 384 times.
386 if (result != 0)
119 2 return result;
120 768 extent = archive->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_ONE
121 282 ? archive->mpq_header.archive_size
122
2/2
✓ Branch 0 taken 282 times.
✓ Branch 1 taken 102 times.
384 : required;
123
3/4
✓ Branch 0 taken 381 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 381 times.
✗ Branch 3 not taken.
384 if (extent < required || (uint64_t)archive->archive_offset > archive->file_size ||
124
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 378 times.
381 extent > archive->file_size - (uint64_t)archive->archive_offset)
125 6 return LIBMPQ_ERROR_FORMAT;
126 378 *size = extent;
127 378 return 0;
128 }
129
130 /*
131 * Release a cached block offset table when the last user closes it.
132 * Reference counting permits nested block operations while ensuring the cache
133 * is freed only after the final matching close.
134 */
135 int32_t
136 15344 libmpq__reader_offsets_release(mpq_archive_s *mpq_archive, uint32_t file_number)
137 {
138
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 15344 times.
15344 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
139 ✗ return LIBMPQ_ERROR_EXIST;
140 }
141
142
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15344 times.
15344 if (mpq_archive->mpq_file[file_number] == NULL) {
143 ✗ return LIBMPQ_ERROR_OPEN;
144 }
145
146 15344 mpq_archive->mpq_file[file_number]->open_count--;
147
148
2/2
✓ Branch 0 taken 8064 times.
✓ Branch 1 taken 7280 times.
15344 if (mpq_archive->mpq_file[file_number]->open_count != 0) {
149
150 /* Keep the cache alive until every matching open operation closes. */
151 8064 return 0;
152 }
153
154 7280 free(mpq_archive->mpq_file[file_number]->packed_offset);
155 7280 free(mpq_archive->mpq_file[file_number]);
156
157 7280 mpq_archive->mpq_file[file_number] = NULL;
158
159 7280 return 0;
160 }
161
162 /*
163 * Read a complete file by opening its block offset table and copying each block.
164 * The output buffer must hold the complete unpacked file, and cached offset
165 * state is closed on both successful and failed block reads.
166 */
167 int32_t
168 3468 libmpq__reader_file_read(
169 mpq_archive_s *mpq_archive, uint32_t file_number, uint8_t *out_buf, libmpq__off_t out_size,
170 libmpq__off_t *transferred
171 )
172 {
173
174 /* Block loop state and total bytes transferred to the caller. */
175 uint32_t i;
176 3468 uint32_t blocks = 0;
177 3468 int32_t result = 0;
178 3468 libmpq__off_t file_offset = 0;
179 3468 libmpq__off_t expected_size = 0;
180 3468 libmpq__off_t unpacked_size = 0;
181 3468 libmpq__off_t transferred_block = 0;
182 3468 libmpq__off_t transferred_total = 0;
183 3468 uint32_t *checksums = NULL;
184 3468 uint32_t mismatches = 0;
185 3468 uint32_t sector_mismatches = 0;
186 3468 int lossy = 0;
187
188
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 3467 times.
3468 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
189 1 return LIBMPQ_ERROR_EXIST;
190 }
191
192 3467 libmpq__file_size_unpacked(mpq_archive, file_number, &expected_size);
193
194
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3466 times.
3467 if (expected_size > out_size) {
195 1 return LIBMPQ_ERROR_SIZE;
196 }
197
198 3466 libmpq__file_offset(mpq_archive, file_number, &file_offset);
199 3466 libmpq__file_blocks(mpq_archive, file_number, &blocks);
200
201
2/2
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 3461 times.
3466 if ((result = libmpq__reader_offsets_acquire(mpq_archive, file_number, NULL)) < 0) {
202 5 return result;
203 }
204
205 /*
206 * Sector tables are optional metadata. Explicit verification reports
207 * malformed tables, while complete reads skip unusable tables and retain
208 * normal extraction behavior.
209 */
210
2/2
✓ Branch 1 taken 66 times.
✓ Branch 2 taken 3395 times.
3461 if (libmpq__reader_sector_checksums(mpq_archive, file_number, &checksums) < 0)
211 66 checksums = NULL;
212
213 /* Read each block into its exact destination slice and maintain one total. */
214
2/2
✓ Branch 0 taken 8925 times.
✓ Branch 1 taken 3457 times.
12382 for (i = 0; i < blocks; i++) {
215 8925 int block_lossy = 0;
216
217 8925 unpacked_size = 0;
218
219 8925 libmpq__block_size_unpacked(mpq_archive, file_number, i, &unpacked_size);
220
221 /* Offset state is already held for the complete file read. */
222
2/2
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 8921 times.
8925 if ((result = libmpq__reader_block_read_acquired(
223 mpq_archive, file_number, i, out_buf + transferred_total, unpacked_size,
224
2/2
✓ Branch 0 taken 1860 times.
✓ Branch 1 taken 7065 times.
8925 &transferred_block, checksums != NULL ? checksums + i : NULL, &sector_mismatches,
225 &block_lossy
226 )) < 0) {
227 4 free(checksums);
228 4 libmpq__reader_offsets_release(mpq_archive, file_number);
229 4 return result;
230 }
231
232
2/2
✓ Branch 0 taken 132 times.
✓ Branch 1 taken 8789 times.
8921 if (block_lossy)
233 132 lossy = 1;
234 8921 transferred_total += transferred_block;
235 }
236
237
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3455 times.
3457 if (transferred_total != expected_size) {
238 2 free(checksums);
239 2 (void)libmpq__reader_offsets_release(mpq_archive, file_number);
240 2 return LIBMPQ_ERROR_READ;
241 }
242 3455 result = libmpq__reader_offsets_release(mpq_archive, file_number);
243 3455 free(checksums);
244
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3455 times.
3455 if (result < 0)
245 ✗ return result;
246
247
2/2
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 3418 times.
3455 if (sector_mismatches != 0)
248 37 return LIBMPQ_ERROR_READ;
249
250 /*
251 * file_read always decodes the complete logical member. Compare lossless
252 * output against attributes without reopening or rereading the member.
253 */
254
2/2
✓ Branch 0 taken 3390 times.
✓ Branch 1 taken 28 times.
3418 if (!lossy) {
255 3390 result = libmpq__attributes_verify_data(
256 mpq_archive, file_number, out_buf, (size_t)transferred_total, &mismatches
257 );
258
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3390 times.
3390 if (result < 0)
259 ✗ return result;
260
2/2
✓ Branch 0 taken 95 times.
✓ Branch 1 taken 3295 times.
3390 if (mismatches != 0)
261 95 return LIBMPQ_ERROR_READ;
262 }
263
264
2/2
✓ Branch 0 taken 3248 times.
✓ Branch 1 taken 75 times.
3323 if (transferred != NULL) {
265 3248 *transferred = transferred_total;
266 }
267
268 3323 return 0;
269 }
270
271 /*
272 * Metadata-only sizes need no decryption key. For sectorized codec files,
273 * reuse the reader's parsed offsets and exclude the checksum-table extent.
274 */
275 int32_t
276 1669 libmpq__reader_block_size_packed(
277 mpq_archive_s *archive, uint32_t number, uint32_t block, libmpq__off_t *packed_size
278 )
279 {
280 uint32_t index;
281 uint32_t flags;
282 1669 uint64_t start = 0;
283 uint64_t length;
284 int32_t status;
285
286
2/2
✓ Branch 0 taken 1571 times.
✓ Branch 1 taken 98 times.
1669 if (packed_size != NULL)
287 1571 *packed_size = 0;
288
4/4
✓ Branch 0 taken 1667 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 98 times.
✓ Branch 3 taken 1569 times.
1669 if (archive == NULL || packed_size == NULL)
289 100 return LIBMPQ_ERROR_EXIST;
290
2/2
✓ Branch 0 taken 196 times.
✓ Branch 1 taken 1373 times.
1569 if (archive->write_mode)
291 196 return LIBMPQ_ERROR_NOT_INITIALIZED;
292
4/4
✓ Branch 1 taken 1177 times.
✓ Branch 2 taken 196 times.
✓ Branch 3 taken 196 times.
✓ Branch 4 taken 981 times.
2550 if (libmpq__reader_validate_file_number(archive, number) < 0 ||
293 1177 libmpq__reader_validate_block_number(archive, number, block) < 0)
294 392 return LIBMPQ_ERROR_EXIST;
295 981 index = archive->mpq_map[number].block_table_indices;
296 981 flags = archive->mpq_block[index].flags;
297
2/2
✓ Branch 0 taken 79 times.
✓ Branch 1 taken 902 times.
981 if ((flags & LIBMPQ_FLAG_SINGLE) != 0) {
298 79 length = archive->mpq_block[index].packed_size;
299
2/2
✓ Branch 0 taken 132 times.
✓ Branch 1 taken 770 times.
902 } else if ((flags & (LIBMPQ_FLAG_COMPRESSED | LIBMPQ_FLAG_COMPRESS_PKZIP)) == 0) {
300 libmpq__off_t unpacked;
301 132 status = libmpq__block_size_unpacked(archive, number, block, &unpacked);
302
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 132 times.
132 if (status < 0)
303 ✗ return status;
304 132 start = (uint64_t)block * archive->block_size;
305 132 length = (uint64_t)unpacked;
306 } else {
307 uint32_t end;
308 770 status = libmpq__reader_offsets_acquire(archive, number, NULL);
309
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 770 times.
770 if (status < 0)
310 ✗ return status;
311 770 start = archive->mpq_file[number]->packed_offset[block];
312 770 end = archive->mpq_file[number]->packed_offset[block + 1U];
313 770 status = libmpq__reader_offsets_release(archive, number);
314
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 770 times.
770 if (status < 0)
315 ✗ return status;
316
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 770 times.
770 if (end < start)
317 ✗ return LIBMPQ_ERROR_FORMAT;
318 770 length = end - start;
319 }
320
1/2
✓ Branch 0 taken 981 times.
✗ Branch 1 not taken.
981 if (start > archive->mpq_block[index].packed_size ||
321
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 981 times.
981 length > archive->mpq_block[index].packed_size - start)
322 ✗ return LIBMPQ_ERROR_FORMAT;
323 981 status = libmpq__reader_validate_payload_range(archive, index, start, length);
324
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 981 times.
981 if (status < 0)
325 ✗ return status;
326 981 *packed_size = (libmpq__off_t)length;
327 981 return 0;
328 }
329
330 /*
331 * Read all or a prefix of an already bounded packed sector. Encryption uses
332 * complete words; a short final word remains literal as in the full reader.
333 */
334 static int32_t
335 20453 read_packed(mpq_archive_s *archive, uint32_t number, uint32_t block, uint8_t *buffer, size_t size)
336 {
337 20453 uint32_t index = archive->mpq_map[number].block_table_indices;
338 uint32_t seed;
339 20453 uint64_t offset = (uint64_t)archive->archive_offset + archive->mpq_block[index].offset +
340 20453 ((uint64_t)archive->mpq_block_ex[index].offset_high << 32) +
341 20453 archive->mpq_file[number]->packed_offset[block];
342 20453 int32_t status = libmpq__source_read_at(archive->source, offset, buffer, size);
343
2/2
✓ Branch 0 taken 117 times.
✓ Branch 1 taken 20336 times.
20453 if (status < 0)
344 117 return status;
345
4/4
✓ Branch 0 taken 8669 times.
✓ Branch 1 taken 11667 times.
✓ Branch 2 taken 8397 times.
✓ Branch 3 taken 272 times.
20336 if ((archive->mpq_block[index].flags & LIBMPQ_FLAG_ENCRYPTED) != 0 &&
346 size >= sizeof(uint32_t)) {
347
3/4
✓ Branch 1 taken 8396 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8396 times.
16793 if (libmpq__reader_get_block_seed(archive, number, block, &seed) < 0 ||
348 8396 libmpq__crypto_decrypt_block(buffer, (uint32_t)size, seed) < 0)
349 1 return LIBMPQ_ERROR_DECRYPT;
350 }
351 20335 return 0;
352 }
353
354 /* Inspect the stored method without a sector-sized buffer or decompression. */
355 int32_t
356 890 libmpq__reader_block_compression(
357 mpq_archive_s *archive, uint32_t number, uint32_t block, uint32_t *compression
358 )
359 {
360 libmpq__off_t packed;
361 libmpq__off_t unpacked;
362 uint32_t flags;
363 uint8_t prefix[4];
364 size_t prefix_size;
365 int32_t status;
366
367
2/2
✓ Branch 0 taken 792 times.
✓ Branch 1 taken 98 times.
890 if (compression != NULL)
368 792 *compression = 0;
369
2/2
✓ Branch 0 taken 98 times.
✓ Branch 1 taken 792 times.
890 if (compression == NULL)
370 98 return LIBMPQ_ERROR_EXIST;
371 792 status = libmpq__reader_block_size_packed(archive, number, block, &packed);
372
2/2
✓ Branch 0 taken 295 times.
✓ Branch 1 taken 497 times.
792 if (status < 0)
373 295 return status;
374 497 status = libmpq__block_size_unpacked(archive, number, block, &unpacked);
375
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 497 times.
497 if (status < 0)
376 ✗ return status;
377 497 flags = archive->mpq_block[archive->mpq_map[number].block_table_indices].flags;
378
2/2
✓ Branch 0 taken 377 times.
✓ Branch 1 taken 120 times.
497 if (packed >= unpacked ||
379
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 377 times.
377 (flags & (LIBMPQ_FLAG_COMPRESS_MULTI | LIBMPQ_FLAG_COMPRESS_PKZIP)) == 0)
380 120 return 0;
381
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 377 times.
377 if (packed == 0)
382 ✗ return LIBMPQ_ERROR_FORMAT;
383
2/2
✓ Branch 0 taken 107 times.
✓ Branch 1 taken 270 times.
377 if ((flags & LIBMPQ_FLAG_COMPRESS_MULTI) == 0) {
384 107 *compression = LIBMPQ_COMPRESSION_PKZIP;
385 107 return 0;
386 }
387 270 status = libmpq__reader_offsets_acquire(archive, number, NULL);
388
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 270 times.
270 if (status < 0)
389 ✗ return status;
390
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 203 times.
270 prefix_size = (flags & LIBMPQ_FLAG_ENCRYPTED) ? sizeof(prefix) : 1U;
391
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 270 times.
270 if (packed < (libmpq__off_t)prefix_size)
392 ✗ prefix_size = (size_t)packed;
393 270 status = read_packed(archive, number, block, prefix, prefix_size);
394 270 (void)libmpq__reader_offsets_release(archive, number);
395
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 236 times.
270 if (status < 0)
396 34 return status;
397 236 *compression = prefix[0];
398 236 return 0;
399 }
400
401 /*
402 * Sector checksums follow packed sectors and are not encrypted, even when
403 * file data is encrypted. Reuse the offset table loaded by open_named().
404 */
405 static int32_t sector_checksums(mpq_archive_s *archive, uint32_t number, uint32_t **checksums);
406
407 int32_t
408 5736 libmpq__reader_sector_checksums(mpq_archive_s *archive, uint32_t number, uint32_t **checksums)
409 {
410 5736 int32_t result = libmpq__reader_offsets_acquire(archive, number, NULL);
411 5736 *checksums = NULL;
412
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5736 times.
5736 if (result < 0)
413 ✗ return result;
414 5736 result = sector_checksums(archive, number, checksums);
415 5736 (void)libmpq__reader_offsets_release(archive, number);
416 5736 return result;
417 }
418
419 static int32_t
420 5736 sector_checksums(mpq_archive_s *archive, uint32_t number, uint32_t **checksums)
421 {
422 5736 uint32_t index = archive->mpq_map[number].block_table_indices;
423 5736 uint32_t flags = archive->mpq_block[index].flags;
424 5736 uint32_t blocks = libmpq__reader_count_file_blocks(archive, number);
425 uint32_t start;
426 uint32_t end;
427 uint32_t packed_size;
428 size_t size;
429 uint64_t offset;
430 5736 uint8_t *packed = NULL;
431 5736 uint32_t *table = NULL;
432 int32_t status;
433
434 5736 *checksums = NULL;
435
5/6
✓ Branch 0 taken 5695 times.
✓ Branch 1 taken 41 times.
✓ Branch 2 taken 2572 times.
✓ Branch 3 taken 3123 times.
✓ Branch 4 taken 2572 times.
✗ Branch 5 not taken.
5736 if (blocks == 0 || (flags & LIBMPQ_FLAG_CRC) == 0 || (flags & LIBMPQ_FLAG_SINGLE) != 0 ||
436
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2572 times.
2572 (flags & (LIBMPQ_FLAG_COMPRESSED | LIBMPQ_FLAG_COMPRESS_PKZIP)) == 0)
437 3164 return 0;
438
1/2
✓ Branch 0 taken 2572 times.
✗ Branch 1 not taken.
2572 if (archive->mpq_file[number] == NULL ||
439
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2572 times.
2572 archive->mpq_file[number]->packed_offset_count != blocks + 1)
440 ✗ return LIBMPQ_ERROR_OPEN;
441
442 /* The existing parser allocates and decodes one extra offset for CRC files. */
443 2572 start = archive->mpq_file[number]->packed_offset[blocks];
444 2572 end = archive->mpq_file[number]->packed_offset[blocks + 1];
445
3/4
✓ Branch 0 taken 2553 times.
✓ Branch 1 taken 19 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2553 times.
2572 if (end == 0 || end == start)
446 19 return 0;
447
3/4
✓ Branch 0 taken 2553 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 198 times.
✓ Branch 3 taken 2355 times.
2553 if (end < start || end > archive->mpq_block[index].packed_size)
448 198 return LIBMPQ_ERROR_FORMAT;
449
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2355 times.
2355 if ((uint64_t)blocks * sizeof(uint32_t) > INT32_MAX ||
450 (uint64_t)blocks * sizeof(uint32_t) > SIZE_MAX)
451 ✗ return LIBMPQ_ERROR_SIZE;
452 2355 size = (size_t)blocks * sizeof(uint32_t);
453 2355 packed_size = end - start;
454
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2355 times.
2355 if (packed_size > size)
455 ✗ return LIBMPQ_ERROR_FORMAT;
456 2355 status = libmpq__reader_validate_payload_range(archive, index, start, packed_size);
457
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2355 times.
2355 if (status < 0)
458 ✗ return status;
459 2355 offset = (uint64_t)archive->archive_offset + archive->mpq_block[index].offset +
460 2355 ((uint64_t)archive->mpq_block_ex[index].offset_high << 32) + start;
461 2355 packed = malloc(packed_size);
462 2355 table = malloc(size);
463
2/4
✓ Branch 0 taken 2355 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2355 times.
2355 if (packed == NULL || table == NULL) {
464 ✗ status = LIBMPQ_ERROR_MALLOC;
465 ✗ goto cleanup;
466 }
467 2355 status = libmpq__source_read_at(archive->source, offset, packed, packed_size);
468
2/2
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 2331 times.
2355 if (status < 0)
469 24 goto cleanup;
470 2331 status = libmpq__compression_decompress_block(
471 packed, packed_size, (uint8_t *)table, size, LIBMPQ_FLAG_COMPRESS_MULTI,
472 2331 archive->mpq_header.version
473 );
474
2/4
✓ Branch 0 taken 2331 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2331 times.
2331 if (status < 0 || (uint64_t)status != size) {
475 ✗ status = LIBMPQ_ERROR_FORMAT;
476 ✗ goto cleanup;
477 }
478 2331 libmpq__reader_decode_uint32_table(table, (const uint8_t *)table, blocks);
479 2331 *checksums = table;
480 2331 table = NULL;
481 2331 status = 0;
482 2355 cleanup:
483 2355 free(table);
484 2355 free(packed);
485 2355 return status;
486 }
487
488 /*
489 * Read, decrypt and decompress one block from an opened file entry.
490 * The routine computes packed bounds, applies per-block encryption, selects
491 * raw or codec output, and reports the exact unpacked byte count.
492 */
493 int32_t libmpq__reader_block_read_acquired(
494 mpq_archive_s *archive, uint32_t number, uint32_t block, uint8_t *buffer, libmpq__off_t size,
495 libmpq__off_t *transferred, const uint32_t *checksum, uint32_t *mismatches, int *lossy
496 );
497
498 int32_t
499 1433 libmpq__reader_block_read(
500 mpq_archive_s *archive, uint32_t number, uint32_t block, uint8_t *buffer, libmpq__off_t size,
501 libmpq__off_t *transferred, const uint32_t *checksum, uint32_t *mismatches, int *lossy
502 )
503 {
504 int32_t result;
505
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1433 times.
1433 if (lossy != NULL)
506 ✗ *lossy = 0;
507
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1432 times.
1433 if (libmpq__reader_validate_block_number(archive, number, block) < 0)
508 1 return LIBMPQ_ERROR_EXIST;
509 1432 result = libmpq__reader_offsets_acquire(archive, number, NULL);
510
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1432 times.
1432 if (result < 0)
511 ✗ return result;
512 1432 result = libmpq__reader_block_read_acquired(
513 archive, number, block, buffer, size, transferred, checksum, mismatches, lossy
514 );
515 1432 (void)libmpq__reader_offsets_release(archive, number);
516 1432 return result;
517 }
518
519 int32_t
520 20202 libmpq__reader_block_read_acquired(
521 mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number, uint8_t *out_buf,
522 libmpq__off_t out_size, libmpq__off_t *transferred, const uint32_t *checksum,
523 uint32_t *mismatches, int *lossy
524 )
525 {
526
527 /* Packed input buffer, size bookkeeping and block decryption state. */
528 uint8_t *in_buf;
529 20202 uint32_t flags = 0;
530 20202 uint32_t encrypted = 0;
531 20202 uint32_t compressed = 0;
532 20202 uint32_t imploded = 0;
533 20202 int32_t tb = 0;
534 20202 uint8_t use_out_buf = 0;
535 20202 libmpq__off_t in_size = 0;
536 20202 libmpq__off_t unpacked_size = 0;
537
538
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 20202 times.
20202 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
539 ✗ return LIBMPQ_ERROR_EXIST;
540 }
541
542
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 20202 times.
20202 if (libmpq__reader_validate_block_number(mpq_archive, file_number, block_number) < 0) {
543 ✗ return LIBMPQ_ERROR_EXIST;
544 }
545
546
1/2
✓ Branch 0 taken 20202 times.
✗ Branch 1 not taken.
20202 if (mpq_archive->mpq_file[file_number] == NULL ||
547
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20202 times.
20202 mpq_archive->mpq_file[file_number]->packed_offset == NULL) {
548 ✗ return LIBMPQ_ERROR_OPEN;
549 }
550
551
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 20202 times.
20202 if (mpq_archive->mpq_file[file_number]->packed_offset_count <= block_number + 1) {
552 ✗ return LIBMPQ_ERROR_EXIST;
553 }
554
555 20202 libmpq__block_size_unpacked(mpq_archive, file_number, block_number, &unpacked_size);
556
557
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 20201 times.
20202 if (unpacked_size > out_size) {
558 1 return LIBMPQ_ERROR_SIZE;
559 }
560
561 /*
562 * Compute the absolute payload position from archive, file, and block offsets.
563 * The stored block offset is relative to the file payload start, not the
564 * beginning of the archive file.
565 */
566 20201 if (mpq_archive->mpq_file[file_number]->packed_offset[block_number + 1] <
567
1/2
✓ Branch 0 taken 20201 times.
✗ Branch 1 not taken.
20201 mpq_archive->mpq_file[file_number]->packed_offset[block_number] ||
568 20201 mpq_archive->mpq_file[file_number]->packed_offset[block_number + 1] >
569 20201 mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices]
570
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 20199 times.
20201 .packed_size) {
571 2 return LIBMPQ_ERROR_FORMAT;
572 }
573 20199 in_size = mpq_archive->mpq_file[file_number]->packed_offset[block_number + 1] -
574 20199 mpq_archive->mpq_file[file_number]->packed_offset[block_number];
575
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 20183 times.
20199 if (libmpq__reader_validate_payload_range(
576 20199 mpq_archive, mpq_archive->mpq_map[file_number].block_table_indices,
577 20199 mpq_archive->mpq_file[file_number]->packed_offset[block_number], (uint64_t)in_size
578 ) < 0) {
579 16 return LIBMPQ_ERROR_READ;
580 }
581
582 20183 libmpq__file_flags(mpq_archive, file_number, &flags);
583 20183 encrypted = flags & LIBMPQ_FILE_FLAG_ENCRYPTED;
584 20183 compressed = flags & LIBMPQ_FILE_FLAG_COMPRESS;
585 20183 imploded = flags & LIBMPQ_FILE_FLAG_IMPLODE;
586
587 /* Raw unencrypted blocks can be read directly into the caller's buffer. */
588
7/8
✓ Branch 0 taken 11521 times.
✓ Branch 1 taken 8662 times.
✓ Branch 2 taken 1930 times.
✓ Branch 3 taken 9591 times.
✓ Branch 4 taken 1514 times.
✓ Branch 5 taken 416 times.
✓ Branch 6 taken 1514 times.
✗ Branch 7 not taken.
20183 use_out_buf = !encrypted && !compressed && !imploded && in_size <= out_size;
589
590
2/2
✓ Branch 0 taken 1514 times.
✓ Branch 1 taken 18669 times.
20183 if (use_out_buf) {
591
592 /* Raw data can bypass a temporary allocation when no transform is needed. */
593 1514 in_buf = out_buf;
594 } else {
595
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18669 times.
18669 if ((in_buf = calloc(1, in_size)) == NULL) {
596 ✗ return LIBMPQ_ERROR_MALLOC;
597 }
598 }
599
600
2/2
✓ Branch 1 taken 84 times.
✓ Branch 2 taken 20099 times.
20183 if ((tb = read_packed(mpq_archive, file_number, block_number, in_buf, (size_t)in_size)) < 0) {
601
2/2
✓ Branch 0 taken 83 times.
✓ Branch 1 taken 1 times.
84 if (!use_out_buf) {
602 83 free(in_buf);
603 }
604 84 return tb;
605 }
606
607 /*
608 * A multi-compression sector only carries a codec mask when it was
609 * actually compressed. MPQ WAVE ADPCM is lossy, so its decoded PCM cannot
610 * be compared with source-byte (attributes) CRC32/MD5 metadata.
611 */
612
7/8
✓ Branch 0 taken 8922 times.
✓ Branch 1 taken 11177 times.
✓ Branch 2 taken 7515 times.
✓ Branch 3 taken 1407 times.
✓ Branch 4 taken 6611 times.
✓ Branch 5 taken 904 times.
✓ Branch 6 taken 6611 times.
✗ Branch 7 not taken.
20099 if (lossy != NULL && compressed && in_size < unpacked_size && in_size != 0 &&
613
2/2
✓ Branch 0 taken 132 times.
✓ Branch 1 taken 6479 times.
6611 (in_buf[0] & (LIBMPQ_COMPRESSION_WAVE_MONO | LIBMPQ_COMPRESSION_WAVE_STEREO)) != 0)
614 132 *lossy = 1;
615
616 /*
617 * MPQ sector CRCs are Adler-32 over decrypted packed bytes, not CRC32.
618 * Zero and all-ones entries are unavailable legacy checksum values.
619 */
620
6/6
✓ Branch 0 taken 8583 times.
✓ Branch 1 taken 11516 times.
✓ Branch 2 taken 8565 times.
✓ Branch 3 taken 18 times.
✓ Branch 4 taken 8549 times.
✓ Branch 5 taken 16 times.
20099 if (checksum != NULL && *checksum != 0 && *checksum != UINT32_MAX &&
621
2/2
✓ Branch 1 taken 318 times.
✓ Branch 2 taken 8231 times.
8549 (uint32_t)adler32(0, in_buf, (uInt)in_size) != *checksum)
622 318 *mismatches |= LIBMPQ_VERIFY_SECTOR_CRC;
623
624 /* Blizzard multi-compression blocks declare their exact backend chain in the payload. */
625
2/2
✓ Branch 0 taken 17568 times.
✓ Branch 1 taken 2531 times.
20099 if (compressed) {
626
627 /* The payload's leading mask selects and orders its decompression stages. */
628
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 17534 times.
17568 if ((tb = libmpq__compression_decompress_block(
629 in_buf, in_size, out_buf, unpacked_size, LIBMPQ_FLAG_COMPRESS_MULTI,
630 17568 mpq_archive->mpq_header.version
631 )) < 0) {
632
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 if (!use_out_buf) {
633 34 free(in_buf);
634 }
635 34 return LIBMPQ_ERROR_UNPACK;
636 }
637 }
638
639 /* PKWARE-imploded blocks use the legacy explode decoder. */
640
2/2
✓ Branch 0 taken 879 times.
✓ Branch 1 taken 19186 times.
20065 if (imploded) {
641
642 /* Standalone PKWARE payloads use the legacy decoder without a mask byte. */
643
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 879 times.
879 if ((tb = libmpq__compression_decompress_block(
644 in_buf, in_size, out_buf, unpacked_size, LIBMPQ_FLAG_COMPRESS_PKZIP,
645 879 mpq_archive->mpq_header.version
646 )) < 0) {
647 ✗ if (!use_out_buf) {
648 ✗ free(in_buf);
649 }
650 ✗ return LIBMPQ_ERROR_UNPACK;
651 }
652 }
653
654
3/4
✓ Branch 0 taken 17534 times.
✓ Branch 1 taken 2531 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 17534 times.
20065 if (compressed && imploded) {
655 ✗ if (!use_out_buf) {
656 ✗ free(in_buf);
657 }
658 ✗ return LIBMPQ_ERROR_UNPACK;
659 }
660
661
4/4
✓ Branch 0 taken 2531 times.
✓ Branch 1 taken 17534 times.
✓ Branch 2 taken 1652 times.
✓ Branch 3 taken 879 times.
20065 if (!compressed && !imploded) {
662
663 /* A raw block is copied only after encrypted and compressed paths are excluded. */
664
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1652 times.
1652 if ((tb = libmpq__compression_decompress_block(
665 in_buf, in_size, out_buf, unpacked_size, LIBMPQ_FLAG_COMPRESS_NONE,
666 1652 mpq_archive->mpq_header.version
667 )) < 0) {
668 ✗ if (!use_out_buf) {
669 ✗ free(in_buf);
670 }
671 ✗ return LIBMPQ_ERROR_UNPACK;
672 }
673 }
674
675
2/2
✓ Branch 0 taken 18552 times.
✓ Branch 1 taken 1513 times.
20065 if (!use_out_buf) {
676 18552 free(in_buf);
677 }
678
679
1/2
✓ Branch 0 taken 20065 times.
✗ Branch 1 not taken.
20065 if (transferred != NULL) {
680 20065 *transferred = tb;
681 }
682
683 20065 return 0;
684 }
685
686 /*
687 * Verify that a file payload subrange is both internally consistent and
688 * contained in the physical backing file captured when the archive opened.
689 * Sector offsets and block-table sizes are archive-controlled, so this check
690 * must happen before using either value for allocation or source reads.
691 */
692 int32_t
693 29265 libmpq__reader_validate_payload_range(
694 const mpq_archive_s *mpq_archive, uint32_t block_table_index, uint64_t relative_offset,
695 uint64_t size
696 )
697 {
698 uint64_t payload_offset;
699 uint64_t absolute_offset;
700
701
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29265 times.
29265 if (mpq_archive->archive_offset < 0) {
702 ✗ return LIBMPQ_ERROR_FORMAT;
703 }
704
705 29265 payload_offset = ((uint64_t)mpq_archive->mpq_block_ex[block_table_index].offset_high << 32) |
706 29265 mpq_archive->mpq_block[block_table_index].offset;
707 29265 absolute_offset = (uint64_t)mpq_archive->archive_offset;
708
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29265 times.
29265 if (payload_offset > UINT64_MAX - absolute_offset) {
709 ✗ return LIBMPQ_ERROR_FORMAT;
710 }
711 29265 absolute_offset += payload_offset;
712
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29265 times.
29265 if (relative_offset > UINT64_MAX - absolute_offset) {
713 ✗ return LIBMPQ_ERROR_FORMAT;
714 }
715 29265 absolute_offset += relative_offset;
716
2/2
✓ Branch 0 taken 29241 times.
✓ Branch 1 taken 24 times.
29265 if (absolute_offset > mpq_archive->file_size ||
717
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29241 times.
29241 size > mpq_archive->file_size - absolute_offset) {
718 24 return LIBMPQ_ERROR_READ;
719 }
720
721 29241 return 0;
722 }
723
724 /*
725 * Open a file entry and cache its packed block offset table for block operations.
726 * Compressed entries load and decrypt their serialized offsets, while raw or
727 * single-unit entries receive synthesized offsets from block metadata.
728 */
729 int32_t
730 15359 libmpq__reader_offsets_acquire(mpq_archive_s *mpq_archive, uint32_t file_number, const char *name)
731 {
732
733 /* Packed block table state, file seed and read status. */
734 uint32_t blocks;
735 uint32_t i;
736 uint32_t block_table_index;
737 uint32_t packed_offset_count;
738 uint32_t packed_size;
739 15359 int32_t result = 0;
740 15359 uint8_t *packed_data = NULL;
741
742
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 15359 times.
15359 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
743 ✗ return LIBMPQ_ERROR_EXIST;
744 }
745
746
2/2
✓ Branch 0 taken 8064 times.
✓ Branch 1 taken 7295 times.
15359 if (mpq_archive->mpq_file[file_number]) {
747
748 /* A named internal read can supply a key to an anonymous cached open. */
749
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 8064 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
8064 if (name != NULL && !mpq_archive->mpq_file[file_number]->seed_known) {
750 ✗ uint32_t index = mpq_archive->mpq_map[file_number].block_table_indices;
751 ✗ uint32_t seed = libmpq__crypto_hash_string(name, 0x300);
752 ✗ if (mpq_archive->mpq_block[index].flags & 0x00020000u)
753 ✗ seed = (seed + mpq_archive->mpq_block[index].offset) ^
754 ✗ mpq_archive->mpq_block[index].unpacked_size;
755 ✗ mpq_archive->mpq_file[file_number]->seed = seed;
756 ✗ mpq_archive->mpq_file[file_number]->seed_known = 1;
757 }
758
759 /* Nested callers share the cached offsets through a reference count. */
760 8064 mpq_archive->mpq_file[file_number]->open_count++;
761 8064 return 0;
762 }
763
764 7295 block_table_index = mpq_archive->mpq_map[file_number].block_table_indices;
765 7295 blocks = libmpq__reader_count_file_blocks(mpq_archive, file_number);
766
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7295 times.
7295 if (blocks > UINT32_MAX / sizeof(uint32_t) - 2U) {
767 ✗ return LIBMPQ_ERROR_FORMAT;
768 }
769 7295 packed_offset_count = blocks + 1;
770 7295 packed_size = sizeof(uint32_t) * packed_offset_count;
771
772
2/2
✓ Branch 0 taken 3839 times.
✓ Branch 1 taken 3456 times.
7295 if ((mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_CRC) != 0) {
773 3839 packed_size += sizeof(uint32_t);
774 }
775
776
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7295 times.
7295 if ((mpq_archive->mpq_file[file_number] = calloc(1, sizeof(mpq_file_s))) == NULL) {
777 ✗ result = LIBMPQ_ERROR_MALLOC;
778 ✗ goto error;
779 }
780
781
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7295 times.
7295 if ((mpq_archive->mpq_file[file_number]->packed_offset = calloc(1, packed_size)) == NULL) {
782 ✗ result = LIBMPQ_ERROR_MALLOC;
783 ✗ goto error;
784 }
785
786 7295 mpq_archive->mpq_file[file_number]->packed_offset_count = packed_offset_count;
787 7295 mpq_archive->mpq_file[file_number]->open_count = 1;
788
789
2/2
✓ Branch 0 taken 1101 times.
✓ Branch 1 taken 6194 times.
7295 if (name != NULL) {
790 1101 uint32_t seed = libmpq__crypto_hash_string(name, 0x300);
791
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1101 times.
1101 if (mpq_archive->mpq_block[block_table_index].flags & 0x00020000u)
792 ✗ seed = (seed + mpq_archive->mpq_block[block_table_index].offset) ^
793 ✗ mpq_archive->mpq_block[block_table_index].unpacked_size;
794 1101 mpq_archive->mpq_file[file_number]->seed = seed;
795 1101 mpq_archive->mpq_file[file_number]->seed_known = 1;
796 }
797
798 /*
799 * Compressed multi-sector files carry serialized offsets before their first
800 * payload, so load that table before any block can be read.
801 */
802
2/2
✓ Branch 0 taken 6211 times.
✓ Branch 1 taken 1084 times.
7295 if ((mpq_archive->mpq_block[block_table_index].flags &
803 6211 (LIBMPQ_FLAG_COMPRESSED | LIBMPQ_FLAG_COMPRESS_PKZIP)) != 0 &&
804
2/2
✓ Branch 0 taken 5730 times.
✓ Branch 1 taken 481 times.
6211 (mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_SINGLE) == 0) {
805
3/4
✓ Branch 0 taken 5730 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 5722 times.
11460 if (mpq_archive->mpq_block[block_table_index].packed_size < packed_size ||
806 5730 libmpq__reader_validate_payload_range(mpq_archive, block_table_index, 0, packed_size) <
807 0) {
808 8 result = LIBMPQ_ERROR_FORMAT;
809 8 goto error;
810 }
811
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5722 times.
5722 if ((packed_data = malloc(packed_size)) == NULL) {
812 ✗ result = LIBMPQ_ERROR_MALLOC;
813 ✗ goto error;
814 }
815
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5722 times.
5722 if ((result = libmpq__source_read_at(
816 5722 mpq_archive->source,
817 5722 mpq_archive->mpq_block[block_table_index].offset +
818 5722 ((uint64_t)mpq_archive->mpq_block_ex[block_table_index].offset_high << 32) +
819 5722 (uint64_t)mpq_archive->archive_offset,
820 packed_data, packed_size
821 )) < 0) {
822 ✗ goto error;
823 }
824
825 /* Some protected archives omit the encrypted flag; a wrong first offset exposes that. */
826
2/2
✓ Branch 1 taken 2266 times.
✓ Branch 2 taken 3456 times.
5722 if (libmpq__load_le32(packed_data) != packed_size &&
827
1/2
✓ Branch 1 taken 2266 times.
✗ Branch 2 not taken.
2266 libmpq__load_le32(packed_data) != packed_size + 4) {
828 2266 mpq_archive->mpq_block[block_table_index].flags |= LIBMPQ_FLAG_ENCRYPTED;
829 }
830
831 /* The packed offset table uses seed - 1, so recover the file seed first. */
832
2/2
✓ Branch 0 taken 2266 times.
✓ Branch 1 taken 3456 times.
5722 if (mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_ENCRYPTED) {
833 2266 uint32_t seed = mpq_archive->mpq_file[file_number]->seed;
834
835
3/4
✓ Branch 0 taken 2257 times.
✓ Branch 1 taken 9 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2257 times.
4523 if (!mpq_archive->mpq_file[file_number]->seed_known &&
836 2257 libmpq__crypto_derive_block_table_seed(
837 packed_data, packed_size, mpq_archive->block_size, &seed
838 ) < 0) {
839 ✗ result = LIBMPQ_ERROR_DECRYPT;
840 2 goto error;
841 }
842 2266 mpq_archive->mpq_file[file_number]->seed = seed;
843 2266 mpq_archive->mpq_file[file_number]->seed_known = 1;
844
845
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2266 times.
2266 if (libmpq__crypto_decrypt_block(
846 2266 packed_data, packed_size, mpq_archive->mpq_file[file_number]->seed - 1
847 ) < 0) {
848 ✗ result = LIBMPQ_ERROR_DECRYPT;
849 ✗ goto error;
850 }
851
852 /* A valid decrypted table starts with its own byte size. */
853
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2264 times.
2266 if (libmpq__load_le32(packed_data) != packed_size) {
854 2 result = LIBMPQ_ERROR_DECRYPT;
855 2 goto error;
856 }
857 }
858
859 5720 libmpq__reader_decode_uint32_table(
860 5720 mpq_archive->mpq_file[file_number]->packed_offset, packed_data,
861 packed_size / sizeof(uint32_t)
862 );
863
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5720 times.
5720 if (mpq_archive->mpq_file[file_number]->packed_offset[0] != packed_size) {
864 ✗ result = LIBMPQ_ERROR_FORMAT;
865 ✗ goto error;
866 }
867
2/2
✓ Branch 0 taken 56618 times.
✓ Branch 1 taken 5719 times.
62337 for (i = 1; i < packed_offset_count; i++) {
868 56618 if (mpq_archive->mpq_file[file_number]->packed_offset[i] <
869
1/2
✓ Branch 0 taken 56618 times.
✗ Branch 1 not taken.
56618 mpq_archive->mpq_file[file_number]->packed_offset[i - 1] ||
870 56618 mpq_archive->mpq_file[file_number]->packed_offset[i] >
871
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 56617 times.
56618 mpq_archive->mpq_block[block_table_index].packed_size) {
872 1 result = LIBMPQ_ERROR_FORMAT;
873 1 goto error;
874 }
875 }
876 5719 free(packed_data);
877 5719 packed_data = NULL;
878 } else {
879
880 /* Raw sectorized files derive offsets directly from their fixed sector size. */
881
2/2
✓ Branch 0 taken 391 times.
✓ Branch 1 taken 1174 times.
1565 if ((mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags &
882 LIBMPQ_FLAG_SINGLE) == 0) {
883
884 /* Synthesize offsets for uncompressed multi-sector files. */
885
2/2
✓ Branch 0 taken 1420 times.
✓ Branch 1 taken 391 times.
1811 for (i = 0; i < packed_offset_count; i++) {
886
2/2
✓ Branch 0 taken 391 times.
✓ Branch 1 taken 1029 times.
1420 if (i == blocks) {
887 391 mpq_archive->mpq_file[file_number]->packed_offset[i] =
888 mpq_archive
889 391 ->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices]
890 391 .unpacked_size;
891 } else {
892 1029 mpq_archive->mpq_file[file_number]->packed_offset[i] =
893 1029 i * mpq_archive->block_size;
894 }
895 }
896 } else {
897 1174 mpq_archive->mpq_file[file_number]->packed_offset[0] = 0;
898 1174 mpq_archive->mpq_file[file_number]->packed_offset[1] =
899 1174 mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices]
900 1174 .packed_size;
901 }
902 }
903
904 /*
905 * Raw encrypted files have no encrypted offset table from which to derive a seed.
906 * The MPQ cipher leaves a trailing partial word unchanged, so an anonymous
907 * payload shorter than one word needs no seed and remains readable.
908 */
909
2/2
✓ Branch 0 taken 64 times.
✓ Branch 1 taken 7220 times.
7284 if ((mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags &
910 64 (LIBMPQ_FLAG_ENCRYPTED | LIBMPQ_FLAG_COMPRESSED)) == LIBMPQ_FLAG_ENCRYPTED &&
911
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 7 times.
64 !mpq_archive->mpq_file[file_number]->seed_known &&
912
2/2
✓ Branch 0 taken 55 times.
✓ Branch 1 taken 2 times.
57 mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].packed_size >=
913 sizeof(uint32_t)) {
914 uint8_t first_block[8];
915 uint32_t first_offset;
916 uint32_t second_offset;
917 uint32_t first_size;
918 uint32_t seed;
919
920
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 55 times.
55 if (packed_offset_count < 2) {
921 ✗ result = LIBMPQ_ERROR_FORMAT;
922 4 goto error;
923 }
924
925 55 first_offset = mpq_archive->mpq_file[file_number]->packed_offset[0];
926 55 second_offset = mpq_archive->mpq_file[file_number]->packed_offset[1];
927
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 55 times.
55 if (second_offset < first_offset) {
928 ✗ result = LIBMPQ_ERROR_FORMAT;
929 ✗ goto error;
930 }
931
932 55 first_size = second_offset - first_offset;
933
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 55 times.
55 if (first_size < sizeof(first_block)) {
934 ✗ result = LIBMPQ_ERROR_DECRYPT;
935 ✗ goto error;
936 }
937
938
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 55 times.
55 if ((result = libmpq__source_read_at(
939 55 mpq_archive->source,
940 55 mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices]
941 55 .offset +
942 55 ((uint64_t)mpq_archive
943 55 ->mpq_block_ex[mpq_archive->mpq_map[file_number].block_table_indices]
944 55 .offset_high
945 55 << 32) +
946 55 (uint64_t)mpq_archive->archive_offset,
947 first_block, sizeof(first_block)
948 )) < 0) {
949 ✗ goto error;
950 }
951
952 /* Raw encrypted payloads require signature-based key recovery instead. */
953
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 51 times.
55 if (libmpq__crypto_detect_file_key(
954 first_block, sizeof(first_block),
955 55 mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices]
956 .unpacked_size,
957 &seed
958 ) < 0) {
959 4 result = LIBMPQ_ERROR_DECRYPT;
960 4 goto error;
961 }
962
963 51 mpq_archive->mpq_file[file_number]->seed = seed;
964 51 mpq_archive->mpq_file[file_number]->seed_known = 1;
965 }
966
967 7280 return 0;
968
969 15 error:
970
971 15 free(packed_data);
972
973
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 if (mpq_archive->mpq_file[file_number] != NULL) {
974 15 free(mpq_archive->mpq_file[file_number]->packed_offset);
975 15 free(mpq_archive->mpq_file[file_number]);
976 15 mpq_archive->mpq_file[file_number] = NULL;
977 }
978
979 15 return result;
980 }
981
982 /*
983 * Calculate a serialized table size while rejecting arithmetic overflow.
984 * MPQ table lengths are stored in 32-bit fields, so both native allocation
985 * size and on-disk representation must fit before the caller proceeds.
986 */
987 static int32_t
988 6997 table_size(uint32_t count, size_t item_size, size_t *size)
989 {
990
4/6
✓ Branch 0 taken 6997 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6997 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 6995 times.
6997 if (item_size == 0 || count > SIZE_MAX / item_size || (size_t)count * item_size > UINT32_MAX) {
991 2 return LIBMPQ_ERROR_FORMAT;
992 }
993
994 6995 *size = (size_t)count * item_size;
995 6995 return 0;
996 }
997
998 /*
999 * Decode a packed array of little-endian 32-bit values in place.
1000 * This is used for sector offset tables whose serialized representation is
1001 * independent of the host CPU's byte order.
1002 */
1003 void
1004 8051 libmpq__reader_decode_uint32_table(uint32_t *table, const uint8_t *raw, uint32_t count)
1005 {
1006 uint32_t i;
1007
1008
2/2
✓ Branch 0 taken 98148 times.
✓ Branch 1 taken 8051 times.
106199 for (i = 0; i < count; i++) {
1009 98148 table[i] = libmpq__load_le32(raw + i * sizeof(uint32_t));
1010 }
1011 8051 }
1012
1013 /*
1014 * Open an MPQ archive path and prepare decoded metadata for later operations.
1015 * The routine locates the header, loads and decrypts all metadata tables, and
1016 * builds the compact file map used by the public archive and block APIs.
1017 */
1018 static int32_t
1019 1663 libmpq__reader_archive_open_source(
1020 mpq_archive_s **mpq_archive, const char *mpq_filename, libmpq__off_t archive_offset,
1021 mpq_source_s *source
1022 )
1023 {
1024
1025 /* Archive table counters and status used while building the file map. */
1026 1663 uint32_t i = 0;
1027 1663 uint32_t count = 0;
1028 1663 int32_t result = 0;
1029 1663 uint32_t header_search = 0;
1030 uint8_t header_data[LIBMPQ_HEADER_WIRE_SIZE];
1031 uint8_t header_ex_data[LIBMPQ_HEADER_EX_WIRE_SIZE];
1032 1663 uint8_t *table_data = NULL;
1033 1663 size_t table_bytes = 0;
1034
1035
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1663 times.
1663 if (mpq_archive == NULL) {
1036 ✗ libmpq__source_discard(source);
1037 ✗ return LIBMPQ_ERROR_EXIST;
1038 }
1039 1663 *mpq_archive = NULL;
1040
1041 /* A sentinel offset requests the embedded-archive scan used by readers. */
1042
2/2
✓ Branch 0 taken 478 times.
✓ Branch 1 taken 1185 times.
1663 if (archive_offset == -1) {
1043 478 archive_offset = 0;
1044 478 header_search = 1;
1045
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1184 times.
1185 } else if (archive_offset < 0) {
1046 1 libmpq__source_discard(source);
1047 1 return LIBMPQ_ERROR_SEEK;
1048 }
1049
1050
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1662 times.
1662 if ((*mpq_archive = calloc(1, sizeof(mpq_archive_s))) == NULL) {
1051 ✗ libmpq__source_discard(source);
1052 ✗ return LIBMPQ_ERROR_MALLOC;
1053 }
1054
1055 /* Transfer source ownership before any later archive initialization can fail. */
1056 1662 (*mpq_archive)->source = source;
1057
1058
2/2
✓ Branch 0 taken 1631 times.
✓ Branch 1 taken 31 times.
1662 if (mpq_filename != NULL) {
1059 1631 (*mpq_archive)->filename = malloc(strlen(mpq_filename) + 1);
1060
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1631 times.
1631 if ((*mpq_archive)->filename == NULL) {
1061 ✗ result = LIBMPQ_ERROR_MALLOC;
1062 ✗ goto error;
1063 }
1064 1631 memcpy((*mpq_archive)->filename, mpq_filename, strlen(mpq_filename) + 1);
1065 }
1066
1067 1662 (*mpq_archive)->file_identity_valid =
1068 1662 libmpq__source_file_identity(
1069 1662 source, &(*mpq_archive)->file_device, &(*mpq_archive)->file_inode
1070 1662 ) == 0;
1071
1072 1662 (*mpq_archive)->file_size = libmpq__source_size(source);
1073
1074 1662 (*mpq_archive)->mpq_header.mpq_magic = 0;
1075 1662 (*mpq_archive)->files = 0;
1076
1077 /* Probe the requested location, or advance in 512-byte steps for embedded archives. */
1078 while (1) {
1079 2169 (*mpq_archive)->mpq_header.mpq_magic = 0;
1080
1081
2/2
✓ Branch 0 taken 2154 times.
✓ Branch 1 taken 15 times.
2169 if ((uint64_t)archive_offset > (*mpq_archive)->file_size ||
1082
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2149 times.
2154 sizeof(header_data) > (*mpq_archive)->file_size - (uint64_t)archive_offset) {
1083 20 result = LIBMPQ_ERROR_FORMAT;
1084 20 goto error;
1085 }
1086
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 2146 times.
2149 if ((result = libmpq__source_read_at(
1087 2149 (*mpq_archive)->source, (uint64_t)archive_offset, header_data, sizeof(header_data)
1088 )) < 0)
1089 3 goto error;
1090
1091
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2146 times.
2146 if ((result = libmpq__header_decode(
1092 2146 &(*mpq_archive)->mpq_header, header_data, sizeof(header_data)
1093 )) < 0)
1094 ✗ goto error;
1095
1096
2/2
✓ Branch 0 taken 1631 times.
✓ Branch 1 taken 515 times.
2146 if ((*mpq_archive)->mpq_header.mpq_magic == LIBMPQ_HEADER) {
1097
2/2
✓ Branch 0 taken 1134 times.
✓ Branch 1 taken 497 times.
1631 if ((*mpq_archive)->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_ONE) {
1098
1099 /* Protected archives may store a bogus header size; normalize it locally. */
1100
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1134 times.
1134 if ((*mpq_archive)->mpq_header.header_size != LIBMPQ_HEADER_WIRE_SIZE) {
1101 ✗ (*mpq_archive)->mpq_header.header_size = LIBMPQ_HEADER_WIRE_SIZE;
1102 }
1103 }
1104
1105
2/2
✓ Branch 0 taken 497 times.
✓ Branch 1 taken 1134 times.
1631 if ((*mpq_archive)->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_TWO) {
1106
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 497 times.
497 if ((*mpq_archive)->mpq_header.header_size !=
1107 LIBMPQ_HEADER_WIRE_SIZE + LIBMPQ_HEADER_EX_WIRE_SIZE) {
1108 ✗ (*mpq_archive)->mpq_header.header_size =
1109 LIBMPQ_HEADER_WIRE_SIZE + LIBMPQ_HEADER_EX_WIRE_SIZE;
1110 }
1111 }
1112
1113
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1631 times.
1631 if ((*mpq_archive)->mpq_header.version > LIBMPQ_ARCHIVE_VERSION_TWO) {
1114 ✗ result = LIBMPQ_ERROR_FORMAT;
1115 ✗ goto error;
1116 }
1117
1118 1631 break;
1119 }
1120
1121
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 507 times.
515 if (!header_search) {
1122 8 result = LIBMPQ_ERROR_FORMAT;
1123 8 goto error;
1124 }
1125 507 archive_offset += 512;
1126 }
1127
1128
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1629 times.
1631 if ((*mpq_archive)->mpq_header.block_size > 22) {
1129 2 result = LIBMPQ_ERROR_FORMAT;
1130 2 goto error;
1131 }
1132
1133 1629 (*mpq_archive)->block_size = 512U << (*mpq_archive)->mpq_header.block_size;
1134 1629 (*mpq_archive)->archive_offset = archive_offset;
1135
1136
2/2
✓ Branch 0 taken 1627 times.
✓ Branch 1 taken 2 times.
1629 if (table_size(
1137 1629 (*mpq_archive)->mpq_header.hash_table_count, LIBMPQ_HASH_ENTRY_WIRE_SIZE, &table_bytes
1138 1627 ) < 0 ||
1139
2/4
✓ Branch 0 taken 1627 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1627 times.
✗ Branch 3 not taken.
3254 (uint64_t)table_bytes > (*mpq_archive)->file_size ||
1140 1627 table_size(
1141 1627 (*mpq_archive)->mpq_header.block_table_count, LIBMPQ_BLOCK_ENTRY_WIRE_SIZE, &table_bytes
1142 1627 ) < 0 ||
1143
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1627 times.
1627 (uint64_t)table_bytes > (*mpq_archive)->file_size) {
1144 2 result = LIBMPQ_ERROR_FORMAT;
1145 2 goto error;
1146 }
1147
1148 /* MPQ v2 stores high table offsets in a separate extension immediately after v1. */
1149
2/2
✓ Branch 0 taken 497 times.
✓ Branch 1 taken 1130 times.
1627 if ((*mpq_archive)->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_TWO) {
1150
1/2
✓ Branch 0 taken 497 times.
✗ Branch 1 not taken.
497 if ((uint64_t)archive_offset > UINT64_MAX - LIBMPQ_HEADER_WIRE_SIZE ||
1151
1/2
✓ Branch 0 taken 497 times.
✗ Branch 1 not taken.
497 (uint64_t)archive_offset + LIBMPQ_HEADER_WIRE_SIZE > (*mpq_archive)->file_size ||
1152 sizeof(header_ex_data) >
1153
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 495 times.
497 (*mpq_archive)->file_size - ((uint64_t)archive_offset + LIBMPQ_HEADER_WIRE_SIZE)) {
1154 2 result = LIBMPQ_ERROR_FORMAT;
1155 2 goto error;
1156 }
1157
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 495 times.
495 if ((result = libmpq__source_read_at(
1158 495 (*mpq_archive)->source, (uint64_t)archive_offset + LIBMPQ_HEADER_WIRE_SIZE,
1159 header_ex_data, sizeof(header_ex_data)
1160 )) < 0)
1161 ✗ goto error;
1162
1163
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 495 times.
495 if ((result = libmpq__header_ex_decode(
1164 495 &(*mpq_archive)->mpq_header_ex, header_ex_data, sizeof(header_ex_data)
1165 )) < 0)
1166 ✗ goto error;
1167 }
1168
1169 /* Metadata tables are decoded once and kept with the archive handle for later lookups. */
1170
2/2
✓ Branch 0 taken 1622 times.
✓ Branch 1 taken 3 times.
1625 if (((*mpq_archive)->mpq_header.block_table_count != 0 &&
1171 1622 (((*mpq_archive)->mpq_block =
1172
1/2
✓ Branch 0 taken 1622 times.
✗ Branch 1 not taken.
1622 calloc((*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_block_s))) == NULL ||
1173 1622 ((*mpq_archive)->mpq_block_ex =
1174
1/2
✓ Branch 0 taken 1622 times.
✗ Branch 1 not taken.
1622 calloc((*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_block_ex_s))) ==
1175 1622 NULL ||
1176 1622 ((*mpq_archive)->mpq_file =
1177
1/2
✓ Branch 0 taken 1622 times.
✗ Branch 1 not taken.
1622 calloc((*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_file_s *))) ==
1178 1622 NULL ||
1179 1622 ((*mpq_archive)->mpq_map =
1180
1/2
✓ Branch 0 taken 1622 times.
✗ Branch 1 not taken.
1622 calloc((*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_map_s))) == NULL)) ||
1181
2/2
✓ Branch 0 taken 1623 times.
✓ Branch 1 taken 2 times.
1625 ((*mpq_archive)->mpq_header.hash_table_count != 0 &&
1182 1623 ((*mpq_archive)->mpq_hash =
1183
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1623 times.
1623 calloc((*mpq_archive)->mpq_header.hash_table_count, sizeof(mpq_hash_s))) == NULL)) {
1184 ✗ result = LIBMPQ_ERROR_MALLOC;
1185 ✗ goto error;
1186 }
1187
1188
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1625 times.
1625 if (table_size(
1189 1625 (*mpq_archive)->mpq_header.hash_table_count, LIBMPQ_HASH_ENTRY_WIRE_SIZE, &table_bytes
1190 ) < 0) {
1191 ✗ result = LIBMPQ_ERROR_FORMAT;
1192 ✗ goto error;
1193 }
1194
3/4
✓ Branch 0 taken 1623 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1623 times.
1625 if (table_bytes != 0 && (table_data = malloc(table_bytes)) == NULL) {
1195 ✗ result = LIBMPQ_ERROR_MALLOC;
1196 ✗ goto error;
1197 }
1198
1199 /* Locate, read, decrypt, and decode the hash table before file lookup begins. */
1200
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1622 times.
1625 if ((result = libmpq__source_read_at(
1201 1625 (*mpq_archive)->source,
1202 1625 (*mpq_archive)->mpq_header.hash_table_offset +
1203 1625 ((uint64_t)(*mpq_archive)->mpq_header_ex.hash_table_offset_high << 32) +
1204 1625 (uint64_t)(*mpq_archive)->archive_offset,
1205 table_data, table_bytes
1206 )) < 0) {
1207 3 goto error;
1208 }
1209
1210 /* MPQ stores the hash table encrypted with the fixed "(hash table)" key. */
1211 1622 libmpq__crypto_decrypt_block(
1212 table_data, (uint32_t)table_bytes, libmpq__crypto_hash_string("(hash table)", 0x300)
1213 );
1214 1622 result = libmpq__hash_table_decode(
1215 1622 table_data, table_bytes, (*mpq_archive)->mpq_hash,
1216 1622 (*mpq_archive)->mpq_header.hash_table_count
1217 );
1218
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1622 times.
1622 if (result < 0)
1219 ✗ goto error;
1220 1622 free(table_data);
1221 1622 table_data = NULL;
1222
1223
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1622 times.
1622 if (table_size(
1224 1622 (*mpq_archive)->mpq_header.block_table_count, LIBMPQ_BLOCK_ENTRY_WIRE_SIZE, &table_bytes
1225 ) < 0) {
1226 ✗ result = LIBMPQ_ERROR_FORMAT;
1227 ✗ goto error;
1228 }
1229
3/4
✓ Branch 0 taken 1621 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1621 times.
1622 if (table_bytes != 0 && (table_data = malloc(table_bytes)) == NULL) {
1230 ✗ result = LIBMPQ_ERROR_MALLOC;
1231 ✗ goto error;
1232 }
1233
1234 /* The block table uses the same fixed key pattern as the hash table. */
1235
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1621 times.
1622 if ((result = libmpq__source_read_at(
1236 1622 (*mpq_archive)->source,
1237 1622 (*mpq_archive)->mpq_header.block_table_offset +
1238 1622 ((uint64_t)(*mpq_archive)->mpq_header_ex.block_table_offset_high << 32) +
1239 1622 (uint64_t)(*mpq_archive)->archive_offset,
1240 table_data, table_bytes
1241 )) < 0) {
1242 1 goto error;
1243 }
1244
1245 /* MPQ stores the block table encrypted with the fixed "(block table)" key. */
1246 1621 libmpq__crypto_decrypt_block(
1247 table_data, (uint32_t)table_bytes, libmpq__crypto_hash_string("(block table)", 0x300)
1248 );
1249 1621 result = libmpq__block_table_decode(
1250 1621 table_data, table_bytes, (*mpq_archive)->mpq_block,
1251 1621 (*mpq_archive)->mpq_header.block_table_count
1252 );
1253
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1621 times.
1621 if (result < 0)
1254 ✗ goto error;
1255 1621 free(table_data);
1256 1621 table_data = NULL;
1257
1258 /* v2 block high words are optional and are loaded only when present. */
1259
2/2
✓ Branch 0 taken 494 times.
✓ Branch 1 taken 1127 times.
1621 if ((*mpq_archive)->mpq_header_ex.extended_offset > 0) {
1260
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 494 times.
494 if (table_size(
1261 494 (*mpq_archive)->mpq_header.block_table_count, LIBMPQ_BLOCK_EX_ENTRY_WIRE_SIZE,
1262 &table_bytes
1263 ) < 0) {
1264 ✗ result = LIBMPQ_ERROR_FORMAT;
1265 ✗ goto error;
1266 }
1267
2/4
✓ Branch 0 taken 494 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 494 times.
494 if (table_bytes != 0 && (table_data = malloc(table_bytes)) == NULL) {
1268 ✗ result = LIBMPQ_ERROR_MALLOC;
1269 ✗ goto error;
1270 }
1271
1272
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 493 times.
494 if ((result = libmpq__source_read_at(
1273 494 (*mpq_archive)->source,
1274 494 (*mpq_archive)->mpq_header_ex.extended_offset + (uint64_t)archive_offset,
1275 table_data, table_bytes
1276 )) < 0) {
1277
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (result == LIBMPQ_ERROR_READ)
1278 1 result = LIBMPQ_ERROR_FORMAT;
1279 1 goto error;
1280 }
1281 493 result = libmpq__block_ex_table_decode(
1282 493 table_data, table_bytes, (*mpq_archive)->mpq_block_ex,
1283 493 (*mpq_archive)->mpq_header.block_table_count
1284 );
1285
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 493 times.
493 if (result < 0)
1286 ✗ goto error;
1287 493 free(table_data);
1288 493 table_data = NULL;
1289 }
1290
1291 /* Build the compact public file-number map from existing block-table entries. */
1292
2/2
✓ Branch 0 taken 116771 times.
✓ Branch 1 taken 1620 times.
118391 for (i = 0; i < (*mpq_archive)->mpq_header.block_table_count; i++) {
1293 116771 (*mpq_archive)->mpq_map[i].block_table_diff = i - count;
1294
1295
2/2
✓ Branch 0 taken 104987 times.
✓ Branch 1 taken 11784 times.
116771 if (((*mpq_archive)->mpq_block[i].flags & LIBMPQ_FLAG_EXISTS) == 0) {
1296 104987 continue;
1297 }
1298
1299 11784 (*mpq_archive)->mpq_map[count].block_table_indices = i;
1300 11784 count++;
1301 }
1302
1303 1620 (*mpq_archive)->files = count;
1304
1305 1620 free(table_data);
1306 1620 return 0;
1307
1308 42 error:
1309
1310 /* All partially allocated reader state is released through one failure path. */
1311 42 free(table_data);
1312
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 if ((*mpq_archive)->source)
1313 42 libmpq__source_discard((*mpq_archive)->source);
1314
1315 42 free((*mpq_archive)->mpq_map);
1316 42 free((*mpq_archive)->mpq_file);
1317 42 free((*mpq_archive)->mpq_hash);
1318 42 free((*mpq_archive)->mpq_block);
1319 42 free((*mpq_archive)->mpq_block_ex);
1320 42 free((*mpq_archive)->filename);
1321 42 free(*mpq_archive);
1322
1323 42 *mpq_archive = NULL;
1324
1325 42 return result;
1326 }
1327
1328 int32_t
1329 1297 libmpq__reader_archive_open_path(
1330 mpq_archive_s **mpq_archive, const char *mpq_filename, libmpq__off_t archive_offset
1331 )
1332 {
1333 1297 mpq_source_s *source = NULL;
1334 int32_t result;
1335
1336
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1296 times.
1297 if (mpq_archive == NULL)
1337 1 return LIBMPQ_ERROR_EXIST;
1338 1296 *mpq_archive = NULL;
1339 1296 result = libmpq__source_open_file(&source, mpq_filename);
1340
1341 1291 return result == 0 ? libmpq__reader_archive_open_source(
1342 mpq_archive, mpq_filename, archive_offset, source
1343 )
1344
2/2
✓ Branch 0 taken 1291 times.
✓ Branch 1 taken 5 times.
2587 : result;
1345 }
1346
1347 int32_t
1348 137 libmpq__reader_archive_open_mpqe(
1349 mpq_archive_s **mpq_archive, const char *mpq_filename, libmpq__off_t archive_offset,
1350 const uint8_t *auth_code, size_t auth_code_size
1351 )
1352 {
1353 137 mpq_source_s *source = NULL;
1354 int32_t result;
1355
1356
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 136 times.
137 if (mpq_archive == NULL)
1357 1 return LIBMPQ_ERROR_EXIST;
1358 136 *mpq_archive = NULL;
1359 136 result = libmpq__source_open_mpqe(&source, mpq_filename, auth_code, auth_code_size);
1360
1361 132 return result == 0 ? libmpq__reader_archive_open_source(
1362 mpq_archive, mpq_filename, archive_offset, source
1363 )
1364
2/2
✓ Branch 0 taken 132 times.
✓ Branch 1 taken 4 times.
268 : result;
1365 }
1366
1367 int32_t
1368 20 libmpq__reader_archive_open_io(
1369 mpq_archive_s **mpq_archive, void *context, libmpq_read_at_fn read_at,
1370 libmpq__off_t source_size, libmpq__off_t archive_offset, const char *source_name
1371 )
1372 {
1373 20 mpq_source_s *source = NULL;
1374 int32_t result;
1375
1376
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 19 times.
20 if (mpq_archive == NULL)
1377 1 return LIBMPQ_ERROR_EXIST;
1378 19 *mpq_archive = NULL;
1379
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 18 times.
19 if (read_at == NULL)
1380 1 return LIBMPQ_ERROR_EXIST;
1381
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 17 times.
18 if (source_size < 0)
1382 1 return LIBMPQ_ERROR_SIZE;
1383
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 16 times.
17 if (archive_offset < -1)
1384 1 return LIBMPQ_ERROR_SEEK;
1385
4/4
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 9 times.
16 if (archive_offset >= 0 && (uint64_t)archive_offset > (uint64_t)source_size)
1386 1 return LIBMPQ_ERROR_SEEK;
1387 15 result = libmpq__source_open_io(&source, context, read_at, (uint64_t)source_size);
1388 15 return result == 0 ? libmpq__reader_archive_open_source(
1389 mpq_archive, source_name, archive_offset, source
1390 )
1391
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
30 : result;
1392 }
1393
1394 int32_t
1395 6 libmpq__reader_archive_open_mpqe_io(
1396 mpq_archive_s **mpq_archive, void *context, libmpq_read_at_fn read_at,
1397 libmpq__off_t source_size, libmpq__off_t archive_offset, const uint8_t *auth_code,
1398 size_t auth_code_size, const char *source_name
1399 )
1400 {
1401 6 mpq_source_s *source = NULL;
1402 int32_t result;
1403
1404
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (mpq_archive == NULL)
1405 ✗ return LIBMPQ_ERROR_EXIST;
1406 6 *mpq_archive = NULL;
1407
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (read_at == NULL)
1408 ✗ return LIBMPQ_ERROR_EXIST;
1409
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (source_size < 0)
1410 ✗ return LIBMPQ_ERROR_SIZE;
1411
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (archive_offset < -1)
1412 ✗ return LIBMPQ_ERROR_SEEK;
1413
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 3 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
6 if (archive_offset >= 0 && (uint64_t)archive_offset > (uint64_t)source_size)
1414 ✗ return LIBMPQ_ERROR_SEEK;
1415 6 result = libmpq__source_open_mpqe_io(
1416 &source, context, read_at, (uint64_t)source_size, auth_code, auth_code_size
1417 );
1418 6 return result == 0 ? libmpq__reader_archive_open_source(
1419 mpq_archive, source_name, archive_offset, source
1420 )
1421
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
12 : result;
1422 }
1423
1424 int32_t
1425 220 libmpq__reader_archive_clone(mpq_archive_s **clone, const mpq_archive_s *source)
1426 {
1427 220 mpq_source_s *clone_source = NULL;
1428 int32_t result;
1429
1430
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 220 times.
220 if (clone == NULL)
1431 ✗ return LIBMPQ_ERROR_EXIST;
1432 220 *clone = NULL;
1433
2/4
✓ Branch 0 taken 220 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 220 times.
220 if (source == NULL || source->source == NULL)
1434 ✗ return LIBMPQ_ERROR_EXIST;
1435 220 result = libmpq__source_clone(&clone_source, source->source, source->filename);
1436
3/4
✓ Branch 0 taken 220 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 208 times.
✓ Branch 3 taken 12 times.
220 if (result == 0 && source->file_identity_valid) {
1437 uint64_t device;
1438 uint64_t inode;
1439
1440 208 result = libmpq__source_file_identity(clone_source, &device, &inode);
1441
4/6
✓ Branch 0 taken 208 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 208 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 207 times.
208 if (result == 0 && (device != source->file_device || inode != source->file_inode))
1442 1 result = LIBMPQ_ERROR_EXIST;
1443
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 207 times.
208 if (result != 0)
1444 1 libmpq__source_discard(clone_source);
1445 }
1446
1447 219 return result == 0 ? libmpq__reader_archive_open_source(
1448 219 clone, source->filename, source->archive_offset, clone_source
1449 )
1450
2/2
✓ Branch 0 taken 219 times.
✓ Branch 1 taken 1 times.
439 : result;
1451 }
1452
1453 /*
1454 * Validate that a public file number maps to an extractable archive entry.
1455 * Public numbering excludes unused block-table slots, so this check protects
1456 * all later map and block-table accesses from an invalid compact index.
1457 */
1458 int32_t
1459 155967 libmpq__reader_validate_file_number(mpq_archive_s *mpq_archive, uint32_t file_number)
1460 {
1461
2/2
✓ Branch 0 taken 295 times.
✓ Branch 1 taken 155672 times.
155967 if (file_number >= mpq_archive->files) {
1462 295 return LIBMPQ_ERROR_EXIST;
1463 }
1464
1465 155672 return 0;
1466 }
1467
1468 /*
1469 * Return the number of sectors needed to represent a file entry.
1470 * Single-unit files always have one payload block; sectorized files use the
1471 * archive block size and round the unpacked length up to a complete sector.
1472 */
1473 uint32_t
1474 131011 libmpq__reader_count_file_blocks(mpq_archive_s *mpq_archive, uint32_t file_number)
1475 {
1476 131011 uint32_t block_table_index = mpq_archive->mpq_map[file_number].block_table_indices;
1477 131011 uint32_t unpacked_size = mpq_archive->mpq_block[block_table_index].unpacked_size;
1478
1479
2/2
✓ Branch 0 taken 7250 times.
✓ Branch 1 taken 123761 times.
131011 if ((mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_SINGLE) != 0) {
1480 7250 return 1;
1481 }
1482
1483 123761 return (unpacked_size + mpq_archive->block_size - 1) / mpq_archive->block_size;
1484 }
1485
1486 /*
1487 * Validate that a block number exists for the selected file entry.
1488 * The file's storage mode determines the valid range, including the special
1489 * one-block case for single-unit entries.
1490 */
1491 int32_t
1492 74126 libmpq__reader_validate_block_number(
1493 mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number
1494 )
1495 {
1496
2/2
✓ Branch 1 taken 276 times.
✓ Branch 2 taken 73850 times.
74126 if (block_number >= libmpq__reader_count_file_blocks(mpq_archive, file_number)) {
1497 276 return LIBMPQ_ERROR_EXIST;
1498 }
1499
1500 73850 return 0;
1501 }
1502
1503 /*
1504 * Return the per-block decryption seed derived from the file seed and block number.
1505 * The helper validates file and block ownership, ensures offset metadata is
1506 * available, and refuses to guess a key when anonymous decryption failed.
1507 */
1508 int32_t
1509 8411 libmpq__reader_get_block_seed(
1510 mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number, uint32_t *seed
1511 )
1512 {
1513
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8411 times.
8411 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
1514 ✗ return LIBMPQ_ERROR_EXIST;
1515 }
1516
1517
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 8411 times.
8411 if (libmpq__reader_validate_block_number(mpq_archive, file_number, block_number) < 0) {
1518 ✗ return LIBMPQ_ERROR_EXIST;
1519 }
1520
1521
1/2
✓ Branch 0 taken 8411 times.
✗ Branch 1 not taken.
8411 if (mpq_archive->mpq_file[file_number] == NULL ||
1522
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8411 times.
8411 mpq_archive->mpq_file[file_number]->packed_offset == NULL) {
1523 ✗ return LIBMPQ_ERROR_OPEN;
1524 }
1525
1526
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8411 times.
8411 if (mpq_archive->mpq_file[file_number]->packed_offset_count <= block_number + 1) {
1527 ✗ return LIBMPQ_ERROR_EXIST;
1528 }
1529
1530
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 8409 times.
8411 if (!mpq_archive->mpq_file[file_number]->seed_known) {
1531 2 return LIBMPQ_ERROR_DECRYPT;
1532 }
1533
1534 8409 *seed = mpq_archive->mpq_file[file_number]->seed + block_number;
1535
1536 8409 return 0;
1537 }
1538