GCC Code Coverage Report


Directory: src/
File: src/mpq-api.c
Date: 2026-10-06 13:47:41
Exec Total Coverage
Lines: 283 293 96.6%
Functions: 65 65 100.0%
Branches: 93 114 81.6%

Line Branch Exec Source
1 /*
2 * mpq-api.c -- public archive, file and block operations.
3 *
4 * Copyright (c) 2003-2026 Maik Broemme <mbroemme@libmpq.org>
5 *
6 * This file is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU Lesser General Public License as published by
8 * the Free Software Foundation; either version 2.1 of the License, or
9 * (at your option) any later version.
10 *
11 * This file is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU Lesser General Public License for more details.
15 *
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this file; if not, see <https://www.gnu.org/licenses/>.
18 */
19
20 #ifdef HAVE_CONFIG_H
21 #include "config.h"
22 #endif
23
24 /* Export only public wrappers when building a Windows DLL. */
25 #if defined(_WIN32) && defined(DLL_EXPORT)
26 #define LIBMPQ_API __declspec(dllexport)
27 #endif
28
29 #include <libmpq/mpq.h>
30
31 #include "mpq-archive.h"
32 #include "mpq-attributes.h"
33 #include "mpq-block.h"
34 #include "mpq-compression.h"
35 #include "mpq-crypto.h"
36 #include "mpq-endian.h"
37 #include "mpq-patch-writer.h"
38 #include "mpq-reader.h"
39 #include "mpq-rsa.h"
40 #include "mpq-signature.h"
41 #include "mpq-source.h"
42 #include "mpq-stream.h"
43 #include "mpq-update.h"
44 #include "mpq-verify.h"
45 #include "mpq-writer.h"
46
47 #include <stddef.h>
48 #include <stdlib.h>
49 #include <string.h>
50
51 /*
52 * C99-compatible compile-time checks of native public value layouts.
53 * These describe host-side structs, not the little-endian disk format.
54 */
55 #define LIBMPQ_ABI_SIZE(type, bytes) \
56 typedef char type##_size_check[sizeof(type) == (bytes) ? 1 : -1]
57 #define LIBMPQ_ABI_FIELD(type, field, offset, bytes) \
58 typedef char type##_##field##_layout_check \
59 [offsetof(type, field) == (offset) && sizeof(((type *)0)->field) == (bytes) ? 1 : -1]
60
61 LIBMPQ_ABI_SIZE(mpq_archive_create_options_s, 20);
62 LIBMPQ_ABI_FIELD(mpq_archive_create_options_s, version, 0, 4);
63 LIBMPQ_ABI_FIELD(mpq_archive_create_options_s, max_files, 4, 4);
64 LIBMPQ_ABI_FIELD(mpq_archive_create_options_s, sector_size, 8, 4);
65 LIBMPQ_ABI_FIELD(mpq_archive_create_options_s, flags, 12, 4);
66 LIBMPQ_ABI_FIELD(mpq_archive_create_options_s, attributes, 16, 4);
67
68 LIBMPQ_ABI_SIZE(mpq_file_options_s, 16);
69 LIBMPQ_ABI_FIELD(mpq_file_options_s, flags, 0, 4);
70 LIBMPQ_ABI_FIELD(mpq_file_options_s, compression_first, 4, 4);
71 LIBMPQ_ABI_FIELD(mpq_file_options_s, compression_next, 8, 4);
72 LIBMPQ_ABI_FIELD(mpq_file_options_s, locale, 12, 2);
73 LIBMPQ_ABI_FIELD(mpq_file_options_s, platform, 14, 2);
74
75 LIBMPQ_ABI_SIZE(mpq_file_attributes_s, 40);
76 LIBMPQ_ABI_FIELD(mpq_file_attributes_s, flags, 0, 4);
77 LIBMPQ_ABI_FIELD(mpq_file_attributes_s, crc32, 4, 4);
78 LIBMPQ_ABI_FIELD(mpq_file_attributes_s, filetime, 8, 8);
79 LIBMPQ_ABI_FIELD(mpq_file_attributes_s, md5, 16, 16);
80 LIBMPQ_ABI_FIELD(mpq_file_attributes_s, patch_bit, 32, 4);
81 LIBMPQ_ABI_FIELD(mpq_file_attributes_s, reserved, 36, 4);
82
83 #undef LIBMPQ_ABI_FIELD
84 #undef LIBMPQ_ABI_SIZE
85
86 /* Error strings indexed by the negated libmpq error code. */
87 static const char *libmpq_error_strings[] = { "success",
88 "open error on file",
89 "close error on file",
90 "lseek error on file",
91 "read error on file",
92 "write error on file",
93 "memory allocation error",
94 "format error",
95 "init() wasn't called",
96 "buffer size is too small",
97 "archive, file, or block does not exist",
98 "we don't know the decryption seed",
99 "error on unpacking file" };
100
101 /*
102 * Return the configured libmpq package version.
103 * The returned pointer refers to immutable library storage and remains valid
104 * for the lifetime of the process.
105 */
106 const char *
107 2 libmpq__version(void)
108 {
109 2 return VERSION;
110 }
111
112 /* Query whether writer compression is allowed without restricting archive decoding. */
113 int32_t
114 5439 libmpq__archive_compression_allowed(
115 uint32_t archive_version, uint32_t compression_mask, libmpq_compression_policy_t policy
116 )
117 {
118 5439 return libmpq__compression_allowed(archive_version, compression_mask, policy);
119 }
120
121 /* Signature queries and verification are explicit and do not affect reads. */
122 int32_t
123 59 libmpq__archive_signatures(mpq_archive_s *archive, uint32_t *signatures)
124 {
125 59 return libmpq__signature_detect(archive, signatures);
126 }
127
128 int32_t
129 67 libmpq__archive_verify(
130 mpq_archive_s *archive, uint32_t flags, const uint8_t *key, size_t key_size,
131 uint32_t *mismatches
132 )
133 {
134 67 return libmpq__signature_verify(archive, flags, key, key_size, mismatches);
135 }
136
137 int32_t
138 46 libmpq__archive_sign(mpq_archive_s *archive, uint32_t type, const uint8_t *key, size_t key_size)
139 {
140 46 return libmpq__signature_configure(archive, type, key, key_size);
141 }
142
143 /*
144 * Return the optional attributes header flags without affecting normal reads.
145 * Absence and malformed metadata are distinct negative results.
146 */
147 int32_t
148 3835 libmpq__archive_attributes(mpq_archive_s *archive, uint32_t *flags)
149 {
150 int32_t result;
151
1/2
✓ Branch 0 taken 3835 times.
✗ Branch 1 not taken.
3835 if (flags != NULL)
152 3835 *flags = 0;
153
3/4
✓ Branch 0 taken 3834 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3834 times.
3835 if (archive == NULL || flags == NULL)
154 1 return LIBMPQ_ERROR_EXIST;
155
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 3802 times.
3834 if (archive->write_mode)
156 32 return LIBMPQ_ERROR_NOT_INITIALIZED;
157 3802 result = libmpq__attributes_load(archive);
158
2/2
✓ Branch 0 taken 1771 times.
✓ Branch 1 taken 2031 times.
3802 if (result == 0)
159 1771 *flags = archive->attributes->flags;
160 3802 return result;
161 }
162
163 /*
164 * Translate a public file number before reading stored per-block attributes.
165 * Availability flags distinguish absent legacy values from legitimate zeroes.
166 */
167 int32_t
168 3696 libmpq__file_attributes(mpq_archive_s *archive, uint32_t number, mpq_file_attributes_s *attributes)
169 {
170 uint32_t flags;
171 int32_t result;
172
1/2
✓ Branch 0 taken 3696 times.
✗ Branch 1 not taken.
3696 if (attributes != NULL)
173 3696 memset(attributes, 0, sizeof(*attributes));
174
3/4
✓ Branch 0 taken 3695 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3695 times.
3696 if (archive == NULL || attributes == NULL)
175 1 return LIBMPQ_ERROR_EXIST;
176
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3695 times.
3695 if (archive->write_mode)
177 ✗ return LIBMPQ_ERROR_NOT_INITIALIZED;
178
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 3695 times.
3695 if (libmpq__reader_validate_file_number(archive, number) != 0)
179 ✗ return LIBMPQ_ERROR_EXIST;
180 3695 result = libmpq__archive_attributes(archive, &flags);
181
2/2
✓ Branch 0 taken 1671 times.
✓ Branch 1 taken 2024 times.
3695 if (result == 0)
182 1671 libmpq__attributes_get(
183 1671 archive->attributes, archive->mpq_map[number].block_table_indices, attributes
184 );
185 3695 return result;
186 }
187
188 /* Explicit verification is implemented separately from the public API facade. */
189 int32_t
190 1484 libmpq__file_verify(
191 mpq_archive_s *archive, uint32_t file_number, uint32_t verify_flags, uint32_t *mismatches
192 )
193 {
194 1484 return libmpq__verify_file(archive, file_number, verify_flags, mismatches);
195 }
196
197 /* Explicitly verify one sector and return its stored checksum. */
198 int32_t
199 1784 libmpq__block_verify(
200 mpq_archive_s *archive, uint32_t file_number, uint32_t block_number, uint32_t *checksum,
201 uint32_t *mismatches
202 )
203 {
204 1784 return libmpq__verify_block(archive, file_number, block_number, checksum, mismatches);
205 }
206
207 /*
208 * Supply an explicit timestamp for an unfinished source file.
209 * The writer owns the value; no filesystem timestamp is consulted.
210 */
211 int32_t
212 33 libmpq__writer_timestamp(mpq_writer_s *writer, uint64_t filetime)
213 {
214 33 return libmpq__writer_file_timestamp(writer, filetime);
215 }
216
217 /*
218 * Translate a libmpq return code into a static diagnostic string.
219 * Valid codes index an internal immutable table; invalid positive or out-of-
220 * range negative values return NULL instead of reading outside that table.
221 */
222 const char *
223 15 libmpq__strerror(int32_t return_code)
224 {
225
226 /* Only negative libmpq error codes and zero are valid table indexes. */
227
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 1 times.
15 if (-return_code < 0 ||
228
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 13 times.
14 (size_t)-return_code >= sizeof(libmpq_error_strings) / sizeof(libmpq_error_strings[0]))
229 2 return NULL;
230
231 /* Return the static string owned by the library. */
232 13 return libmpq_error_strings[-return_code];
233 }
234
235 /*
236 * Create an MPQ archive through the internal writer implementation.
237 * This public facade preserves the stable API while keeping archive layout
238 * and file-table construction in the writer module.
239 */
240 int32_t
241 428 libmpq__archive_create(
242 mpq_archive_s **out, const char *path, const mpq_archive_create_options_s *options
243 )
244 {
245 428 return libmpq__writer_archive_create(out, path, options);
246 }
247
248 /* Create an MPQE-wrapped archive through the normal writer and private final transform. */
249 int32_t
250 84 libmpq__archive_create_mpqe(
251 mpq_archive_s **out, const char *path, const uint8_t *auth_code, size_t auth_code_size,
252 const mpq_archive_create_options_s *options
253 )
254 {
255 84 return libmpq__writer_archive_create_mpqe(out, path, auth_code, auth_code_size, options);
256 }
257
258 int32_t
259 35 libmpq__update_begin(mpq_update_s **update, const char *path)
260 {
261 35 mpq_archive_s *archive = NULL;
262 35 int32_t result = libmpq__update_transaction_begin(update, path);
263
264
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 35 times.
35 if (result != 0)
265 ✗ return result;
266 35 result = libmpq__archive_open(&archive, libmpq__update_path(*update), -1);
267
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 2 times.
35 if (archive != NULL) {
268 33 int32_t close_result = libmpq__archive_close(archive);
269
270
1/2
✓ Branch 0 taken 33 times.
✗ Branch 1 not taken.
33 if (result == 0)
271 33 result = close_result;
272 }
273
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 33 times.
35 if (result != 0) {
274 2 (void)libmpq__update_transaction_abort(*update);
275 2 *update = NULL;
276 }
277 35 return result;
278 }
279
280 /* Begin an MPQE transaction with explicit caller authentication. */
281 int32_t
282 14 libmpq__update_begin_mpqe(
283 mpq_update_s **update, const char *path, const uint8_t *auth_code, size_t auth_code_size
284 )
285 {
286 14 return libmpq__update_transaction_begin_mpqe(update, path, auth_code, auth_code_size);
287 }
288
289 int32_t
290 24 libmpq__update_replace_data(
291 mpq_update_s *update, const char *filename, const uint8_t *data, libmpq__off_t size,
292 const mpq_file_options_s *options
293 )
294 {
295 24 return libmpq__update_transaction_replace(update, filename, data, size, NULL, options);
296 }
297
298 int32_t
299 6 libmpq__update_replace_path(
300 mpq_update_s *update, const char *filename, const char *source_path,
301 const mpq_file_options_s *options
302 )
303 {
304 6 return libmpq__update_transaction_replace(update, filename, NULL, 0, source_path, options);
305 }
306
307 int32_t
308 9 libmpq__update_remove(mpq_update_s *update, const char *filename)
309 {
310 9 return libmpq__update_transaction_remove(update, filename);
311 }
312
313 int32_t
314 13 libmpq__update_rename(mpq_update_s *update, const char *old_filename, const char *new_filename)
315 {
316 13 return libmpq__update_transaction_rename(update, old_filename, new_filename);
317 }
318
319 int32_t
320 32 libmpq__update_commit(mpq_update_s *update)
321 {
322 32 mpq_archive_s *archive = NULL;
323 int32_t result;
324
325
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 31 times.
32 if (update == NULL)
326 1 return LIBMPQ_ERROR_EXIST;
327 31 result = libmpq__archive_open(&archive, libmpq__update_path(update), -1);
328
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 2 times.
31 if (archive != NULL) {
329 29 int32_t close_result = libmpq__archive_close(archive);
330
331
1/2
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
29 if (result == 0)
332 29 result = close_result;
333 }
334
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 29 times.
31 if (result != 0) {
335 2 (void)libmpq__update_transaction_abort(update);
336 2 return result;
337 }
338 29 return libmpq__update_transaction_commit(update);
339 }
340
341 int32_t
342 16 libmpq__update_abort(mpq_update_s *update)
343 {
344 16 return libmpq__update_transaction_abort(update);
345 }
346
347 int32_t
348 12 libmpq__patch_begin(mpq_patch_s **patch, const char *base_archive, const char *output_patch)
349 {
350 12 return libmpq__patch_writer_begin(patch, base_archive, output_patch);
351 }
352
353 int32_t
354 7 libmpq__patch_begin_mpqe(
355 mpq_patch_s **patch, const char *base_archive, const char *output_patch,
356 const uint8_t *auth_code, size_t auth_code_size
357 )
358 {
359 7 return libmpq__patch_writer_begin_mpqe(
360 patch, base_archive, output_patch, auth_code, auth_code_size
361 );
362 }
363
364 int32_t
365 30 libmpq__patch_sign(
366 mpq_patch_s *patch, uint32_t signature_type, const uint8_t *private_key, size_t private_key_size
367 )
368 {
369 30 return libmpq__patch_writer_sign(patch, signature_type, private_key, private_key_size);
370 }
371
372 int32_t
373 12 libmpq__patch_replace_data(
374 mpq_patch_s *patch, const char *filename, const uint8_t *data, libmpq__off_t size,
375 const mpq_file_options_s *options
376 )
377 {
378 12 return libmpq__patch_writer_replace(patch, filename, data, size, options);
379 }
380
381 int32_t
382 3 libmpq__patch_replace_path(
383 mpq_patch_s *patch, const char *filename, const char *source_path,
384 const mpq_file_options_s *options
385 )
386 {
387 3 return libmpq__patch_writer_replace_path(patch, filename, source_path, options);
388 }
389
390 int32_t
391 7 libmpq__patch_remove(mpq_patch_s *patch, const char *filename)
392 {
393 7 return libmpq__patch_writer_remove(patch, filename);
394 }
395
396 int32_t
397 14 libmpq__patch_finish(mpq_patch_s *patch)
398 {
399 14 return libmpq__patch_writer_finish(patch);
400 }
401
402 int32_t
403 4 libmpq__patch_abort(mpq_patch_s *patch)
404 {
405 4 return libmpq__patch_writer_abort(patch);
406 }
407
408 /*
409 * Begin a streamed file through the internal writer implementation.
410 * The returned opaque writer owns the in-progress file state until finish or
411 * an error closes the stream.
412 */
413 int32_t
414 47 libmpq__writer_begin(
415 mpq_archive_s *archive, const char *name, libmpq__off_t size, const mpq_file_options_s *options,
416 mpq_writer_s **out
417 )
418 {
419 47 return libmpq__writer_file_begin(archive, name, size, options, out);
420 }
421
422 /*
423 * Write one input range through the internal writer implementation.
424 * The writer validates the declared file size and buffers or flushes sectors
425 * according to the selected storage and compression options.
426 */
427 int32_t
428 80 libmpq__writer_write(mpq_writer_s *writer, const uint8_t *buffer, libmpq__off_t size)
429 {
430 80 return libmpq__writer_file_write(writer, buffer, size);
431 }
432
433 /*
434 * Finish a streamed file through the internal writer implementation.
435 * Finalization verifies that all declared bytes were supplied and publishes
436 * the completed file entry in the archive tables.
437 */
438 int32_t
439 41 libmpq__writer_finish(mpq_writer_s *writer)
440 {
441 41 return libmpq__writer_file_finish(writer);
442 }
443
444 /*
445 * Add an in-memory file through the internal writer implementation.
446 * The convenience call performs begin, write, and finish operations while
447 * retaining the same validation and compression behavior as streaming.
448 */
449 int32_t
450 2483 libmpq__archive_add_data(
451 mpq_archive_s *archive, const char *name, const uint8_t *data, libmpq__off_t size,
452 const mpq_file_options_s *options
453 )
454 {
455 2483 return libmpq__writer_file_add(archive, name, data, size, options);
456 }
457
458 /*
459 * Add a filesystem file through the internal writer implementation.
460 * The source is read in bounded chunks, so callers need not load the complete
461 * file into memory before archive creation begins.
462 */
463 int32_t
464 5 libmpq__archive_add_path(
465 mpq_archive_s *archive, const char *name, const char *source, const mpq_file_options_s *options
466 )
467 {
468 5 return libmpq__writer_file_add_path(archive, name, source, options);
469 }
470
471 /*
472 * Open an MPQ archive from a path and optional embedded archive offset.
473 * A sentinel offset enables embedded-header scanning, while an explicit offset
474 * restricts parsing to the requested archive location.
475 */
476 int32_t
477 1287 libmpq__archive_open(
478 mpq_archive_s **mpq_archive, const char *mpq_filename, libmpq__off_t archive_offset
479 )
480 {
481 1287 return libmpq__reader_archive_open_path(mpq_archive, mpq_filename, archive_offset);
482 }
483
484 int32_t
485 20 libmpq__archive_open_io(
486 mpq_archive_s **mpq_archive, void *context, libmpq_read_at_fn read_at,
487 libmpq__off_t source_size, libmpq__off_t archive_offset, const char *source_name
488 )
489 {
490 20 return libmpq__reader_archive_open_io(
491 mpq_archive, context, read_at, source_size, archive_offset, source_name
492 );
493 }
494
495 /* Open a caller-authenticated MPQE stream before parsing the contained MPQ. */
496 int32_t
497 119 libmpq__archive_open_mpqe(
498 mpq_archive_s **mpq_archive, const char *mpq_filename, libmpq__off_t archive_offset,
499 const uint8_t *auth_code, size_t auth_code_size
500 )
501 {
502 119 return libmpq__reader_archive_open_mpqe(
503 mpq_archive, mpq_filename, archive_offset, auth_code, auth_code_size
504 );
505 }
506
507 int32_t
508 6 libmpq__archive_open_mpqe_io(
509 mpq_archive_s **mpq_archive, void *context, libmpq_read_at_fn read_at,
510 libmpq__off_t source_size, libmpq__off_t archive_offset, const uint8_t *auth_code,
511 size_t auth_code_size, const char *source_name
512 )
513 {
514 6 return libmpq__reader_archive_open_mpqe_io(
515 mpq_archive, context, read_at, source_size, archive_offset, auth_code, auth_code_size,
516 source_name
517 );
518 }
519
520 /*
521 * Reopen an archive with independent file I/O, metadata, and lazy caches.
522 * Cloning rejects writer handles and verifies a path-backed source still
523 * identifies the same file before reparsing it into a separate archive object.
524 */
525 int32_t
526 222 libmpq__archive_clone(mpq_archive_s **clone, mpq_archive_s *source)
527 {
528
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 221 times.
222 if (clone == NULL)
529 1 return LIBMPQ_ERROR_EXIST;
530 221 *clone = NULL;
531
532
3/4
✓ Branch 0 taken 221 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 220 times.
221 if (source == NULL || source->write_mode)
533 1 return LIBMPQ_ERROR_EXIST;
534
535 220 return libmpq__reader_archive_clone(clone, source);
536 }
537
538 /*
539 * Close the archive file and release all metadata tables allocated during archive open.
540 * Writer handles are finalized before their archive storage is freed, while
541 * reader-side cached block offsets are released entry by entry.
542 */
543 int32_t
544 2239 libmpq__archive_close(mpq_archive_s *mpq_archive)
545 {
546 uint32_t i;
547 2239 int32_t result = 0;
548
549
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2238 times.
2239 if (mpq_archive == NULL)
550 1 return LIBMPQ_ERROR_EXIST;
551
552
2/2
✓ Branch 0 taken 618 times.
✓ Branch 1 taken 1620 times.
2238 if (mpq_archive->write_mode) {
553
554 /* Writer closure must serialize tables before releasing writer storage. */
555 618 result = libmpq__writer_finalize(mpq_archive);
556 618 libmpq__writer_file_abort(mpq_archive->write_current);
557
4/4
✓ Branch 0 taken 615 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 83 times.
✓ Branch 3 taken 532 times.
618 if (result == 0 && mpq_archive->write_mpqe)
558 83 result = libmpq__writer_finalize_mpqe(mpq_archive);
559
3/6
✓ Branch 0 taken 535 times.
✓ Branch 1 taken 83 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 535 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
618 if (mpq_archive->fp != NULL && fclose(mpq_archive->fp) < 0 && result == 0)
560 ✗ result = LIBMPQ_ERROR_CLOSE;
561 618 libmpq__writer_mpqe_cleanup(mpq_archive);
562 618 libmpq__rsa_clear(
563 618 mpq_archive->write_signature_key, sizeof(mpq_archive->write_signature_key)
564 );
565 618 libmpq__rsa_clear(
566 618 mpq_archive->write_strong_signature_key, sizeof(mpq_archive->write_strong_signature_key)
567 );
568
2/2
✓ Branch 0 taken 36815 times.
✓ Branch 1 taken 618 times.
37433 for (i = 0; i < mpq_archive->write_capacity; i++)
569
1/2
✓ Branch 0 taken 36815 times.
✗ Branch 1 not taken.
36815 free(mpq_archive->write_names ? mpq_archive->write_names[i] : NULL);
570 618 free(mpq_archive->write_names);
571 618 free(mpq_archive->write_locales);
572 618 free(mpq_archive->write_platforms);
573 618 libmpq__attributes_free(mpq_archive);
574 618 free(mpq_archive->mpq_hash);
575 618 free(mpq_archive->mpq_block);
576 618 free(mpq_archive->mpq_block_ex);
577 618 free(mpq_archive->filename);
578 618 free(mpq_archive->write_mpqe_destination);
579 618 free(mpq_archive->write_mpqe_output_path);
580 618 free(mpq_archive);
581 618 return result;
582 }
583
584 1620 result = libmpq__source_close(mpq_archive->source);
585
586
2/2
✓ Branch 0 taken 117032 times.
✓ Branch 1 taken 1620 times.
118652 for (i = 0; i < mpq_archive->mpq_header.block_table_count; i++) {
587
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 117032 times.
117032 if (mpq_archive->mpq_file[i] != NULL) {
588 ✗ free(mpq_archive->mpq_file[i]->packed_offset);
589 ✗ free(mpq_archive->mpq_file[i]);
590 }
591 }
592
593 1620 free(mpq_archive->mpq_map);
594 1620 libmpq__attributes_free(mpq_archive);
595 1620 free(mpq_archive->mpq_file);
596 1620 free(mpq_archive->mpq_hash);
597 1620 free(mpq_archive->mpq_block);
598 1620 free(mpq_archive->mpq_block_ex);
599 1620 free(mpq_archive->filename);
600 1620 free(mpq_archive);
601
602 1620 return result;
603 }
604
605 /*
606 * Return the sum of packed sizes for all files in the archive block table.
607 * The caller supplies an accumulator, allowing this query to preserve the
608 * library's existing additive API behavior.
609 */
610 int32_t
611 1 libmpq__archive_size_packed(mpq_archive_s *mpq_archive, libmpq__off_t *packed_size)
612 {
613
614 /* Running total across all block-table entries. */
615 uint32_t i;
616
617
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
7 for (i = 0; i < mpq_archive->files; i++) {
618 6 *packed_size +=
619 6 mpq_archive->mpq_block[mpq_archive->mpq_map[i].block_table_indices].packed_size;
620 }
621
622 1 return 0;
623 }
624
625 /*
626 * Return the sum of unpacked sizes for all files in the archive block table.
627 * Only live public file-map entries are counted; unused block-table capacity
628 * does not contribute to the reported total.
629 */
630 int32_t
631 1 libmpq__archive_size_unpacked(mpq_archive_s *mpq_archive, libmpq__off_t *unpacked_size)
632 {
633
634 /* Running total across all block-table entries. */
635 uint32_t i;
636
637
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
7 for (i = 0; i < mpq_archive->files; i++) {
638 6 *unpacked_size +=
639 6 mpq_archive->mpq_block[mpq_archive->mpq_map[i].block_table_indices].unpacked_size;
640 }
641
642 1 return 0;
643 }
644
645 /*
646 * Return the byte offset where the MPQ archive starts in the backing file.
647 * Embedded archives therefore report their discovered start rather than zero.
648 */
649 int32_t
650 1 libmpq__archive_offset(mpq_archive_s *mpq_archive, libmpq__off_t *offset)
651 {
652 1 *offset = mpq_archive->archive_offset;
653
654 1 return 0;
655 }
656
657 /*
658 * Return the MPQ archive format version stored in the header.
659 * The internal zero-based version is converted to the public one-based API
660 * value before being written to the caller's output.
661 */
662 int32_t
663 7 libmpq__archive_version(mpq_archive_s *mpq_archive, uint32_t *version)
664 {
665 7 *version = mpq_archive->mpq_header.version + 1;
666
667 7 return 0;
668 }
669
670 /*
671 * Return the number of valid file entries discovered while opening the archive.
672 * This is the compact public count, not the reserved block-table capacity.
673 */
674 int32_t
675 73 libmpq__archive_files(mpq_archive_s *mpq_archive, uint32_t *files)
676 {
677 73 *files = mpq_archive->files;
678
679 73 return 0;
680 }
681
682 /*
683 * Return the packed size of a file entry by block-table number.
684 * The public file number is validated and translated through the compact map
685 * before reading the corresponding block-table entry.
686 */
687 int32_t
688 88 libmpq__file_size_packed(
689 mpq_archive_s *mpq_archive, uint32_t file_number, libmpq__off_t *packed_size
690 )
691 {
692
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 87 times.
88 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
693 1 return LIBMPQ_ERROR_EXIST;
694 }
695
696 87 *packed_size =
697 87 mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].packed_size;
698
699 87 return 0;
700 }
701
702 /*
703 * Return the unpacked size of a file entry by block-table number.
704 * Invalid compact file numbers are rejected before any archive metadata is
705 * accessed.
706 */
707 int32_t
708 10784 libmpq__file_size_unpacked(
709 mpq_archive_s *mpq_archive, uint32_t file_number, libmpq__off_t *unpacked_size
710 )
711 {
712
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 10783 times.
10784 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
713 1 return LIBMPQ_ERROR_EXIST;
714 }
715
716 10783 *unpacked_size =
717 10783 mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].unpacked_size;
718
719 10783 return 0;
720 }
721
722 /*
723 * Return the file data offset relative to the start of the archive.
724 * MPQ v2 high offset words are combined with the legacy low word to produce
725 * the complete offset visible through the public API.
726 */
727 int32_t
728 3497 libmpq__file_offset(mpq_archive_s *mpq_archive, uint32_t file_number, libmpq__off_t *offset)
729 {
730
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 3496 times.
3497 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
731 1 return LIBMPQ_ERROR_EXIST;
732 }
733
734 3496 *offset = mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].offset +
735 3496 (((long long)mpq_archive
736 3496 ->mpq_block_ex[mpq_archive->mpq_map[file_number].block_table_indices]
737 3496 .offset_high)
738 3496 << 32);
739
740 3496 return 0;
741 }
742
743 /*
744 * Return the number of blocks needed to store the selected file.
745 * The reader distinguishes single-unit files from sectorized entries and
746 * applies the archive sector size for the latter.
747 */
748 int32_t
749 4973 libmpq__file_blocks(mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t *blocks)
750 {
751
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 4972 times.
4973 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
752 1 return LIBMPQ_ERROR_EXIST;
753 }
754
755 4972 *blocks = libmpq__reader_count_file_blocks(mpq_archive, file_number);
756
757 4972 return 0;
758 }
759
760 /* Return stored block-table flags without reading or modifying payload data. */
761 int32_t
762 20893 libmpq__file_flags(mpq_archive_s *archive, uint32_t file_number, uint32_t *flags)
763 {
764
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 20892 times.
20893 if (flags == NULL)
765 1 return LIBMPQ_ERROR_EXIST;
766 20892 *flags = 0;
767
4/4
✓ Branch 0 taken 20891 times.
✓ Branch 1 taken 1 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 20890 times.
20892 if (archive == NULL || libmpq__reader_validate_file_number(archive, file_number) < 0)
768 2 return LIBMPQ_ERROR_EXIST;
769
770 20890 *flags = archive->mpq_block[archive->mpq_map[file_number].block_table_indices].flags;
771 20890 return 0;
772 }
773
774 /*
775 * Calculate the three Storm hashes used to identify an MPQ file name.
776 * Each output corresponds to a distinct hash-table phase used during MPQ
777 * name lookup and collision probing.
778 */
779 void
780 18207 libmpq__file_hash(const char *filename, uint32_t *hash1, uint32_t *hash2, uint32_t *hash3)
781 {
782 18207 *hash1 = libmpq__crypto_hash_string(filename, 0x0);
783 18207 *hash2 = libmpq__crypto_hash_string(filename, 0x100);
784 18207 *hash3 = libmpq__crypto_hash_string(filename, 0x200);
785 18207 }
786
787 /*
788 * Resolve a precomputed MPQ file-name hash to a public file number.
789 * The first hash selects a slot and linear probing continues until the stored
790 * pair matches or the table wraps without finding the file.
791 */
792 int32_t
793 10159 libmpq__file_number_from_hash(
794 mpq_archive_s *mpq_archive, uint32_t hash1, uint32_t hash2, uint32_t hash3, uint32_t *number
795 )
796 {
797
798 /* Hash table probe state and archive hash-table size. */
799 uint32_t i;
800 uint32_t ht_count;
801 uint32_t block_table_index;
802
803 10159 ht_count = mpq_archive->mpq_header.hash_table_count;
804
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10159 times.
10159 if (ht_count == 0) {
805 ✗ return LIBMPQ_ERROR_EXIST;
806 }
807
808 10159 hash1 %= ht_count;
809
810 /* The first hash selects the initial probe slot; collisions use linear probing. */
811
2/2
✓ Branch 0 taken 7318 times.
✓ Branch 1 taken 4945 times.
12263 for (i = hash1; mpq_archive->mpq_hash[i].block_table_index != LIBMPQ_HASH_FREE;
812 2104 i = (i + 1) % ht_count) {
813
3/4
✓ Branch 0 taken 5214 times.
✓ Branch 1 taken 2104 times.
✓ Branch 2 taken 5214 times.
✗ Branch 3 not taken.
7318 if (mpq_archive->mpq_hash[i].hash_a == hash2 && mpq_archive->mpq_hash[i].hash_b == hash3) {
814 5214 block_table_index = mpq_archive->mpq_hash[i].block_table_index;
815
1/2
✓ Branch 0 taken 5214 times.
✗ Branch 1 not taken.
5214 if (block_table_index >= mpq_archive->mpq_header.block_table_count ||
816
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5214 times.
5214 (mpq_archive->mpq_block[block_table_index].flags & LIBMPQ_FLAG_EXISTS) == 0) {
817 ✗ return LIBMPQ_ERROR_FORMAT;
818 }
819 5214 *number = block_table_index - mpq_archive->mpq_map[block_table_index].block_table_diff;
820
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5214 times.
5214 if (*number >= mpq_archive->files) {
821 ✗ return LIBMPQ_ERROR_FORMAT;
822 }
823
824 5214 return 0;
825 }
826
827
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2104 times.
2104 if ((i + 1) % ht_count == hash1) {
828 ✗ break;
829 }
830 }
831
832 4945 return LIBMPQ_ERROR_EXIST;
833 }
834
835 /*
836 * Resolve an MPQ file name to its block-table number through the hash table.
837 * The name is hashed with all three Storm phases before the collision-aware
838 * lookup is delegated to the precomputed-hash helper.
839 */
840 int32_t
841 10158 libmpq__file_number(mpq_archive_s *mpq_archive, const char *filename, uint32_t *number)
842 {
843 uint32_t hash1;
844 uint32_t hash2;
845 uint32_t hash3;
846
847 10158 libmpq__file_hash(filename, &hash1, &hash2, &hash3);
848 10158 return libmpq__file_number_from_hash(mpq_archive, hash1, hash2, hash3, number);
849 }
850
851 /* Complete reads scope their offset cache inside the reader. */
852 int32_t
853 3468 libmpq__file_read(
854 mpq_archive_s *archive, uint32_t number, uint8_t *buffer, libmpq__off_t size,
855 libmpq__off_t *transferred
856 )
857 {
858 3468 return libmpq__reader_file_read(archive, number, buffer, size, transferred);
859 }
860
861 int32_t
862 7 libmpq__stream_open(mpq_archive_s *archive, uint32_t file_number, mpq_stream_s **stream)
863 {
864 7 return libmpq__reader_stream_open(archive, file_number, stream);
865 }
866
867 int32_t
868 94 libmpq__stream_open_name(mpq_archive_s *archive, const char *filename, mpq_stream_s **stream)
869 {
870 94 return libmpq__reader_stream_open_name(archive, filename, stream);
871 }
872
873 int32_t
874 606 libmpq__stream_read(
875 mpq_stream_s *stream, uint8_t *buffer, libmpq__off_t size, libmpq__off_t *transferred
876 )
877 {
878 606 return libmpq__reader_stream_read(stream, buffer, size, transferred);
879 }
880
881 int32_t
882 30 libmpq__stream_seek(mpq_stream_s *stream, libmpq__off_t offset, int32_t origin)
883 {
884 30 return libmpq__reader_stream_seek(stream, offset, origin);
885 }
886
887 int32_t
888 17 libmpq__stream_tell(mpq_stream_s *stream, libmpq__off_t *position)
889 {
890 17 return libmpq__reader_stream_tell(stream, position);
891 }
892
893 int32_t
894 36 libmpq__stream_size(mpq_stream_s *stream, libmpq__off_t *size)
895 {
896 36 return libmpq__reader_stream_size(stream, size);
897 }
898
899 int32_t
900 100 libmpq__stream_close(mpq_stream_s *stream)
901 {
902 100 return libmpq__reader_stream_close(stream);
903 }
904
905 /*
906 * Return one block's unpacked size directly from archive metadata.
907 * Full sectors use the archive sector size, while the final sector is reduced
908 * to the remaining file bytes and single-unit files use their full size.
909 */
910 int32_t
911 41289 libmpq__block_size_unpacked(
912 mpq_archive_s *mpq_archive, uint32_t file_number, uint32_t block_number,
913 libmpq__off_t *unpacked_size
914 )
915 {
916
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 41288 times.
41289 if (libmpq__reader_validate_file_number(mpq_archive, file_number) < 0) {
917 1 return LIBMPQ_ERROR_EXIST;
918 }
919
920
2/2
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 41287 times.
41288 if (libmpq__reader_validate_block_number(mpq_archive, file_number, block_number) < 0) {
921 1 return LIBMPQ_ERROR_EXIST;
922 }
923
924
2/2
✓ Branch 0 taken 2405 times.
✓ Branch 1 taken 38882 times.
41287 if ((mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags &
925 LIBMPQ_FLAG_SINGLE) != 0) {
926
927 /* A single-unit entry has one logical block containing the whole file. */
928 2405 *unpacked_size =
929 2405 mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices]
930 2405 .unpacked_size;
931 }
932
933
2/2
✓ Branch 0 taken 38882 times.
✓ Branch 1 taken 2405 times.
41287 if ((mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices].flags &
934 LIBMPQ_FLAG_SINGLE) == 0) {
935
936 /* Every non-final sector is full-sized; only the tail uses a remainder. */
937
2/2
✓ Branch 1 taken 31899 times.
✓ Branch 2 taken 6983 times.
38882 if (block_number < libmpq__reader_count_file_blocks(mpq_archive, file_number) - 1) {
938 31899 *unpacked_size = mpq_archive->block_size;
939 } else {
940 6983 *unpacked_size =
941 6983 mpq_archive->mpq_block[mpq_archive->mpq_map[file_number].block_table_indices]
942 6983 .unpacked_size -
943 6983 mpq_archive->block_size * block_number;
944 }
945 }
946
947 41287 return 0;
948 }
949
950 /* Query stored sector bytes through the reader's offset-table lifecycle. */
951 int32_t
952 877 libmpq__block_size_packed(
953 mpq_archive_s *archive, uint32_t file_number, uint32_t block_number, libmpq__off_t *packed_size
954 )
955 {
956 877 return libmpq__reader_block_size_packed(archive, file_number, block_number, packed_size);
957 }
958
959 /* Report the serialized method through the reader's packed-sector inspection. */
960 int32_t
961 890 libmpq__block_compression(
962 mpq_archive_s *archive, uint32_t file_number, uint32_t block_number, uint32_t *compression
963 )
964 {
965 890 return libmpq__reader_block_compression(archive, file_number, block_number, compression);
966 }
967
968 /* Normal block reads never enable explicit checksum verification. */
969 int32_t
970 10 libmpq__block_read(
971 mpq_archive_s *archive, uint32_t file_number, uint32_t block_number, uint8_t *out_buf,
972 libmpq__off_t out_size, libmpq__off_t *transferred
973 )
974 {
975 10 return libmpq__reader_block_read(
976 archive, file_number, block_number, out_buf, out_size, transferred, NULL, NULL, NULL
977 );
978 }
979