GCC Code Coverage Report


Directory: src/
File: src/mpq-reader.c
Date: 2026-09-02 16:40:44
Exec Total Coverage
Lines: 204 248 82.3%
Functions: 12 12 100.0%
Branches: 95 140 67.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-crypto.h"
25 #include "mpq-endian.h"
26 #include "mpq-internal.h"
27 #include "mpq-platform.h"
28 #include "mpq-reader.h"
29
30 #include <errno.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <sys/stat.h>
34
35 /* Calculate a serialized table size while rejecting arithmetic overflow.
36 * MPQ table lengths are stored in 32-bit fields, so both native allocation
37 * size and on-disk representation must fit before the caller proceeds. */
38 static int32_t
39 291 table_size(uint32_t count, size_t item_size, size_t *size)
40 {
41
4/6
✓ Branch 0 taken 291 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 291 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 289 times.
291 if (item_size == 0 || count > SIZE_MAX / item_size || (size_t)count * item_size > UINT32_MAX) {
42 2 return LIBMPQ_ERROR_FORMAT;
43 }
44
45 289 *size = (size_t)count * item_size;
46 289 return LIBMPQ_SUCCESS;
47 }
48
49 /* Decode the fixed MPQ v1 header from its little-endian byte representation.
50 * The helper performs no validation; callers validate version, offsets, and
51 * counts after all header fields have been loaded. */
52 static void
53 71 decode_mpq_header(mpq_header_s *header, const uint8_t *raw)
54 {
55 71 header->mpq_magic = libmpq__load_le32(raw + 0);
56 71 header->header_size = libmpq__load_le32(raw + 4);
57 71 header->archive_size = libmpq__load_le32(raw + 8);
58 71 header->version = libmpq__load_le16(raw + 12);
59 71 header->block_size = libmpq__load_le16(raw + 14);
60 71 header->hash_table_offset = libmpq__load_le32(raw + 16);
61 71 header->block_table_offset = libmpq__load_le32(raw + 20);
62 71 header->hash_table_count = libmpq__load_le32(raw + 24);
63 71 header->block_table_count = libmpq__load_le32(raw + 28);
64 71 }
65
66 /* Decode the optional MPQ v2 high-offset header extension.
67 * Its fields extend table and archive offsets without changing the v1 header
68 * layout, so they are loaded separately when the archive version requires it. */
69 static void
70 25 decode_mpq_header_ex(mpq_header_ex_s *header, const uint8_t *raw)
71 {
72 25 header->extended_offset = libmpq__load_le64(raw + 0);
73 25 header->hash_table_offset_high = libmpq__load_le16(raw + 8);
74 25 header->block_table_offset_high = libmpq__load_le16(raw + 10);
75 25 }
76
77 /* Decode the encrypted hash-table entries into native archive structures.
78 * Each entry is read field-by-field to avoid alignment and host-endian
79 * assumptions when the library runs on a different architecture. */
80 static void
81 64 decode_mpq_hash_table(mpq_hash_s *table, const uint8_t *raw, uint32_t count)
82 {
83 uint32_t i;
84
85
3/4
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 63 times.
64 if (table == 0 || raw == 0)
86 1 return;
87
2/2
✓ Branch 0 taken 4032 times.
✓ Branch 1 taken 63 times.
4095 for (i = 0; i < count; i++) {
88 4032 const uint8_t *entry = raw + i * sizeof(mpq_hash_s);
89
90 4032 table[i].hash_a = libmpq__load_le32(entry + 0);
91 4032 table[i].hash_b = libmpq__load_le32(entry + 4);
92 4032 table[i].locale = libmpq__load_le16(entry + 8);
93 4032 table[i].platform = libmpq__load_le16(entry + 10);
94 4032 table[i].block_table_index = libmpq__load_le32(entry + 12);
95 }
96 }
97
98 /* Decode the fixed-width block table used by MPQ v1 and v2 archives.
99 * The high offset words are handled separately by the extended-table helper. */
100 static void
101 63 decode_mpq_block_table(mpq_block_s *table, const uint8_t *raw, uint32_t count)
102 {
103 uint32_t i;
104
105
2/4
✓ Branch 0 taken 63 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 63 times.
63 if (table == 0 || raw == 0)
106 return;
107
2/2
✓ Branch 0 taken 2016 times.
✓ Branch 1 taken 63 times.
2079 for (i = 0; i < count; i++) {
108 2016 const uint8_t *entry = raw + i * sizeof(mpq_block_s);
109
110 2016 table[i].offset = libmpq__load_le32(entry + 0);
111 2016 table[i].packed_size = libmpq__load_le32(entry + 4);
112 2016 table[i].unpacked_size = libmpq__load_le32(entry + 8);
113 2016 table[i].flags = libmpq__load_le32(entry + 12);
114 }
115 }
116
117 /* Decode the optional high 16-bit offset table for MPQ v2 block entries.
118 * The caller has already positioned the input at the extension table and
119 * supplies storage sized for the block-table entry count. */
120 static void
121 25 decode_mpq_block_ex_table(mpq_block_ex_s *table, const uint8_t *raw, uint32_t count)
122 {
123 uint32_t i;
124
125
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 25 times.
25 if (table == 0 || raw == 0)
126 return;
127
2/2
✓ Branch 0 taken 800 times.
✓ Branch 1 taken 25 times.
825 for (i = 0; i < count; i++) {
128 800 table[i].offset_high = libmpq__load_le16(raw + i * sizeof(mpq_block_ex_s));
129 }
130 }
131
132 /* Decode a packed array of little-endian 32-bit values in place.
133 * This is used for sector offset tables whose serialized representation is
134 * independent of the host CPU's byte order. */
135 void
136 422 libmpq__reader_decode_uint32_table(uint32_t *table, const uint8_t *raw, uint32_t count)
137 {
138 uint32_t i;
139
140
2/2
✓ Branch 0 taken 1160 times.
✓ Branch 1 taken 422 times.
1582 for (i = 0; i < count; i++) {
141 1160 table[i] = libmpq__load_le32(raw + i * sizeof(uint32_t));
142 }
143 422 }
144
145 /* Open an MPQ archive path and prepare decoded metadata for later operations.
146 * The routine locates the header, loads and decrypts all metadata tables, and
147 * builds the compact file map used by the public archive and block APIs. */
148 int32_t
149 75 libmpq__reader_archive_open_path(
150 mpq_archive_s **mpq_archive, const char *mpq_filename, libmpq__off_t archive_offset
151 )
152 {
153
154 /* Archive table counters and status used while building the file map. */
155 75 uint32_t i = 0;
156 75 uint32_t count = 0;
157 75 int32_t result = 0;
158 75 uint32_t header_search = FALSE;
159 uint8_t header_data[sizeof(mpq_header_s)];
160 uint8_t header_ex_data[sizeof(mpq_header_ex_s)];
161 75 uint8_t *table_data = NULL;
162 75 size_t table_bytes = 0;
163 libmpq__off_t file_size;
164
165 /* A sentinel offset requests the embedded-archive scan used by readers. */
166
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 75 times.
75 if (archive_offset == -1) {
167 archive_offset = 0;
168 header_search = TRUE;
169 }
170
171
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 75 times.
75 if ((*mpq_archive = calloc(1, sizeof(mpq_archive_s))) == NULL) {
172 return LIBMPQ_ERROR_MALLOC;
173 }
174
175 75 (*mpq_archive)->filename = malloc(strlen(mpq_filename) + 1);
176
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 75 times.
75 if ((*mpq_archive)->filename == NULL) {
177 result = LIBMPQ_ERROR_MALLOC;
178 goto error;
179 }
180 75 memcpy((*mpq_archive)->filename, mpq_filename, strlen(mpq_filename) + 1);
181
182 #if !defined(_WIN32) && !defined(_WIN64)
183 {
184 struct stat file_status;
185
186
2/2
✓ Branch 1 taken 73 times.
✓ Branch 2 taken 2 times.
75 if (stat(mpq_filename, &file_status) == 0) {
187 73 (*mpq_archive)->file_device = (uint64_t)file_status.st_dev;
188 73 (*mpq_archive)->file_inode = (uint64_t)file_status.st_ino;
189 73 (*mpq_archive)->file_identity_valid = TRUE;
190 }
191 }
192 #endif
193
194 /* Open the archive file for binary reads. */
195 75 errno = 0;
196
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 73 times.
75 if (((*mpq_archive)->fp = fopen(mpq_filename, "rb")) == NULL) {
197
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 result = errno == ENOENT ? LIBMPQ_ERROR_EXIST : LIBMPQ_ERROR_OPEN;
198 2 goto error;
199 }
200
201
2/4
✓ Branch 1 taken 73 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 73 times.
146 if (fseeko((*mpq_archive)->fp, 0, SEEK_END) < 0 ||
202 73 (file_size = (libmpq__off_t)ftello((*mpq_archive)->fp)) < 0) {
203 result = LIBMPQ_ERROR_SEEK;
204 goto error;
205 }
206 73 (*mpq_archive)->file_size = (uint64_t)file_size;
207
208 73 (*mpq_archive)->mpq_header.mpq_magic = 0;
209 73 (*mpq_archive)->files = 0;
210
211 /* Probe the requested location, or advance in 512-byte steps for embedded archives. */
212 while (TRUE) {
213 73 (*mpq_archive)->mpq_header.mpq_magic = 0;
214
215
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 72 times.
73 if (fseeko((*mpq_archive)->fp, archive_offset, SEEK_SET) < 0) {
216 1 result = LIBMPQ_ERROR_SEEK;
217 1 goto error;
218 }
219
220
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 71 times.
72 if (fread(header_data, 1, sizeof(header_data), (*mpq_archive)->fp) != sizeof(header_data)) {
221 1 result = LIBMPQ_ERROR_FORMAT;
222 1 goto error;
223 }
224
225 71 decode_mpq_header(&(*mpq_archive)->mpq_header, header_data);
226
227
1/2
✓ Branch 0 taken 71 times.
✗ Branch 1 not taken.
71 if ((*mpq_archive)->mpq_header.mpq_magic == LIBMPQ_HEADER) {
228
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 26 times.
71 if ((*mpq_archive)->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_ONE) {
229
230 /* Protected archives may store a bogus header size; normalize it locally. */
231
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 45 times.
45 if ((*mpq_archive)->mpq_header.header_size != sizeof(mpq_header_s)) {
232 (*mpq_archive)->mpq_header.header_size = sizeof(mpq_header_s);
233 }
234 }
235
236
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 45 times.
71 if ((*mpq_archive)->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_TWO) {
237
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if ((*mpq_archive)->mpq_header.header_size !=
238 sizeof(mpq_header_s) + sizeof(mpq_header_ex_s)) {
239 (*mpq_archive)->mpq_header.header_size =
240 sizeof(mpq_header_s) + sizeof(mpq_header_ex_s);
241 }
242 }
243
244 71 break;
245 }
246
247 if (!header_search) {
248 result = LIBMPQ_ERROR_FORMAT;
249 goto error;
250 }
251 archive_offset += 512;
252 }
253
254
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 69 times.
71 if ((*mpq_archive)->mpq_header.block_size > 22) {
255 2 result = LIBMPQ_ERROR_FORMAT;
256 2 goto error;
257 }
258
259 69 (*mpq_archive)->block_size = 512U << (*mpq_archive)->mpq_header.block_size;
260 69 (*mpq_archive)->archive_offset = archive_offset;
261
262
2/2
✓ Branch 1 taken 67 times.
✓ Branch 2 taken 2 times.
69 if (table_size((*mpq_archive)->mpq_header.hash_table_count, sizeof(mpq_hash_s), &table_bytes) <
263 67 0 ||
264
2/4
✓ Branch 0 taken 67 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 67 times.
✗ Branch 3 not taken.
134 (uint64_t)table_bytes > (uint64_t)file_size ||
265 67 table_size(
266 67 (*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_block_s), &table_bytes
267 67 ) < 0 ||
268
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 67 times.
67 (uint64_t)table_bytes > (uint64_t)file_size) {
269 2 result = LIBMPQ_ERROR_FORMAT;
270 2 goto error;
271 }
272
273 /* MPQ v2 stores high table offsets in a separate extension immediately after v1. */
274
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 41 times.
67 if ((*mpq_archive)->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_TWO) {
275
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
26 if (fseeko((*mpq_archive)->fp, sizeof(mpq_header_s) + archive_offset, SEEK_SET) < 0) {
276 result = LIBMPQ_ERROR_SEEK;
277 goto error;
278 }
279
280
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 25 times.
26 if (fread(header_ex_data, 1, sizeof(header_ex_data), (*mpq_archive)->fp) !=
281 sizeof(header_ex_data)) {
282 1 result = LIBMPQ_ERROR_FORMAT;
283 1 goto error;
284 }
285
286 25 decode_mpq_header_ex(&(*mpq_archive)->mpq_header_ex, header_ex_data);
287 }
288
289 /* Metadata tables are decoded once and kept with the archive handle for later lookups. */
290 66 if (((*mpq_archive)->mpq_block =
291
1/2
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
66 calloc((*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_block_s))) == NULL ||
292 66 ((*mpq_archive)->mpq_block_ex =
293
1/2
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
66 calloc((*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_block_ex_s))) ==
294 66 NULL ||
295 66 ((*mpq_archive)->mpq_hash =
296
1/2
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
66 calloc((*mpq_archive)->mpq_header.hash_table_count, sizeof(mpq_hash_s))) == NULL ||
297 66 ((*mpq_archive)->mpq_file =
298
1/2
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
66 calloc((*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_file_s *))) == NULL ||
299 66 ((*mpq_archive)->mpq_map =
300
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 66 times.
66 calloc((*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_map_s))) == NULL) {
301 result = LIBMPQ_ERROR_MALLOC;
302 goto error;
303 }
304
305
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 66 times.
66 if (table_size((*mpq_archive)->mpq_header.hash_table_count, sizeof(mpq_hash_s), &table_bytes) <
306 0) {
307 result = LIBMPQ_ERROR_FORMAT;
308 goto error;
309 }
310
3/4
✓ Branch 0 taken 65 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 65 times.
66 if (table_bytes != 0 && (table_data = malloc(table_bytes)) == NULL) {
311 result = LIBMPQ_ERROR_MALLOC;
312 goto error;
313 }
314
315 /* Locate, read, decrypt, and decode the hash table before file lookup begins. */
316
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 66 times.
66 if (fseeko(
317 66 (*mpq_archive)->fp,
318 66 (*mpq_archive)->mpq_header.hash_table_offset +
319 66 (((long long)((*mpq_archive)->mpq_header_ex.hash_table_offset_high)) << 32) +
320 66 (*mpq_archive)->archive_offset,
321 SEEK_SET
322 ) < 0) {
323 result = LIBMPQ_ERROR_SEEK;
324 goto error;
325 }
326
327
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 64 times.
66 if (fread(table_data, 1, table_bytes, (*mpq_archive)->fp) != table_bytes) {
328 2 result = LIBMPQ_ERROR_READ;
329 2 goto error;
330 }
331
332 /* MPQ stores the hash table encrypted with the fixed "(hash table)" key. */
333 64 libmpq__crypto_decrypt_block(
334 table_data, (uint32_t)table_bytes, libmpq__crypto_hash_string("(hash table)", 0x300)
335 );
336 64 decode_mpq_hash_table(
337 64 (*mpq_archive)->mpq_hash, table_data, (*mpq_archive)->mpq_header.hash_table_count
338 );
339 64 free(table_data);
340 64 table_data = NULL;
341
342
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 64 times.
64 if (table_size(
343 64 (*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_block_s), &table_bytes
344 ) < 0) {
345 result = LIBMPQ_ERROR_FORMAT;
346 goto error;
347 }
348
2/4
✓ Branch 0 taken 64 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 64 times.
64 if (table_bytes != 0 && (table_data = malloc(table_bytes)) == NULL) {
349 result = LIBMPQ_ERROR_MALLOC;
350 goto error;
351 }
352
353 /* The block table uses the same fixed key pattern as the hash table. */
354
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 64 times.
64 if (fseeko(
355 64 (*mpq_archive)->fp,
356 64 (*mpq_archive)->mpq_header.block_table_offset +
357 64 (((long long)((*mpq_archive)->mpq_header_ex.block_table_offset_high)) << 32) +
358 64 (*mpq_archive)->archive_offset,
359 SEEK_SET
360 ) < 0) {
361 result = LIBMPQ_ERROR_SEEK;
362 goto error;
363 }
364
365
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 63 times.
64 if (fread(table_data, 1, table_bytes, (*mpq_archive)->fp) != table_bytes) {
366 1 result = LIBMPQ_ERROR_READ;
367 1 goto error;
368 }
369
370 /* MPQ stores the block table encrypted with the fixed "(block table)" key. */
371 63 libmpq__crypto_decrypt_block(
372 table_data, (uint32_t)table_bytes, libmpq__crypto_hash_string("(block table)", 0x300)
373 );
374 63 decode_mpq_block_table(
375 63 (*mpq_archive)->mpq_block, table_data, (*mpq_archive)->mpq_header.block_table_count
376 );
377 63 free(table_data);
378 63 table_data = NULL;
379
380 /* v2 block high words are optional and are loaded only when present. */
381
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 38 times.
63 if ((*mpq_archive)->mpq_header_ex.extended_offset > 0) {
382
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
25 if (fseeko(
383 25 (*mpq_archive)->fp, (*mpq_archive)->mpq_header_ex.extended_offset + archive_offset,
384 SEEK_SET
385 ) < 0) {
386 result = LIBMPQ_ERROR_SEEK;
387 goto error;
388 }
389
390
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
25 if (table_size(
391 25 (*mpq_archive)->mpq_header.block_table_count, sizeof(mpq_block_ex_s), &table_bytes
392 ) < 0) {
393 result = LIBMPQ_ERROR_FORMAT;
394 goto error;
395 }
396
2/4
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 25 times.
25 if (table_bytes != 0 && (table_data = malloc(table_bytes)) == NULL) {
397 result = LIBMPQ_ERROR_MALLOC;
398 goto error;
399 }
400
401
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 25 times.
25 if (fread(table_data, 1, table_bytes, (*mpq_archive)->fp) != table_bytes) {
402 result = LIBMPQ_ERROR_FORMAT;
403 goto error;
404 }
405 25 decode_mpq_block_ex_table(
406 25 (*mpq_archive)->mpq_block_ex, table_data, (*mpq_archive)->mpq_header.block_table_count
407 );
408 25 free(table_data);
409 25 table_data = NULL;
410 }
411
412 /* Build the compact public file-number map from existing block-table entries. */
413
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++) {
414 2016 (*mpq_archive)->mpq_map[i].block_table_diff = i - count;
415
416
2/2
✓ Branch 0 taken 1401 times.
✓ Branch 1 taken 615 times.
2016 if (((*mpq_archive)->mpq_block[i].flags & LIBMPQ_FLAG_EXISTS) == 0) {
417 1401 continue;
418 }
419
420 615 (*mpq_archive)->mpq_map[count].block_table_indices = i;
421 615 count++;
422 }
423
424 63 (*mpq_archive)->files = count;
425
426 63 free(table_data);
427 63 return LIBMPQ_SUCCESS;
428
429 12 error:
430
431 /* All partially allocated reader state is released through one failure path. */
432 12 free(table_data);
433
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 2 times.
12 if ((*mpq_archive)->fp)
434 10 fclose((*mpq_archive)->fp);
435
436 12 free((*mpq_archive)->mpq_map);
437 12 free((*mpq_archive)->mpq_file);
438 12 free((*mpq_archive)->mpq_hash);
439 12 free((*mpq_archive)->mpq_block);
440 12 free((*mpq_archive)->mpq_block_ex);
441 12 free((*mpq_archive)->filename);
442 12 free(*mpq_archive);
443
444 12 *mpq_archive = NULL;
445
446 12 return result;
447 }
448
449 /* Validate that a public file number maps to an extractable archive entry.
450 * Public numbering excludes unused block-table slots, so this check protects
451 * all later map and block-table accesses from an invalid compact index. */
452 int32_t
453 9448 libmpq__reader_validate_file_number(mpq_archive_s *mpq_archive, uint32_t file_number)
454 {
455
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 9438 times.
9448 if (file_number >= mpq_archive->files) {
456 10 return LIBMPQ_ERROR_EXIST;
457 }
458
459 9438 return LIBMPQ_SUCCESS;
460 }
461
462 /* Return the number of sectors needed to represent a file entry.
463 * Single-unit files always have one payload block; sectorized files use the
464 * archive block size and round the unpacked length up to a complete sector. */
465 uint32_t
466 6047 libmpq__reader_count_file_blocks(mpq_archive_s *mpq_archive, uint32_t file_number)
467 {
468 6047 uint32_t block_table_index = mpq_archive->mpq_map[file_number].block_table_indices;
469 6047 uint32_t unpacked_size = mpq_archive->mpq_block[block_table_index].unpacked_size;
470
471
2/2
✓ Branch 0 taken 292 times.
✓ Branch 1 taken 5755 times.
6047 if ((mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_SINGLE) != 0) {
472 292 return 1;
473 }
474
475 5755 return (unpacked_size + mpq_archive->block_size - 1) / mpq_archive->block_size;
476 }
477
478 /* Validate that a block number exists for the selected file entry.
479 * The file's storage mode determines the valid range, including the special
480 * one-block case for single-unit entries. */
481 int32_t
482 3120 libmpq__reader_validate_block_number(
483 mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number
484 )
485 {
486
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3118 times.
3120 if (block_number >= libmpq__reader_count_file_blocks(mpq_archive, file_number)) {
487 2 return LIBMPQ_ERROR_EXIST;
488 }
489
490 3118 return LIBMPQ_SUCCESS;
491 }
492
493 /* Return the per-block decryption seed derived from the file seed and block number.
494 * The helper validates file and block ownership, ensures offset metadata is
495 * available, and refuses to guess a key when anonymous decryption failed. */
496 int32_t
497 286 libmpq__reader_get_block_seed(
498 mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number, uint32_t *seed
499 )
500 {
501
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 286 times.
286 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
502 return LIBMPQ_ERROR_EXIST;
503 }
504
505
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 286 times.
286 if (libmpq__reader_validate_block_number(mpq_archive, file_number, block_number) < 0) {
506 return LIBMPQ_ERROR_EXIST;
507 }
508
509
1/2
✓ Branch 0 taken 286 times.
✗ Branch 1 not taken.
286 if (mpq_archive->mpq_file[file_number] == NULL ||
510
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
286 mpq_archive->mpq_file[file_number]->packed_offset == NULL) {
511 return LIBMPQ_ERROR_OPEN;
512 }
513
514
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
286 if (mpq_archive->mpq_file[file_number]->packed_offset_count <= block_number + 1) {
515 return LIBMPQ_ERROR_EXIST;
516 }
517
518
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 286 times.
286 if (!mpq_archive->mpq_file[file_number]->seed_known) {
519 return LIBMPQ_ERROR_DECRYPT;
520 }
521
522 286 *seed = mpq_archive->mpq_file[file_number]->seed + block_number;
523
524 286 return LIBMPQ_SUCCESS;
525 }
526