GCC Code Coverage Report


Directory: src/
File: src/mpq-writer.c
Date: 2026-09-02 16:40:44
Exec Total Coverage
Lines: 352 460 76.5%
Functions: 13 13 100.0%
Branches: 221 312 70.8%

Line Branch Exec Source
1 /*
2 * mpq-writer.c -- seekable MPQ archive creation.
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 #ifdef HAVE_CONFIG_H
20 #include "config.h"
21 #endif
22
23 #include "mpq-compression.h"
24 #include "mpq-crypto.h"
25 #include "mpq-endian.h"
26 #include "mpq-internal.h"
27 #include "mpq-pkware.h"
28 #include "mpq-wave.h"
29 #include "mpq-writer.h"
30 #include <libmpq/mpq.h>
31
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35
36 /* Return the smallest supported power-of-two table capacity at or above value.
37 * MPQ hash tables use power-of-two probing, so this helper provides the next
38 * legal capacity without exceeding the format's 32-bit range. */
39 static uint32_t
40 65 next_power_two(uint32_t value)
41 {
42 65 uint32_t result = 4;
43
3/4
✓ Branch 0 taken 257 times.
✓ Branch 1 taken 65 times.
✓ Branch 2 taken 257 times.
✗ Branch 3 not taken.
322 while (result < value && result < 0x80000000u)
44 257 result <<= 1;
45 65 return result;
46 }
47
48 /* Write one byte range at an absolute archive offset.
49 * Seeking and writing are treated as one archive operation so short writes
50 * and positioning failures are converted to the library's write error. */
51 static int32_t
52 632 write_at(FILE *fp, uint64_t offset, const void *data, size_t size)
53 {
54
3/6
✓ Branch 1 taken 632 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 632 times.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 632 times.
632 if (fseeko(fp, (off_t)offset, SEEK_SET) < 0 || (size && fwrite(data, 1, size, fp) != size))
55 return LIBMPQ_ERROR_WRITE;
56 632 return LIBMPQ_SUCCESS;
57 }
58
59 /* Calculate the MPQ file encryption key from a normalized file name.
60 * Slash normalization matches MPQ's Windows-oriented name hashing while the
61 * temporary normalized string remains private to this calculation. */
62 static uint32_t
63 393 file_key(const char *name)
64 {
65 393 char *normalized = NULL;
66 size_t i;
67 393 size_t length = strlen(name);
68 uint32_t key;
69
70 393 normalized = malloc(length + 1);
71
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 393 times.
393 if (normalized == NULL)
72 return 0;
73
2/2
✓ Branch 0 taken 5951 times.
✓ Branch 1 taken 393 times.
6344 for (i = 0; i < length; i++)
74
1/2
✓ Branch 0 taken 5951 times.
✗ Branch 1 not taken.
5951 normalized[i] = name[i] == '/' ? '\\' : name[i];
75 393 normalized[length] = 0;
76 393 key = libmpq__crypto_hash_string(normalized, 0x300);
77 393 free(normalized);
78 393 return key;
79 }
80
81 /* Compress, encrypt, and append the writer's currently buffered sector.
82 * Sector zero and later sectors may use different masks; the resulting offset
83 * is recorded relative to the payload for the table written during finish. */
84 static int32_t
85 965 stream_flush_sector(mpq_writer_s *writer)
86 {
87 965 mpq_archive_s *archive = writer->archive;
88 965 uint8_t *packed = NULL;
89 965 size_t packed_size = writer->data_size;
90 965 uint8_t emitted = 0;
91 1930 uint32_t requested = writer->sector_index == 0 ? writer->options.compression_first
92
2/2
✓ Branch 0 taken 575 times.
✓ Branch 1 taken 390 times.
965 : writer->options.compression_next;
93 int32_t result;
94
95 /* Reject an extra flush before touching the archive stream. */
96
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 965 times.
965 if (writer->sector_index >= writer->block_count)
97 return LIBMPQ_ERROR_SIZE;
98
2/2
✓ Branch 0 taken 614 times.
✓ Branch 1 taken 351 times.
965 if ((writer->options.flags & LIBMPQ_FILE_FLAG_COMPRESS) != 0)
99 614 result = libmpq__compression_encode_sector(
100 614 writer->data, writer->data_size, requested, &packed, &packed_size, &emitted
101 );
102
2/2
✓ Branch 0 taken 138 times.
✓ Branch 1 taken 213 times.
351 else if ((writer->options.flags & LIBMPQ_FILE_FLAG_IMPLODE) != 0) {
103 138 uint32_t packed32 = 0;
104 138 result = libmpq__pkzip_compress(writer->data, writer->data_size, &packed, &packed32);
105 138 packed_size = packed32;
106
2/4
✓ Branch 0 taken 138 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 138 times.
✗ Branch 3 not taken.
138 if (result == LIBMPQ_SUCCESS && packed_size >= writer->data_size) {
107 138 free(packed);
108
1/2
✓ Branch 0 taken 138 times.
✗ Branch 1 not taken.
138 packed = malloc(writer->data_size ? writer->data_size : 1);
109
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 138 times.
138 if (packed == NULL)
110 result = LIBMPQ_ERROR_MALLOC;
111 else {
112 138 memcpy(packed, writer->data, writer->data_size);
113 138 packed_size = writer->data_size;
114 }
115 }
116 } else {
117
2/2
✓ Branch 0 taken 199 times.
✓ Branch 1 taken 14 times.
213 packed = malloc(packed_size ? packed_size : 1);
118
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 213 times.
213 result = packed == NULL ? LIBMPQ_ERROR_MALLOC : LIBMPQ_SUCCESS;
119
1/2
✓ Branch 0 taken 213 times.
✗ Branch 1 not taken.
213 if (result == 0)
120 213 memcpy(packed, writer->data, packed_size);
121 }
122
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 965 times.
965 if (result < 0) {
123 free(packed);
124 return result;
125 }
126
127 /* Payload encryption uses the file seed advanced by the sector number. */
128
2/2
✓ Branch 0 taken 284 times.
✓ Branch 1 taken 681 times.
965 if (writer->options.flags & LIBMPQ_FILE_FLAG_ENCRYPTED)
129 284 libmpq__crypto_encrypt_block(
130 284 packed, (uint32_t)packed_size, file_key(writer->name) + writer->sector_index
131 );
132
2/2
✓ Branch 0 taken 734 times.
✓ Branch 1 taken 231 times.
965 if (writer->offsets)
133 734 writer->offsets[writer->sector_index] =
134 734 (uint32_t)((uint64_t)ftello(archive->fp) - writer->payload_offset);
135
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 965 times.
965 if (fwrite(packed, 1, packed_size, archive->fp) != packed_size) {
136 free(packed);
137 return LIBMPQ_ERROR_WRITE;
138 }
139 965 writer->packed_total += packed_size;
140
2/2
✓ Branch 0 taken 575 times.
✓ Branch 1 taken 390 times.
965 if (writer->sector_index == 0 &&
141
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 573 times.
575 (writer->options.compression_next &
142 (LIBMPQ_COMPRESSION_WAVE_MONO | LIBMPQ_COMPRESSION_WAVE_STEREO))) {
143 libmpq_wave_info_s wave;
144
145 /* Validate the complete WAVE geometry before preserving lossy output. */
146
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (libmpq__wave_probe_pcm16_prefix(
147 2 writer->data, writer->data_size, (uint64_t)writer->expected, &wave
148 ) == 0) {
149 1 writer->options.compression_next &=
150 ~(LIBMPQ_COMPRESSION_WAVE_MONO | LIBMPQ_COMPRESSION_WAVE_STEREO);
151 1 writer->options.compression_next |=
152
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 wave.channels == 1 ? LIBMPQ_COMPRESSION_WAVE_MONO : LIBMPQ_COMPRESSION_WAVE_STEREO;
153 } else {
154 1 free(packed);
155 1 return LIBMPQ_ERROR_FORMAT;
156 }
157 }
158 964 writer->sector_index++;
159 964 writer->data_size = 0;
160 964 free(packed);
161 964 return LIBMPQ_SUCCESS;
162 }
163
164 /* Finish the streamed file by writing its offset table and archive metadata.
165 * The serialized table is placed before packed sectors, encrypted separately,
166 * and then the completed file is inserted into the block and hash tables. */
167 static int32_t
168 575 stream_finish(mpq_writer_s *writer)
169 {
170 575 mpq_archive_s *archive = writer->archive;
171 uint32_t index;
172 uint32_t slot;
173 uint32_t hash1;
174 uint32_t hash2;
175 uint32_t hash3;
176 uint32_t i;
177 uint64_t total;
178 size_t table_size;
179 uint8_t *table;
180 int32_t result;
181
182 /* A file is publishable only after its declared byte and sector counts match. */
183
3/4
✓ Branch 0 taken 573 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 573 times.
575 if (writer->written != writer->expected || writer->sector_index != writer->block_count)
184 2 return LIBMPQ_ERROR_SIZE;
185
2/2
✓ Branch 0 taken 411 times.
✓ Branch 1 taken 162 times.
573 if (writer->offsets) {
186 411 table_size = (size_t)(writer->block_count + 1) * 4;
187 411 writer->offsets[writer->block_count] =
188 411 (uint32_t)writer->packed_total + (uint32_t)table_size;
189 411 table = malloc(table_size);
190
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 411 times.
411 if (table == NULL)
191 return LIBMPQ_ERROR_MALLOC;
192
2/2
✓ Branch 0 taken 1144 times.
✓ Branch 1 taken 411 times.
1555 for (i = 0; i <= writer->block_count; i++)
193 1144 libmpq__store_le32(table + i * 4, writer->offsets[i]);
194
2/2
✓ Branch 0 taken 109 times.
✓ Branch 1 taken 302 times.
411 if (writer->options.flags & LIBMPQ_FILE_FLAG_ENCRYPTED)
195 109 libmpq__crypto_encrypt_block(table, (uint32_t)table_size, file_key(writer->name) - 1);
196 411 result = write_at(archive->fp, writer->payload_offset, table, table_size);
197 411 free(table);
198
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 411 times.
411 if (result < 0)
199 return result;
200 411 total = writer->packed_total + table_size;
201 } else {
202 162 total = writer->packed_total;
203 }
204
205 /* Restore the append position after rewriting the table at the file start. */
206
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 573 times.
573 if (fseeko(archive->fp, (off_t)(writer->payload_offset + total), SEEK_SET) < 0)
207 return LIBMPQ_ERROR_SEEK;
208
3/6
✓ Branch 0 taken 573 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 573 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 573 times.
573 if (writer->payload_offset > UINT32_MAX || total > UINT32_MAX || writer->expected > UINT32_MAX)
209 return archive->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_ONE
210 ? LIBMPQ_ERROR_SIZE
211 : (writer->payload_offset >> 32 > UINT16_MAX ? LIBMPQ_ERROR_SIZE : 0);
212
213 /* Reserve the next block slot only after payload and offset sizes validate. */
214 573 index = archive->write_next_block;
215 573 libmpq__file_hash(writer->name, &hash1, &hash2, &hash3);
216 573 archive->mpq_block[index].offset = (uint32_t)writer->payload_offset;
217 573 archive->mpq_block[index].packed_size = (uint32_t)total;
218 573 archive->mpq_block[index].unpacked_size = (uint32_t)writer->expected;
219 573 archive->mpq_block[index].flags = LIBMPQ_FLAG_EXISTS | writer->options.flags;
220
2/2
✓ Branch 0 taken 351 times.
✓ Branch 1 taken 222 times.
573 if (writer->options.flags & LIBMPQ_FILE_FLAG_COMPRESS)
221 351 archive->mpq_block[index].flags |= LIBMPQ_FLAG_COMPRESS_MULTI;
222 573 archive->mpq_block_ex[index].offset_high = (uint16_t)(writer->payload_offset >> 32);
223 573 archive->write_names[index] = writer->name;
224 573 archive->write_locales[index] = writer->options.locale;
225 573 archive->write_platforms[index] = writer->options.platform;
226 573 writer->name = NULL;
227 573 archive->write_next_block++;
228 573 archive->files = archive->write_next_block;
229
1/2
✓ Branch 0 taken 693 times.
✗ Branch 1 not taken.
693 for (slot = 0; slot < archive->write_hash_capacity; slot++) {
230 693 uint32_t pos = (hash1 + slot) & (archive->write_hash_capacity - 1);
231
2/2
✓ Branch 0 taken 573 times.
✓ Branch 1 taken 120 times.
693 if (archive->mpq_hash[pos].block_table_index == LIBMPQ_HASH_FREE) {
232 573 archive->mpq_hash[pos].hash_a = hash2;
233 573 archive->mpq_hash[pos].hash_b = hash3;
234 573 archive->mpq_hash[pos].locale = writer->options.locale;
235 573 archive->mpq_hash[pos].platform = writer->options.platform;
236 573 archive->mpq_hash[pos].block_table_index = index;
237 573 return LIBMPQ_SUCCESS;
238 }
239 }
240 return LIBMPQ_ERROR_SIZE;
241 }
242
243 /* Serialize the completed hash, block, extended tables, and archive header.
244 * Finalization optionally creates the listfile, encrypts metadata with the
245 * fixed MPQ table keys, writes v2 high offsets, and commits the header last. */
246 static int32_t
247 65 finalize_archive(mpq_archive_s *a)
248 {
249 uint8_t *raw;
250 uint32_t i;
251 size_t bytes;
252 uint64_t end;
253 uint8_t header[44];
254
255 /* An unfinished streamed file would leave archive tables inconsistent. */
256
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 if (a->write_current)
257 return LIBMPQ_ERROR_SIZE;
258
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 62 times.
65 if ((a->write_flags & LIBMPQ_ARCHIVE_CREATE_LISTFILE) != 0) {
259 3 size_t total = 1;
260 uint8_t *list;
261
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 3 times.
12 for (i = 0; i < a->write_next_block; i++)
262 9 total += strlen(a->write_names[i]) + 1;
263 3 list = malloc(total);
264
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (!list)
265 return LIBMPQ_ERROR_MALLOC;
266 3 total = 0;
267
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 3 times.
12 for (i = 0; i < a->write_next_block; i++) {
268 9 size_t n = strlen(a->write_names[i]);
269 9 memcpy(list + total, a->write_names[i], n);
270 9 total += n;
271 9 list[total++] = '\n';
272 }
273 {
274 3 mpq_file_options_s o = { LIBMPQ_FILE_FLAG_SINGLE, 0, 0, 0, 0 };
275 int32_t r =
276 3 libmpq__writer_file_add(a, LIBMPQ_LISTFILE_NAME, list, (libmpq__off_t)total, &o);
277 3 free(list);
278
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (r < 0)
279 return r;
280 }
281 }
282
283 /* Serialize and encrypt the hash table before writing the block metadata. */
284 65 bytes = (size_t)a->write_hash_capacity * 16;
285 65 raw = malloc(bytes);
286
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 if (!raw)
287 return LIBMPQ_ERROR_MALLOC;
288
2/2
✓ Branch 0 taken 6024 times.
✓ Branch 1 taken 65 times.
6089 for (i = 0; i < a->write_hash_capacity; i++) {
289 6024 libmpq__store_le32(raw + i * 16, a->mpq_hash[i].hash_a);
290 6024 libmpq__store_le32(raw + i * 16 + 4, a->mpq_hash[i].hash_b);
291 6024 libmpq__store_le16(raw + i * 16 + 8, a->mpq_hash[i].locale);
292 6024 libmpq__store_le16(raw + i * 16 + 10, a->mpq_hash[i].platform);
293 6024 libmpq__store_le32(raw + i * 16 + 12, a->mpq_hash[i].block_table_index);
294 }
295 65 libmpq__crypto_encrypt_block(
296 raw, (uint32_t)bytes, libmpq__crypto_hash_string("(hash table)", 0x300)
297 );
298
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 65 times.
65 if (write_at(a->fp, a->mpq_header.hash_table_offset, raw, bytes) < 0) {
299 free(raw);
300 return LIBMPQ_ERROR_WRITE;
301 }
302 65 free(raw);
303
304 /* Serialize the fixed-capacity block table using explicit little-endian fields. */
305 65 bytes = (size_t)a->write_capacity * 16;
306 65 raw = malloc(bytes);
307
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 if (!raw)
308 return LIBMPQ_ERROR_MALLOC;
309
2/2
✓ Branch 0 taken 3010 times.
✓ Branch 1 taken 65 times.
3075 for (i = 0; i < a->write_capacity; i++) {
310 3010 libmpq__store_le32(raw + i * 16, a->mpq_block[i].offset);
311 3010 libmpq__store_le32(raw + i * 16 + 4, a->mpq_block[i].packed_size);
312 3010 libmpq__store_le32(raw + i * 16 + 8, a->mpq_block[i].unpacked_size);
313 3010 libmpq__store_le32(raw + i * 16 + 12, a->mpq_block[i].flags);
314 }
315 65 libmpq__crypto_encrypt_block(
316 raw, (uint32_t)bytes, libmpq__crypto_hash_string("(block table)", 0x300)
317 );
318
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 65 times.
65 if (write_at(a->fp, a->mpq_header.block_table_offset, raw, bytes) < 0) {
319 free(raw);
320 return LIBMPQ_ERROR_WRITE;
321 }
322 65 free(raw);
323
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 39 times.
65 if (a->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_TWO) {
324 26 bytes = (size_t)a->write_capacity * 2;
325 26 raw = malloc(bytes);
326
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 26 times.
26 if (!raw)
327 return LIBMPQ_ERROR_MALLOC;
328
2/2
✓ Branch 0 taken 801 times.
✓ Branch 1 taken 26 times.
827 for (i = 0; i < a->write_capacity; i++)
329 801 libmpq__store_le16(raw + i * 2, a->mpq_block_ex[i].offset_high);
330
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
26 if (write_at(a->fp, a->mpq_header_ex.extended_offset, raw, bytes) < 0) {
331 free(raw);
332 return LIBMPQ_ERROR_WRITE;
333 }
334 26 free(raw);
335 }
336
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 65 times.
65 if (fseeko(a->fp, 0, SEEK_END) < 0)
337 return LIBMPQ_ERROR_SEEK;
338 65 end = (uint64_t)ftello(a->fp);
339
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 if (end > UINT32_MAX)
340 return LIBMPQ_ERROR_SIZE;
341 65 memset(header, 0, sizeof(header));
342 65 libmpq__store_le32(header, LIBMPQ_HEADER);
343 65 libmpq__store_le32(header + 4, a->mpq_header.header_size);
344 65 libmpq__store_le32(header + 8, (uint32_t)end);
345 65 libmpq__store_le16(header + 12, a->mpq_header.version);
346 65 libmpq__store_le16(header + 14, a->mpq_header.block_size);
347 65 libmpq__store_le32(header + 16, a->mpq_header.hash_table_offset);
348 65 libmpq__store_le32(header + 20, a->mpq_header.block_table_offset);
349 65 libmpq__store_le32(header + 24, a->mpq_header.hash_table_count);
350 65 libmpq__store_le32(header + 28, a->mpq_header.block_table_count);
351
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 39 times.
65 if (a->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_TWO) {
352 26 libmpq__store_le64(header + 32, a->mpq_header_ex.extended_offset);
353 26 libmpq__store_le16(
354 26 header + 40, (uint16_t)(((uint64_t)a->mpq_header.hash_table_offset) >> 32)
355 );
356 26 libmpq__store_le16(
357 26 header + 42, (uint16_t)(((uint64_t)a->mpq_header.block_table_offset) >> 32)
358 );
359 }
360
2/4
✓ Branch 1 taken 65 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 65 times.
65 if (write_at(a->fp, 0, header, a->mpq_header.header_size) < 0 || fflush(a->fp) != 0)
361 return LIBMPQ_ERROR_WRITE;
362 65 a->write_finalized = TRUE;
363 65 return LIBMPQ_SUCCESS;
364 }
365
366 /* Create a new seekable MPQ v1 or v2 archive with reserved metadata tables.
367 * The function validates creation options, reserves table space, initializes
368 * empty hash entries, and leaves the file positioned at the first payload. */
369 int32_t
370 70 libmpq__writer_archive_create(
371 mpq_archive_s **out, const char *path, const mpq_archive_create_options_s *options
372 )
373 {
374 70 mpq_archive_create_options_s defaults = { LIBMPQ_ARCHIVE_VERSION_ONE, 1024, 4096, 0 };
375 mpq_archive_s *a;
376 uint32_t i;
377 uint32_t header_size;
378 uint64_t offset;
379 uint8_t *zero;
380
381
4/4
✓ Branch 0 taken 69 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 68 times.
70 if (out == NULL || path == NULL)
382 2 return LIBMPQ_ERROR_EXIST;
383 68 *out = NULL;
384
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 67 times.
68 if (options == NULL)
385 1 options = &defaults;
386
387 /* Reject versions, capacities, and sector sizes that cannot be represented safely. */
388
4/4
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 66 times.
✓ Branch 3 taken 1 times.
68 if (options->version > LIBMPQ_ARCHIVE_VERSION_TWO || options->max_files == UINT32_MAX ||
389
2/4
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 66 times.
66 (options->max_files && options->max_files > 0x40000000u))
390 2 return LIBMPQ_ERROR_FORMAT;
391
1/2
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
66 if (options->sector_size &&
392
3/4
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 65 times.
66 (options->sector_size < 512 || (options->sector_size & (options->sector_size - 1)) != 0))
393 1 return LIBMPQ_ERROR_FORMAT;
394 65 a = calloc(1, sizeof(*a));
395
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 if (a == NULL)
396 return LIBMPQ_ERROR_MALLOC;
397 65 a->fp = fopen(path, "w+b");
398 65 a->filename = strdup(path);
399
2/4
✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 65 times.
65 if (a->fp == NULL || a->filename == NULL) {
400 if (a->fp)
401 fclose(a->fp);
402 free(a->filename);
403 free(a);
404 return LIBMPQ_ERROR_OPEN;
405 }
406 65 a->write_mode = TRUE;
407
1/2
✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
65 a->write_capacity = options->max_files ? options->max_files : 1024;
408
1/2
✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
65 a->write_sector_size = options->sector_size ? options->sector_size : 4096;
409 65 a->write_flags = options->flags;
410
411 /* Keep hash load below one half so linear probing remains bounded. */
412 65 a->write_hash_capacity = next_power_two(a->write_capacity * 2);
413
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 39 times.
65 header_size = options->version == LIBMPQ_ARCHIVE_VERSION_TWO ? 44 : 32;
414 65 a->mpq_header.version = (uint16_t)options->version;
415 65 a->mpq_header.header_size = header_size;
416 65 a->mpq_header.block_size = 0;
417
2/2
✓ Branch 0 taken 173 times.
✓ Branch 1 taken 65 times.
238 while ((512u << a->mpq_header.block_size) < a->write_sector_size)
418 173 a->mpq_header.block_size++;
419 65 a->mpq_header.hash_table_count = a->write_hash_capacity;
420 65 a->mpq_header.block_table_count = a->write_capacity;
421 65 a->mpq_header.hash_table_offset = header_size;
422 65 a->mpq_header.block_table_offset = header_size + a->write_hash_capacity * 16;
423 65 a->mpq_header_ex.extended_offset = 0;
424 65 offset = (uint64_t)a->mpq_header.block_table_offset + (uint64_t)a->write_capacity * 16;
425
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 39 times.
65 if (options->version == LIBMPQ_ARCHIVE_VERSION_TWO) {
426 26 a->mpq_header_ex.extended_offset = offset;
427 26 offset += (uint64_t)a->write_capacity * 2;
428 }
429 65 offset = (offset + 511) & ~UINT64_C(511);
430
3/4
✓ Branch 0 taken 39 times.
✓ Branch 1 taken 26 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 39 times.
65 if (options->version == LIBMPQ_ARCHIVE_VERSION_ONE && offset > UINT32_MAX) {
431 fclose(a->fp);
432 free(a->filename);
433 free(a);
434 return LIBMPQ_ERROR_SIZE;
435 }
436 65 a->mpq_hash = calloc(a->write_hash_capacity, sizeof(*a->mpq_hash));
437 65 a->mpq_block = calloc(a->write_capacity, sizeof(*a->mpq_block));
438 65 a->mpq_block_ex = calloc(a->write_capacity, sizeof(*a->mpq_block_ex));
439 65 a->write_names = calloc(a->write_capacity, sizeof(*a->write_names));
440 65 a->write_locales = calloc(a->write_capacity, sizeof(*a->write_locales));
441 65 a->write_platforms = calloc(a->write_capacity, sizeof(*a->write_platforms));
442
5/10
✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 65 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 65 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 65 times.
✗ Branch 9 not taken.
65 if (!a->mpq_hash || !a->mpq_block || !a->mpq_block_ex || !a->write_names || !a->write_locales ||
443
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 !a->write_platforms) {
444 fclose(a->fp);
445 free(a->filename);
446 free(a->mpq_hash);
447 free(a->mpq_block);
448 free(a->mpq_block_ex);
449 free(a->write_names);
450 free(a->write_locales);
451 free(a->write_platforms);
452 free(a);
453 return LIBMPQ_ERROR_MALLOC;
454 }
455
2/2
✓ Branch 0 taken 6024 times.
✓ Branch 1 taken 65 times.
6089 for (i = 0; i < a->write_hash_capacity; i++)
456 6024 a->mpq_hash[i].block_table_index = LIBMPQ_HASH_FREE;
457 65 zero = calloc(1, (size_t)(offset > 4096 ? 4096 : offset));
458
1/2
✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
65 if (zero == NULL || fwrite(zero, 1, (size_t)(offset > 4096 ? 4096 : offset), a->fp) !=
459
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 65 times.
65 (size_t)(offset > 4096 ? 4096 : offset)) {
460 free(zero);
461 fclose(a->fp);
462 free(a->filename);
463 free(a->mpq_hash);
464 free(a->mpq_block);
465 free(a->mpq_block_ex);
466 free(a->write_names);
467 free(a->write_locales);
468 free(a->write_platforms);
469 free(a);
470 return LIBMPQ_ERROR_WRITE;
471 }
472 65 free(zero);
473
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 65 times.
65 if (fseeko(a->fp, (off_t)offset, SEEK_SET) < 0) {
474 fclose(a->fp);
475 free(a->filename);
476 free(a->mpq_hash);
477 free(a->mpq_block);
478 free(a->mpq_block_ex);
479 free(a->write_names);
480 free(a->write_locales);
481 free(a->write_platforms);
482 free(a);
483 return LIBMPQ_ERROR_SEEK;
484 }
485 65 *out = a;
486 65 return LIBMPQ_SUCCESS;
487 }
488
489 /* Begin streaming one file into the archive using the requested options.
490 * It validates flags and duplicate names, allocates one sector of input space,
491 * and reserves an offset table when compressed multi-sector storage requires it. */
492 int32_t
493 585 libmpq__writer_file_begin(
494 mpq_archive_s *a, const char *name, libmpq__off_t size, const mpq_file_options_s *options,
495 mpq_writer_s **out
496 )
497 {
498 585 mpq_file_options_s defaults = { 0, 0, 0, 0, 0 };
499 mpq_writer_s *w;
500
11/12
✓ Branch 0 taken 584 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 584 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 583 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 582 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 580 times.
✓ Branch 9 taken 2 times.
✓ Branch 10 taken 1 times.
✓ Branch 11 taken 579 times.
585 if (!a || !a->write_mode || !name || !out || size < 0 || a->write_current)
501 6 return LIBMPQ_ERROR_FORMAT;
502
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 571 times.
579 if (options == NULL)
503 8 options = &defaults;
504 579 w = calloc(1, sizeof(*w));
505
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 579 times.
579 if (w == NULL)
506 return LIBMPQ_ERROR_MALLOC;
507
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 579 times.
579 if ((uint64_t)size > SIZE_MAX || a->write_next_block >= a->write_capacity) {
508 free(w);
509 return LIBMPQ_ERROR_SIZE;
510 }
511
2/2
✓ Branch 0 taken 80 times.
✓ Branch 1 taken 499 times.
579 if ((options->flags & LIBMPQ_FILE_FLAG_IMPLODE) &&
512
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 78 times.
80 (options->flags & LIBMPQ_FILE_FLAG_COMPRESS)) {
513 2 free(w);
514 2 return LIBMPQ_ERROR_FORMAT;
515 }
516
3/4
✓ Branch 0 taken 352 times.
✓ Branch 1 taken 225 times.
✓ Branch 2 taken 352 times.
✗ Branch 3 not taken.
929 if ((options->flags & LIBMPQ_FILE_FLAG_COMPRESS) &&
517
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 352 times.
704 (!libmpq__compression_supported_mask(options->compression_first) ||
518 352 !libmpq__compression_supported_mask(options->compression_next))) {
519 free(w);
520 return LIBMPQ_ERROR_FORMAT;
521 }
522
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 520 times.
577 if ((options->flags & LIBMPQ_FILE_FLAG_SINGLE) &&
523
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 57 times.
57 ((options->compression_first | options->compression_next) &
524 (LIBMPQ_COMPRESSION_WAVE_MONO | LIBMPQ_COMPRESSION_WAVE_STEREO))) {
525 free(w);
526 return LIBMPQ_ERROR_FORMAT;
527 }
528
529 /* Probe existing entries for duplicate name/locale/platform combinations. */
530 {
531 uint32_t h1;
532 uint32_t h2;
533 uint32_t h3;
534 uint32_t i;
535 577 libmpq__file_hash(name, &h1, &h2, &h3);
536
2/2
✓ Branch 0 taken 3172 times.
✓ Branch 1 taken 576 times.
3748 for (i = 0; i < a->write_next_block; i++) {
537 uint32_t a1;
538 uint32_t a2;
539 uint32_t a3;
540
2/4
✓ Branch 0 taken 3172 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3172 times.
✗ Branch 3 not taken.
3172 if (a->write_names[i] == NULL || a->write_locales[i] != options->locale ||
541
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3172 times.
3172 a->write_platforms[i] != options->platform)
542 continue;
543 3172 libmpq__file_hash(a->write_names[i], &a1, &a2, &a3);
544
4/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3171 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
3172 if (h1 == a1 && h2 == a2 && h3 == a3) {
545 1 free(w);
546 1 return LIBMPQ_ERROR_EXIST;
547 }
548 }
549 }
550
1/2
✓ Branch 0 taken 576 times.
✗ Branch 1 not taken.
576 w->data = malloc(a->write_sector_size ? a->write_sector_size : 1);
551 576 w->name = strdup(name);
552
2/4
✓ Branch 0 taken 576 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 576 times.
576 if (w->data == NULL || w->name == NULL) {
553 free(w->data);
554 free(w->name);
555 free(w);
556 return LIBMPQ_ERROR_MALLOC;
557 }
558 576 w->archive = a;
559 576 w->expected = size;
560 576 w->options = *options;
561 576 w->options.compression_first &=
562 ~(LIBMPQ_COMPRESSION_WAVE_MONO | LIBMPQ_COMPRESSION_WAVE_STEREO);
563
2/2
✓ Branch 0 taken 224 times.
✓ Branch 1 taken 352 times.
576 if (w->options.compression_next == 0)
564 224 w->options.compression_next = w->options.compression_first;
565
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 576 times.
576 if (a->write_sector_size == 0) {
566 free(w->name);
567 free(w->data);
568 free(w);
569 return LIBMPQ_ERROR_FORMAT;
570 }
571 1152 w->block_count = (w->options.flags & LIBMPQ_FILE_FLAG_SINGLE)
572 ? 1
573
2/2
✓ Branch 0 taken 519 times.
✓ Branch 1 taken 57 times.
576 : (uint32_t)((size + a->write_sector_size - 1) / a->write_sector_size);
574
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 568 times.
576 if (w->block_count == 0)
575 8 w->block_count = 1;
576 576 w->payload_offset = (uint64_t)ftello(a->fp);
577
578 /* Reserve offset-table space before the first packed sector is written. */
579
2/2
✓ Branch 0 taken 519 times.
✓ Branch 1 taken 57 times.
576 if (!(w->options.flags & LIBMPQ_FILE_FLAG_SINGLE) &&
580
2/2
✓ Branch 0 taken 412 times.
✓ Branch 1 taken 107 times.
519 (w->options.flags & (LIBMPQ_FILE_FLAG_COMPRESS | LIBMPQ_FILE_FLAG_IMPLODE))) {
581 412 size_t table_size = (size_t)(w->block_count + 1) * 4;
582 412 w->offsets = calloc(w->block_count + 1, sizeof(*w->offsets));
583
2/4
✓ Branch 0 taken 412 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 412 times.
412 if (w->offsets == NULL || table_size > UINT32_MAX) {
584 free(w->offsets);
585 free(w->data);
586 free(w->name);
587 free(w);
588 return LIBMPQ_ERROR_MALLOC;
589 }
590 412 w->offsets[0] = (uint32_t)table_size;
591
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 412 times.
412 if (fseeko(a->fp, (off_t)(w->payload_offset + table_size), SEEK_SET) < 0) {
592 free(w->offsets);
593 free(w->data);
594 free(w->name);
595 free(w);
596 return LIBMPQ_ERROR_SEEK;
597 }
598 }
599 576 a->write_current = w;
600 576 *out = w;
601 576 return LIBMPQ_SUCCESS;
602 }
603
604 /* Append caller-provided bytes and flush complete sectors immediately.
605 * The input is copied into a single sector buffer, so memory use is bounded
606 * independently of the total file size. */
607 int32_t
608 584 libmpq__writer_file_write(mpq_writer_s *w, const uint8_t *buffer, libmpq__off_t size)
609 {
610
9/10
✓ Branch 0 taken 582 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 579 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 581 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✓ Branch 9 taken 579 times.
584 if (!w || (!buffer && size != 0) || size < 0 || size > w->expected - w->written)
611 5 return LIBMPQ_ERROR_SIZE;
612
613 /* Fill the current sector, flushing exactly when it reaches capacity. */
614
2/2
✓ Branch 0 taken 955 times.
✓ Branch 1 taken 578 times.
1533 while (size != 0) {
615 955 uint32_t room = w->archive->write_sector_size - w->data_size;
616
2/2
✓ Branch 0 taken 401 times.
✓ Branch 1 taken 554 times.
955 uint32_t take = (uint32_t)((size < room) ? size : room);
617 955 memcpy(w->data + w->data_size, buffer, take);
618 955 w->data_size += take;
619 955 w->written += take;
620 955 buffer += take;
621 955 size -= take;
622
2/2
✓ Branch 0 taken 554 times.
✓ Branch 1 taken 401 times.
955 if (w->data_size == w->archive->write_sector_size) {
623 554 int32_t result = stream_flush_sector(w);
624
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 553 times.
554 if (result < 0)
625 1 return result;
626 }
627 }
628 578 return LIBMPQ_SUCCESS;
629 }
630
631 /* Flush the final sector and commit the file's block and hash-table entries.
632 * Cleanup occurs on both success and failure so the archive never retains an
633 * active writer after this function returns. */
634 int32_t
635 576 libmpq__writer_file_finish(mpq_writer_s *w)
636 {
637 int32_t result;
638
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 575 times.
576 if (!w)
639 1 return LIBMPQ_ERROR_EXIST;
640
4/4
✓ Branch 0 taken 178 times.
✓ Branch 1 taken 397 times.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 164 times.
575 if (w->data_size != 0 || w->expected == 0) {
641 411 result = stream_flush_sector(w);
642 } else {
643 164 result = 0;
644 }
645
1/2
✓ Branch 0 taken 575 times.
✗ Branch 1 not taken.
575 if (result == 0)
646 575 result = stream_finish(w);
647 575 w->archive->write_current = NULL;
648 575 free(w->name);
649 575 free(w->data);
650 575 free(w->offsets);
651 575 free(w);
652 575 return result;
653 }
654
655 /* Add an in-memory file through the streaming writer interface.
656 * This convenience wrapper uses the same sector pipeline as explicit begin,
657 * write, and finish calls and cleans up an aborted stream. */
658 int32_t
659 568 libmpq__writer_file_add(
660 mpq_archive_s *a, const char *name, const uint8_t *data, libmpq__off_t size,
661 const mpq_file_options_s *options
662 )
663 {
664 mpq_writer_s *w;
665 568 int32_t result = libmpq__writer_file_begin(a, name, size, options, &w);
666
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 565 times.
568 if (result < 0)
667 3 return result;
668 565 result = libmpq__writer_file_write(w, data, size);
669
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 564 times.
565 if (result < 0) {
670 1 free(w->name);
671 1 free(w->data);
672 1 free(w->offsets);
673 1 free(w);
674 1 a->write_current = NULL;
675 1 return result;
676 }
677 564 return libmpq__writer_file_finish(w);
678 }
679
680 /* Add a filesystem file while keeping only one input sector in memory.
681 * The source length is determined first so the streaming writer can enforce
682 * its declared size while reading bounded chunks from disk. */
683 int32_t
684 5 libmpq__writer_file_add_path(
685 mpq_archive_s *a, const char *name, const char *source, const mpq_file_options_s *options
686 )
687 {
688 FILE *fp;
689 off_t size;
690 uint8_t buffer[4096];
691 size_t got;
692 int32_t result;
693 mpq_writer_s *writer;
694 5 fp = fopen(source, "rb");
695
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (!fp)
696 1 return LIBMPQ_ERROR_OPEN;
697
3/6
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
4 if (fseeko(fp, 0, SEEK_END) < 0 || (size = ftello(fp)) < 0 || fseeko(fp, 0, SEEK_SET) < 0) {
698 fclose(fp);
699 return LIBMPQ_ERROR_SEEK;
700 }
701 4 result = libmpq__writer_file_begin(a, name, size, options, &writer);
702
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (result < 0) {
703 fclose(fp);
704 return result;
705 }
706
3/4
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
8 while (!feof(fp) && !ferror(fp)) {
707 4 got = fread(buffer, 1, sizeof(buffer), fp);
708
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (got == 0)
709 break;
710 4 result = libmpq__writer_file_write(writer, buffer, (libmpq__off_t)got);
711
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (result < 0)
712 break;
713 }
714
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
4 if (ferror(fp))
715 result = LIBMPQ_ERROR_READ;
716 4 fclose(fp);
717
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (result == 0)
718 4 result = libmpq__writer_file_finish(writer);
719 else {
720 a->write_current = NULL;
721 free(writer->name);
722 free(writer->data);
723 free(writer->offsets);
724 free(writer);
725 }
726 4 return result;
727 }
728
729 /* Finalize a writer archive and make it readable by libmpq.
730 * The public close path uses this internal wrapper to keep serialization in
731 * the writer module while preserving the archive handle lifecycle. */
732 int32_t
733 65 libmpq__writer_finalize(mpq_archive_s *a)
734 {
735 65 return finalize_archive(a);
736 }
737