GCC Code Coverage Report


Directory: src/
File: src/mpq-writer.c
Date: 2026-10-06 13:47:41
Exec Total Coverage
Lines: 615 795 77.4%
Functions: 28 28 100.0%
Branches: 431 622 69.3%

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-archive.h"
24 #include "mpq-attributes.h"
25 #include "mpq-block.h"
26 #include "mpq-compression.h"
27 #include "mpq-crypto.h"
28 #include "mpq-endian.h"
29 #include "mpq-file.h"
30 #include "mpq-mpqe.h"
31 #include "mpq-patch-reader.h"
32 #include "mpq-pkware.h"
33 #include "mpq-signature.h"
34 #include "mpq-wave.h"
35 #include "mpq-writer.h"
36 #include <libmpq/mpq.h>
37
38 #include <stdio.h>
39 #include <stdlib.h>
40 #include <string.h>
41 #include <zlib.h>
42
43 /* Resolve the private writer policy from the existing archive-creation flags. */
44 static libmpq_compression_policy_t
45 11745 compression_policy(const mpq_archive_s *archive)
46 {
47 11745 return (archive->write_flags & LIBMPQ_ARCHIVE_CREATE_COMPRESSION_EXTENDED) != 0
48 ? LIBMPQ_COMPRESSION_POLICY_EXTENDED
49 11745 : LIBMPQ_COMPRESSION_POLICY_STANDARD;
50 }
51
52 /*
53 * Return the smallest supported power-of-two table capacity at or above value.
54 * MPQ hash tables use power-of-two probing, so this helper provides the next
55 * legal capacity without exceeding the format's 32-bit range.
56 */
57 static uint32_t
58 618 next_power_two(uint32_t value)
59 {
60 618 uint32_t result = 4;
61
3/4
✓ Branch 0 taken 1233 times.
✓ Branch 1 taken 618 times.
✓ Branch 2 taken 1233 times.
✗ Branch 3 not taken.
1851 while (result < value && result < 0x80000000u)
62 1233 result <<= 1;
63 618 return result;
64 }
65
66 /*
67 * Write one byte range at an absolute archive offset.
68 * Seeking and writing are treated as one archive operation so short writes
69 * and positioning failures are converted to the library's write error.
70 */
71 static int32_t
72 3842 write_at(FILE *fp, uint64_t offset, const void *data, size_t size)
73 {
74
3/6
✓ Branch 1 taken 3842 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3842 times.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 3842 times.
3842 if (libmpq__file_seek(fp, offset, SEEK_SET) < 0 || (size && fwrite(data, 1, size, fp) != size))
75 ✗ return LIBMPQ_ERROR_WRITE;
76 3842 return 0;
77 }
78
79 /*
80 * Calculate the MPQ file encryption key from a normalized file name.
81 * Slash normalization matches MPQ's Windows-oriented name hashing while the
82 * temporary normalized string remains private to this calculation.
83 */
84 static uint32_t
85 2436 file_key(const char *name)
86 {
87 2436 char *normalized = NULL;
88 size_t i;
89 2436 size_t length = strlen(name);
90 uint32_t key;
91
92 2436 normalized = malloc(length + 1);
93
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2436 times.
2436 if (normalized == NULL)
94 ✗ return 0;
95
2/2
✓ Branch 0 taken 26343 times.
✓ Branch 1 taken 2436 times.
28779 for (i = 0; i < length; i++)
96
1/2
✓ Branch 0 taken 26343 times.
✗ Branch 1 not taken.
26343 normalized[i] = name[i] == '/' ? '\\' : name[i];
97 2436 normalized[length] = 0;
98 2436 key = libmpq__crypto_hash_string(normalized, 0x300);
99 2436 free(normalized);
100 2436 return key;
101 }
102
103 /*
104 * Compress, encrypt, and append the writer's currently buffered sector.
105 * Sector zero and later sectors may use different masks; the resulting offset
106 * is recorded relative to the payload for the table written during finish.
107 */
108 static int32_t
109 5931 stream_flush_sector(mpq_writer_s *writer)
110 {
111 5931 mpq_archive_s *archive = writer->archive;
112 5931 uint8_t *packed = NULL;
113 5931 size_t packed_size = writer->data_size;
114 5931 uint8_t emitted = 0;
115 11862 uint32_t requested = writer->sector_index == 0 ? writer->options.compression_first
116
2/2
✓ Branch 0 taken 2954 times.
✓ Branch 1 taken 2977 times.
5931 : writer->options.compression_next;
117 int32_t result;
118
119 /* Reject an extra flush before touching the archive stream. */
120
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5931 times.
5931 if (writer->sector_index >= writer->block_count)
121 ✗ return LIBMPQ_ERROR_SIZE;
122
2/2
✓ Branch 0 taken 4646 times.
✓ Branch 1 taken 1285 times.
5931 if ((writer->options.flags & LIBMPQ_FILE_FLAG_COMPRESS) != 0)
123 4646 result = libmpq__compression_encode_sector(
124 4646 writer->data, writer->data_size, requested, archive->mpq_header.version,
125 compression_policy(archive), &packed, &packed_size, &emitted
126 );
127
2/2
✓ Branch 0 taken 259 times.
✓ Branch 1 taken 1026 times.
1285 else if ((writer->options.flags & LIBMPQ_FILE_FLAG_IMPLODE) != 0) {
128 259 uint32_t packed32 = 0;
129 259 result = libmpq__pkzip_compress(writer->data, writer->data_size, &packed, &packed32);
130 259 packed_size = packed32;
131
3/4
✓ Branch 0 taken 259 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 51 times.
✓ Branch 3 taken 208 times.
259 if (result == 0 && packed_size >= writer->data_size) {
132 51 free(packed);
133
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 6 times.
51 packed = malloc(writer->data_size ? writer->data_size : 1);
134
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 51 times.
51 if (packed == NULL)
135 ✗ result = LIBMPQ_ERROR_MALLOC;
136 else {
137 51 memcpy(packed, writer->data, writer->data_size);
138 51 packed_size = writer->data_size;
139 }
140 }
141 } else {
142
2/2
✓ Branch 0 taken 950 times.
✓ Branch 1 taken 76 times.
1026 packed = malloc(packed_size ? packed_size : 1);
143
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1026 times.
1026 result = packed == NULL ? LIBMPQ_ERROR_MALLOC : 0;
144
1/2
✓ Branch 0 taken 1026 times.
✗ Branch 1 not taken.
1026 if (result == 0)
145 1026 memcpy(packed, writer->data, packed_size);
146 }
147
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5931 times.
5931 if (result < 0) {
148 ✗ free(packed);
149 ✗ return result;
150 }
151
152 /* Sector checksums cover packed bytes before payload encryption. */
153
2/2
✓ Branch 0 taken 196 times.
✓ Branch 1 taken 5735 times.
5931 if (writer->checksums != NULL)
154 196 writer->checksums[writer->sector_index] = (uint32_t)adler32(0, packed, (uInt)packed_size);
155
156 /* Payload encryption uses the file seed advanced by the sector number. */
157
2/2
✓ Branch 0 taken 1917 times.
✓ Branch 1 taken 4014 times.
5931 if (writer->options.flags & LIBMPQ_FILE_FLAG_ENCRYPTED)
158 1917 libmpq__crypto_encrypt_block(
159 1917 packed, (uint32_t)packed_size, file_key(writer->name) + writer->sector_index
160 );
161
2/2
✓ Branch 0 taken 4514 times.
✓ Branch 1 taken 1417 times.
5931 if (writer->offsets) {
162 4514 libmpq__off_t position = libmpq__file_tell(archive->fp);
163
164
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4514 times.
4514 if (position < 0) {
165 ✗ free(packed);
166 ✗ return LIBMPQ_ERROR_SEEK;
167 }
168 4514 writer->offsets[writer->sector_index] =
169 4514 (uint32_t)((uint64_t)position - writer->payload_offset);
170 }
171
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 5931 times.
5931 if (fwrite(packed, 1, packed_size, archive->fp) != packed_size) {
172 ✗ free(packed);
173 ✗ return LIBMPQ_ERROR_WRITE;
174 }
175 5931 writer->packed_total += packed_size;
176
2/2
✓ Branch 0 taken 2954 times.
✓ Branch 1 taken 2977 times.
5931 if (writer->sector_index == 0 &&
177
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2949 times.
2954 (writer->options.compression_next &
178 (LIBMPQ_COMPRESSION_WAVE_MONO | LIBMPQ_COMPRESSION_WAVE_STEREO))) {
179 libmpq_wave_info_s wave;
180
181 /* Validate the complete WAVE geometry before preserving lossy output. */
182
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
5 if (libmpq__wave_probe_pcm16_prefix(
183 5 writer->data, writer->data_size, (uint64_t)writer->expected, &wave
184 ) == 0) {
185 4 writer->options.compression_next &=
186 ~(LIBMPQ_COMPRESSION_WAVE_MONO | LIBMPQ_COMPRESSION_WAVE_STEREO);
187 4 writer->options.compression_next |=
188
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 wave.channels == 1 ? LIBMPQ_COMPRESSION_WAVE_MONO : LIBMPQ_COMPRESSION_WAVE_STEREO;
189 } else {
190 1 free(packed);
191 1 return LIBMPQ_ERROR_FORMAT;
192 }
193 }
194 5930 writer->sector_index++;
195 5930 writer->data_size = 0;
196 5930 free(packed);
197 5930 return 0;
198 }
199
200 /*
201 * Append the optional checksum table without file encryption. Like sector data,
202 * zlib framing is retained only when it reduces the serialized table size.
203 */
204 static int32_t
205 42 stream_write_checksums(mpq_writer_s *writer)
206 {
207 42 size_t size = (size_t)writer->block_count * sizeof(uint32_t);
208 42 uint8_t *raw = malloc(size);
209 42 uint8_t *packed = NULL;
210 42 size_t packed_size = 0;
211 42 uint8_t emitted = 0;
212 uint32_t i;
213 int32_t result;
214
215
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 42 times.
42 if (raw == NULL)
216 ✗ return LIBMPQ_ERROR_MALLOC;
217
2/2
✓ Branch 0 taken 196 times.
✓ Branch 1 taken 42 times.
238 for (i = 0; i < writer->block_count; ++i)
218 196 libmpq__store_le32(raw + i * 4, writer->checksums[i]);
219 42 result = libmpq__compression_encode_sector(
220 42 raw, size, LIBMPQ_COMPRESSION_ZLIB, writer->archive->mpq_header.version,
221 LIBMPQ_COMPRESSION_POLICY_STANDARD, &packed, &packed_size, &emitted
222 );
223 42 free(raw);
224
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 if (result == 0) {
225
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 42 times.
42 if (packed_size > UINT32_MAX - writer->offsets[writer->block_count])
226 ✗ result = LIBMPQ_ERROR_SIZE;
227
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
42 else if (fwrite(packed, 1, packed_size, writer->archive->fp) != packed_size)
228 ✗ result = LIBMPQ_ERROR_WRITE;
229 else {
230 42 writer->packed_total += packed_size;
231 42 writer->offsets[writer->block_count + 1] =
232 42 writer->offsets[writer->block_count] + (uint32_t)packed_size;
233 }
234 }
235 42 free(packed);
236 42 return result;
237 }
238
239 /*
240 * Finish the streamed file by writing its offset table and archive metadata.
241 * The serialized table is placed before packed sectors, encrypted separately,
242 * and then the completed file is inserted into the block and hash tables.
243 */
244 static int32_t
245 2954 stream_finish(mpq_writer_s *writer)
246 {
247 2954 mpq_archive_s *archive = writer->archive;
248 uint32_t index;
249 uint32_t slot;
250 uint32_t hash1;
251 uint32_t hash2;
252 uint32_t hash3;
253 uint32_t i;
254 uint64_t total;
255 uint64_t block_offset;
256 size_t table_size;
257 uint8_t *table;
258 int32_t result;
259
260 /* A file is publishable only after its declared byte and sector counts match. */
261
3/4
✓ Branch 0 taken 2952 times.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2952 times.
2954 if (writer->written != writer->expected || writer->sector_index != writer->block_count)
262 2 return LIBMPQ_ERROR_SIZE;
263
2/2
✓ Branch 0 taken 1738 times.
✓ Branch 1 taken 1214 times.
2952 if (writer->offsets) {
264 1738 uint32_t entries = writer->block_count + 1 + (writer->checksums != NULL);
265 1738 table_size = (size_t)entries * 4;
266
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1738 times.
1738 if (writer->packed_total > UINT32_MAX - table_size)
267 ✗ return LIBMPQ_ERROR_SIZE;
268 1738 writer->offsets[writer->block_count] =
269 1738 (uint32_t)writer->packed_total + (uint32_t)table_size;
270
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 1696 times.
1738 if (writer->checksums != NULL) {
271 42 result = stream_write_checksums(writer);
272
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 42 times.
42 if (result < 0)
273 ✗ return result;
274 }
275 1738 table = malloc(table_size);
276
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1738 times.
1738 if (table == NULL)
277 ✗ return LIBMPQ_ERROR_MALLOC;
278
2/2
✓ Branch 0 taken 6293 times.
✓ Branch 1 taken 1738 times.
8031 for (i = 0; i < entries; i++)
279 6293 libmpq__store_le32(table + i * 4, writer->offsets[i]);
280
2/2
✓ Branch 0 taken 519 times.
✓ Branch 1 taken 1219 times.
1738 if (writer->options.flags & LIBMPQ_FILE_FLAG_ENCRYPTED)
281 519 libmpq__crypto_encrypt_block(table, (uint32_t)table_size, file_key(writer->name) - 1);
282 1738 result = write_at(archive->fp, writer->payload_offset, table, table_size);
283 1738 free(table);
284
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1738 times.
1738 if (result < 0)
285 ✗ return result;
286 1738 total = writer->packed_total + table_size;
287 } else {
288 1214 total = writer->packed_total;
289 }
290
291 /* Restore the append position after rewriting the table at the file start. */
292
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 2952 times.
2952 if (libmpq__file_seek(archive->fp, (writer->payload_offset + total), SEEK_SET) < 0)
293 ✗ return LIBMPQ_ERROR_SEEK;
294
1/2
✓ Branch 0 taken 2952 times.
✗ Branch 1 not taken.
2952 if (writer->payload_offset < writer->prefix_size ||
295
3/4
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 2930 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 22 times.
2952 (writer->prefix_size != 0 && total > UINT32_MAX - writer->prefix_size))
296 ✗ return LIBMPQ_ERROR_SIZE;
297 2952 block_offset = writer->payload_offset - writer->prefix_size;
298
3/6
✓ Branch 0 taken 2952 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2952 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2952 times.
2952 if (writer->payload_offset > UINT32_MAX || total > UINT32_MAX || writer->expected > UINT32_MAX)
299 ✗ return archive->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_ONE
300 ? LIBMPQ_ERROR_SIZE
301 ✗ : (writer->payload_offset >> 32 > UINT16_MAX ? LIBMPQ_ERROR_SIZE : 0);
302
303 /* Reserve the next block slot only after payload and offset sizes validate. */
304 2952 index = archive->write_next_block;
305 2952 libmpq__file_hash(writer->name, &hash1, &hash2, &hash3);
306 2952 archive->mpq_block[index].offset = (uint32_t)block_offset;
307 2952 archive->mpq_block[index].packed_size = (uint32_t)total + writer->prefix_size;
308 2952 archive->mpq_block[index].unpacked_size =
309
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 2930 times.
2952 writer->patch_file ? writer->patch_result_size : (uint32_t)writer->expected;
310 5904 archive->mpq_block[index].flags = LIBMPQ_FLAG_EXISTS | writer->options.flags |
311
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 2930 times.
2952 (writer->patch_file ? LIBMPQ_FILE_FLAG_PATCH_FILE : 0);
312
2/2
✓ Branch 0 taken 2014 times.
✓ Branch 1 taken 938 times.
2952 if (writer->options.flags & LIBMPQ_FILE_FLAG_COMPRESS)
313 2014 archive->mpq_block[index].flags |= LIBMPQ_FLAG_COMPRESS_MULTI;
314 2952 archive->mpq_block_ex[index].offset_high = (uint16_t)(block_offset >> 32);
315 2952 archive->write_names[index] = writer->name;
316 2952 archive->write_locales[index] = writer->options.locale;
317 2952 archive->write_platforms[index] = writer->options.platform;
318 2952 writer->name = NULL;
319 2952 archive->write_next_block++;
320 2952 archive->files = archive->write_next_block;
321
1/2
✓ Branch 0 taken 3225 times.
✗ Branch 1 not taken.
3225 for (slot = 0; slot < archive->write_hash_capacity; slot++) {
322 3225 uint32_t pos = (hash1 + slot) & (archive->write_hash_capacity - 1);
323
2/2
✓ Branch 0 taken 2952 times.
✓ Branch 1 taken 273 times.
3225 if (archive->mpq_hash[pos].block_table_index == LIBMPQ_HASH_FREE) {
324 2952 archive->mpq_hash[pos].hash_a = hash2;
325 2952 archive->mpq_hash[pos].hash_b = hash3;
326 2952 archive->mpq_hash[pos].locale = writer->options.locale;
327 2952 archive->mpq_hash[pos].platform = writer->options.platform;
328 2952 archive->mpq_hash[pos].block_table_index = index;
329
2/2
✓ Branch 0 taken 904 times.
✓ Branch 1 taken 2048 times.
2952 if (archive->write_attributes != NULL) {
330
2/2
✓ Branch 0 taken 792 times.
✓ Branch 1 taken 112 times.
904 if (writer->attributes.flags & LIBMPQ_ATTRIBUTE_MD5)
331 792 libmpq__md5_final(&writer->md5, writer->attributes.md5);
332 904 archive->write_attributes[index] = writer->attributes;
333 }
334 2952 return 0;
335 }
336 }
337 ✗ return LIBMPQ_ERROR_SIZE;
338 }
339
340 /*
341 * Serialize the completed hash, block, extended tables, and archive header.
342 * Finalization optionally creates the listfile, encrypts metadata with the
343 * fixed MPQ table keys, writes v2 high offsets, and commits the header last.
344 */
345 static int32_t
346 616 finalize_archive(mpq_archive_s *a)
347 {
348 uint8_t *raw;
349 uint32_t i;
350 size_t bytes;
351 uint64_t end;
352 uint8_t header[LIBMPQ_HEADER_WIRE_SIZE + LIBMPQ_HEADER_EX_WIRE_SIZE];
353 mpq_header_s wire_header;
354 mpq_header_ex_s wire_ex;
355 int32_t result;
356
357 /* An unfinished streamed file would leave archive tables inconsistent. */
358
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 615 times.
616 if (a->write_current)
359 1 return LIBMPQ_ERROR_SIZE;
360 615 a->write_internal = 1;
361
3/4
✓ Branch 1 taken 349 times.
✓ Branch 2 taken 266 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 349 times.
615 if (libmpq__attributes_write_flags(a) != 0 && a->write_attributes == NULL) {
362 ✗ a->write_attributes = calloc(a->write_capacity, sizeof(*a->write_attributes));
363 ✗ if (a->write_attributes == NULL)
364 ✗ return LIBMPQ_ERROR_MALLOC;
365 }
366
2/2
✓ Branch 0 taken 133 times.
✓ Branch 1 taken 482 times.
615 if ((a->write_flags & LIBMPQ_ARCHIVE_CREATE_LISTFILE) != 0) {
367
2/2
✓ Branch 1 taken 92 times.
✓ Branch 2 taken 41 times.
133 size_t total = 1 + (libmpq__attributes_write_flags(a) ? sizeof(LIBMPQ_ATTRIBUTES_NAME) : 0);
368 uint8_t *list;
369
2/2
✓ Branch 0 taken 863 times.
✓ Branch 1 taken 133 times.
996 for (i = 0; i < a->write_next_block; i++)
370 863 total += strlen(a->write_names[i]) + 1;
371 133 list = malloc(total);
372
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 133 times.
133 if (!list)
373 ✗ return LIBMPQ_ERROR_MALLOC;
374 133 total = 0;
375
2/2
✓ Branch 0 taken 863 times.
✓ Branch 1 taken 133 times.
996 for (i = 0; i < a->write_next_block; i++) {
376 863 size_t n = strlen(a->write_names[i]);
377 863 memcpy(list + total, a->write_names[i], n);
378 863 total += n;
379 863 list[total++] = '\n';
380 }
381
2/2
✓ Branch 1 taken 92 times.
✓ Branch 2 taken 41 times.
133 if (libmpq__attributes_write_flags(a)) {
382 92 memcpy(list + total, LIBMPQ_ATTRIBUTES_NAME "\n", sizeof(LIBMPQ_ATTRIBUTES_NAME));
383 92 total += sizeof(LIBMPQ_ATTRIBUTES_NAME);
384 }
385 {
386 133 mpq_file_options_s o = { LIBMPQ_FILE_FLAG_SINGLE, 0, 0, 0, 0 };
387 int32_t r =
388 133 libmpq__writer_file_add(a, LIBMPQ_LISTFILE_NAME, list, (libmpq__off_t)total, &o);
389 133 free(list);
390
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 133 times.
133 if (r < 0)
391 ✗ return r;
392 }
393 }
394
395
2/2
✓ Branch 1 taken 349 times.
✓ Branch 2 taken 266 times.
615 if (libmpq__attributes_write_flags(a) != 0) {
396 349 mpq_file_options_s options = { LIBMPQ_FILE_FLAG_COMPRESS, LIBMPQ_COMPRESSION_ZLIB,
397 LIBMPQ_COMPRESSION_ZLIB, 0, 0 };
398
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 324 times.
349 if (a->write_patch_mode &&
399
1/2
✓ Branch 1 taken 25 times.
✗ Branch 2 not taken.
25 (libmpq__attributes_write_flags(a) & LIBMPQ_ATTRIBUTE_PATCH_BIT) != 0) {
400
2/2
✓ Branch 0 taken 61 times.
✓ Branch 1 taken 25 times.
86 for (i = 0; i < a->write_next_block; i++)
401 61 a->write_attributes[i].patch_bit =
402 61 (a->mpq_block[i].flags & LIBMPQ_FILE_FLAG_PATCH_FILE) != 0;
403 }
404 int32_t result =
405 349 (a->write_patch_mode ? libmpq__attributes_serialize_patch_bits
406
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 324 times.
349 : libmpq__attributes_serialize)(
407 349 a->write_attributes, a->write_capacity, a->write_next_block,
408 libmpq__attributes_write_flags(a), &raw, &bytes
409 );
410
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 349 times.
349 if (result != 0)
411 ✗ return result;
412 result =
413 349 libmpq__writer_file_add(a, LIBMPQ_ATTRIBUTES_NAME, raw, (libmpq__off_t)bytes, &options);
414 349 free(raw);
415
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 349 times.
349 if (result != 0)
416 ✗ return result;
417 }
418
419 /* Serialize and encrypt the hash table before writing the block metadata. */
420 615 bytes = (size_t)a->write_hash_capacity * LIBMPQ_HASH_ENTRY_WIRE_SIZE;
421 615 raw = malloc(bytes);
422
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 615 times.
615 if (!raw)
423 ✗ return LIBMPQ_ERROR_MALLOC;
424 615 result = libmpq__hash_table_encode(a->mpq_hash, a->write_hash_capacity, raw, bytes);
425
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 615 times.
615 if (result < 0) {
426 ✗ free(raw);
427 ✗ return result;
428 }
429 615 libmpq__crypto_encrypt_block(
430 raw, (uint32_t)bytes, libmpq__crypto_hash_string("(hash table)", 0x300)
431 );
432
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 615 times.
615 if (write_at(a->fp, a->mpq_header.hash_table_offset, raw, bytes) < 0) {
433 ✗ free(raw);
434 ✗ return LIBMPQ_ERROR_WRITE;
435 }
436 615 free(raw);
437
438 /* Serialize the fixed-capacity block table before its existing encryption. */
439 615 bytes = (size_t)a->write_capacity * LIBMPQ_BLOCK_ENTRY_WIRE_SIZE;
440 615 raw = malloc(bytes);
441
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 615 times.
615 if (!raw)
442 ✗ return LIBMPQ_ERROR_MALLOC;
443 615 result = libmpq__block_table_encode(a->mpq_block, a->write_capacity, raw, bytes);
444
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 615 times.
615 if (result < 0) {
445 ✗ free(raw);
446 ✗ return result;
447 }
448 615 libmpq__crypto_encrypt_block(
449 raw, (uint32_t)bytes, libmpq__crypto_hash_string("(block table)", 0x300)
450 );
451
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 615 times.
615 if (write_at(a->fp, a->mpq_header.block_table_offset, raw, bytes) < 0) {
452 ✗ free(raw);
453 ✗ return LIBMPQ_ERROR_WRITE;
454 }
455 615 free(raw);
456
2/2
✓ Branch 0 taken 237 times.
✓ Branch 1 taken 378 times.
615 if (a->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_TWO) {
457 237 bytes = (size_t)a->write_capacity * LIBMPQ_BLOCK_EX_ENTRY_WIRE_SIZE;
458 237 raw = malloc(bytes);
459
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 237 times.
237 if (!raw)
460 ✗ return LIBMPQ_ERROR_MALLOC;
461 237 result = libmpq__block_ex_table_encode(a->mpq_block_ex, a->write_capacity, raw, bytes);
462
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 237 times.
237 if (result < 0) {
463 ✗ free(raw);
464 ✗ return result;
465 }
466
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 237 times.
237 if (write_at(a->fp, a->mpq_header_ex.extended_offset, raw, bytes) < 0) {
467 ✗ free(raw);
468 ✗ return LIBMPQ_ERROR_WRITE;
469 }
470 237 free(raw);
471 }
472
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 615 times.
615 if (libmpq__file_seek(a->fp, 0, SEEK_END) < 0)
473 ✗ return LIBMPQ_ERROR_SEEK;
474 615 end = (uint64_t)libmpq__file_tell(a->fp);
475
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 615 times.
615 if (end > INT64_MAX)
476 ✗ return LIBMPQ_ERROR_SEEK;
477
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 615 times.
615 if (end > UINT32_MAX)
478 ✗ return LIBMPQ_ERROR_SIZE;
479 615 memset(header, 0, sizeof(header));
480 615 wire_header = a->mpq_header;
481 615 wire_header.mpq_magic = LIBMPQ_HEADER;
482 615 wire_header.archive_size = (uint32_t)end;
483 615 result = libmpq__header_encode(&wire_header, header, sizeof(header));
484
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 615 times.
615 if (result < 0)
485 ✗ return result;
486
2/2
✓ Branch 0 taken 237 times.
✓ Branch 1 taken 378 times.
615 if (a->mpq_header.version == LIBMPQ_ARCHIVE_VERSION_TWO) {
487 237 wire_ex = a->mpq_header_ex;
488 237 wire_ex.hash_table_offset_high =
489 237 (uint16_t)(((uint64_t)a->mpq_header.hash_table_offset) >> 32);
490 237 wire_ex.block_table_offset_high =
491 237 (uint16_t)(((uint64_t)a->mpq_header.block_table_offset) >> 32);
492 237 result = libmpq__header_ex_encode(
493 &wire_ex, header + LIBMPQ_HEADER_WIRE_SIZE, LIBMPQ_HEADER_EX_WIRE_SIZE
494 );
495
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 237 times.
237 if (result < 0)
496 ✗ return result;
497 }
498
2/4
✓ Branch 1 taken 615 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 615 times.
615 if (write_at(a->fp, 0, header, a->mpq_header.header_size) < 0 || fflush(a->fp) != 0)
499 ✗ return LIBMPQ_ERROR_WRITE;
500 {
501 615 int32_t result = libmpq__signature_finish(a, end);
502
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 615 times.
615 if (result != 0)
503 ✗ return result;
504 }
505
2/2
✓ Branch 0 taken 532 times.
✓ Branch 1 taken 83 times.
615 if (!a->write_mpqe)
506 532 a->write_finalized = 1;
507 615 return 0;
508 }
509
510 /* Transform the finalized raw archive with bounded sequential MPQE I/O. */
511 int32_t
512 87 libmpq__writer_mpqe_transform_file(
513 FILE *input, FILE *output, const uint8_t key[LIBMPQ_MPQE_CHUNK_SIZE]
514 )
515 {
516 uint8_t chunk[LIBMPQ_MPQE_CHUNK_SIZE];
517 87 uint64_t offset = 0;
518 size_t read;
519
520
3/6
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 87 times.
87 if (input == NULL || output == NULL || key == NULL)
521 ✗ return LIBMPQ_ERROR_EXIST;
522
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
87 if (fflush(input) != 0)
523 ✗ return LIBMPQ_ERROR_WRITE;
524
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
87 if (libmpq__file_seek(input, 0, SEEK_SET) < 0)
525 ✗ return LIBMPQ_ERROR_SEEK;
526
2/2
✓ Branch 1 taken 13498 times.
✓ Branch 2 taken 87 times.
13585 while ((read = fread(chunk, 1, sizeof(chunk), input)) != 0) {
527
2/2
✓ Branch 0 taken 86 times.
✓ Branch 1 taken 13412 times.
13498 if (read < sizeof(chunk))
528 86 memset(chunk + read, 0, sizeof(chunk) - read);
529 13498 libmpq__mpqe_transform_chunk(chunk, key, offset);
530
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 13498 times.
13498 if (fwrite(chunk, 1, read, output) != read) {
531 ✗ libmpq__mpqe_clear(chunk, sizeof(chunk));
532 ✗ return LIBMPQ_ERROR_WRITE;
533 }
534 13498 offset += read;
535 }
536 87 libmpq__mpqe_clear(chunk, sizeof(chunk));
537
2/4
✓ Branch 1 taken 87 times.
✗ Branch 2 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 87 times.
87 if (ferror(input) || fflush(output) != 0)
538 ✗ return LIBMPQ_ERROR_WRITE;
539 87 return 0;
540 }
541
542 /* Keep the archive-writer finalizer on the shared MPQE transform path. */
543 static int32_t
544 81 transform_mpqe(mpq_archive_s *a)
545 {
546 81 return libmpq__writer_mpqe_transform_file(a->fp, a->write_mpqe_output, a->write_mpqe_key);
547 }
548
549 /* Production MPQE finalization operations used by every newly created MPQE archive. */
550 static const mpq_writer_mpqe_ops_s default_mpqe_ops = {
551 finalize_archive,
552 transform_mpqe,
553 fclose,
554 libmpq__directory_replace,
555 };
556
557 /* Finish MPQE output only after the ordinary MPQ writer has finalized its raw stream. */
558 int32_t
559 83 libmpq__writer_finalize_mpqe(mpq_archive_s *a)
560 {
561 const mpq_writer_mpqe_ops_s *ops;
562 int32_t result;
563
564
2/4
✓ Branch 0 taken 83 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 83 times.
83 if (a == NULL || !a->write_mpqe)
565 ✗ return LIBMPQ_ERROR_EXIST;
566 83 ops = a->write_mpqe_ops;
567
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 83 times.
83 if (ops == NULL)
568 ✗ return LIBMPQ_ERROR_EXIST;
569 83 result = ops->transform(a);
570
2/6
✓ Branch 0 taken 83 times.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✓ Branch 4 taken 83 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
83 if (a->fp != NULL && fclose(a->fp) != 0 && result == 0)
571 ✗ result = LIBMPQ_ERROR_CLOSE;
572 83 a->fp = NULL;
573
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 81 times.
83 if (result != 0)
574 2 return result;
575
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 81 times.
81 if (libmpq__directory_remove(a->write_mpqe_directory, a->filename) != 0)
576 ✗ return LIBMPQ_ERROR_WRITE;
577
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 79 times.
81 if (ops->close_output(a->write_mpqe_output) != 0) {
578 2 a->write_mpqe_output = NULL;
579 2 return LIBMPQ_ERROR_CLOSE;
580 }
581 79 a->write_mpqe_output = NULL;
582 result =
583 79 ops->publish(a->write_mpqe_directory, a->write_mpqe_output_path, a->write_mpqe_destination);
584
2/2
✓ Branch 0 taken 77 times.
✓ Branch 1 taken 2 times.
79 if (result == 0)
585 77 a->write_finalized = 1;
586 79 return result;
587 }
588
589 /* Close/unlink private MPQE artifacts on every handled completion or failure. */
590 void
591 618 libmpq__writer_mpqe_cleanup(mpq_archive_s *a)
592 {
593
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 618 times.
618 if (a == NULL)
594 ✗ return;
595
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 614 times.
618 if (a->write_mpqe_output != NULL)
596 4 (void)fclose(a->write_mpqe_output);
597 618 a->write_mpqe_output = NULL;
598
4/6
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 533 times.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 85 times.
✗ Branch 5 not taken.
618 if (a->write_mpqe && a->write_mpqe_directory != NULL && a->filename != NULL)
599 85 (void)libmpq__directory_remove(a->write_mpqe_directory, a->filename);
600
3/4
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 533 times.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
618 if (a->write_mpqe_directory != NULL && a->write_mpqe_output_path != NULL)
601 85 (void)libmpq__directory_remove(a->write_mpqe_directory, a->write_mpqe_output_path);
602
2/2
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 533 times.
618 if (a->write_mpqe_directory != NULL)
603 85 (void)libmpq__directory_close(a->write_mpqe_directory);
604 618 a->write_mpqe_directory = NULL;
605 618 libmpq__mpqe_clear(a->write_mpqe_key, sizeof(a->write_mpqe_key));
606 }
607
608 /*
609 * Create a new seekable MPQ v1 or v2 archive with reserved metadata tables.
610 * The function validates creation options, reserves table space, initializes
611 * empty hash entries, and leaves the file positioned at the first payload.
612 */
613 static int32_t
614 1131 writer_validate_options(const mpq_archive_create_options_s *options)
615 {
616
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1129 times.
1131 if (options == NULL)
617 2 return 0;
618
4/4
✓ Branch 0 taken 1128 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1127 times.
✓ Branch 3 taken 1 times.
1129 if (options->version > LIBMPQ_ARCHIVE_VERSION_TWO || options->max_files == UINT32_MAX ||
619
2/4
✓ Branch 0 taken 1127 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1127 times.
1127 (options->max_files && options->max_files > 0x40000000u))
620 2 return LIBMPQ_ERROR_FORMAT;
621
1/2
✓ Branch 0 taken 1127 times.
✗ Branch 1 not taken.
1127 if (options->sector_size &&
622
3/4
✓ Branch 0 taken 1127 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1126 times.
1127 (options->sector_size < 512 || (options->sector_size & (options->sector_size - 1)) != 0))
623 1 return LIBMPQ_ERROR_FORMAT;
624
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1122 times.
1126 if ((options->attributes & ~LIBMPQ_ATTRIBUTES_ALL) != 0)
625 4 return LIBMPQ_ERROR_FORMAT;
626
4/6
✓ Branch 0 taken 679 times.
✓ Branch 1 taken 443 times.
✓ Branch 2 taken 679 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 679 times.
1801 if (options->attributes != 0 && options->max_files != 0 &&
627
2/2
✓ Branch 0 taken 165 times.
✓ Branch 1 taken 514 times.
679 options->max_files < 1u + ((options->flags & LIBMPQ_ARCHIVE_CREATE_LISTFILE) != 0))
628 ✗ return LIBMPQ_ERROR_SIZE;
629 1122 return 0;
630 }
631
632 /* Allocate the writer before opening ordinary output, or adopt a private stream. */
633 static int32_t
634 618 writer_archive_create_handle(
635 mpq_archive_s **out, const char *path, FILE *file, const mpq_archive_create_options_s *options
636 )
637 {
638 618 mpq_archive_create_options_s defaults = { LIBMPQ_ARCHIVE_VERSION_ONE, 1024, 4096, 0, 0 };
639 mpq_archive_s *a;
640 uint32_t i;
641 uint32_t header_size;
642 uint64_t offset;
643 uint8_t *zero;
644
645
2/4
✓ Branch 0 taken 618 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 618 times.
618 if (out == NULL || path == NULL)
646 ✗ return LIBMPQ_ERROR_EXIST;
647 618 *out = NULL;
648
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 616 times.
618 if (options == NULL)
649 2 options = &defaults;
650
651 /* Reject versions, capacities, and sector sizes that cannot be represented safely. */
652
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 618 times.
618 if (writer_validate_options(options) != 0) {
653 ✗ if (file != NULL)
654 ✗ (void)fclose(file);
655 ✗ return LIBMPQ_ERROR_FORMAT;
656 }
657 618 a = calloc(1, sizeof(*a));
658
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 618 times.
618 if (a == NULL) {
659 ✗ if (file != NULL)
660 ✗ (void)fclose(file);
661 ✗ return LIBMPQ_ERROR_MALLOC;
662 }
663 618 a->write_mpqe_directory = NULL;
664 618 a->fp = file;
665
2/2
✓ Branch 0 taken 421 times.
✓ Branch 1 taken 197 times.
618 if (a->fp == NULL)
666 421 a->fp = libmpq__file_open(path, "w+b");
667 618 a->filename = libmpq__string_duplicate(path);
668
2/4
✓ Branch 0 taken 618 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 618 times.
618 if (a->fp == NULL || a->filename == NULL) {
669 ✗ if (a->fp != NULL)
670 ✗ (void)fclose(a->fp);
671 ✗ free(a->filename);
672 ✗ free(a);
673 ✗ return LIBMPQ_ERROR_OPEN;
674 }
675 618 a->write_mode = 1;
676
1/2
✓ Branch 0 taken 618 times.
✗ Branch 1 not taken.
618 a->write_capacity = options->max_files ? options->max_files : 1024;
677
1/2
✓ Branch 0 taken 618 times.
✗ Branch 1 not taken.
618 a->write_sector_size = options->sector_size ? options->sector_size : 4096;
678 618 a->write_flags = options->flags;
679 618 a->write_attributes_flags = options->attributes;
680
681 /* Keep hash load below one half so linear probing remains bounded. */
682 618 a->write_hash_capacity = next_power_two(a->write_capacity * 2);
683 1236 header_size = options->version == LIBMPQ_ARCHIVE_VERSION_TWO
684 ? LIBMPQ_HEADER_WIRE_SIZE + LIBMPQ_HEADER_EX_WIRE_SIZE
685
2/2
✓ Branch 0 taken 237 times.
✓ Branch 1 taken 381 times.
618 : LIBMPQ_HEADER_WIRE_SIZE;
686 618 a->mpq_header.version = (uint16_t)options->version;
687 618 a->mpq_header.header_size = header_size;
688 618 a->mpq_header.block_size = 0;
689
2/2
✓ Branch 0 taken 868 times.
✓ Branch 1 taken 618 times.
1486 while ((512u << a->mpq_header.block_size) < a->write_sector_size)
690 868 a->mpq_header.block_size++;
691 618 a->mpq_header.hash_table_count = a->write_hash_capacity;
692 618 a->mpq_header.block_table_count = a->write_capacity;
693 618 a->mpq_header.hash_table_offset = header_size;
694
695 /*
696 * StormLib skips v1 attributes when a table starts exactly at header end.
697 * Reserve a small gap only for opt-in attributes, leaving ordinary output unchanged.
698 */
699
4/4
✓ Branch 0 taken 381 times.
✓ Branch 1 taken 237 times.
✓ Branch 3 taken 185 times.
✓ Branch 4 taken 196 times.
618 if (options->version == LIBMPQ_ARCHIVE_VERSION_ONE && libmpq__attributes_write_flags(a))
700 185 a->mpq_header.hash_table_offset += 16;
701 618 a->mpq_header.block_table_offset =
702 618 a->mpq_header.hash_table_offset + a->write_hash_capacity * LIBMPQ_HASH_ENTRY_WIRE_SIZE;
703 618 a->mpq_header_ex.extended_offset = 0;
704 618 offset = (uint64_t)a->mpq_header.block_table_offset +
705 618 (uint64_t)a->write_capacity * LIBMPQ_BLOCK_ENTRY_WIRE_SIZE;
706
2/2
✓ Branch 0 taken 237 times.
✓ Branch 1 taken 381 times.
618 if (options->version == LIBMPQ_ARCHIVE_VERSION_TWO) {
707 237 a->mpq_header_ex.extended_offset = offset;
708 237 offset += (uint64_t)a->write_capacity * LIBMPQ_BLOCK_EX_ENTRY_WIRE_SIZE;
709 }
710 618 offset = (offset + 511) & ~UINT64_C(511);
711
3/4
✓ Branch 0 taken 381 times.
✓ Branch 1 taken 237 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 381 times.
618 if (options->version == LIBMPQ_ARCHIVE_VERSION_ONE && offset > UINT32_MAX) {
712 ✗ fclose(a->fp);
713 ✗ free(a->filename);
714 ✗ free(a);
715 ✗ return LIBMPQ_ERROR_SIZE;
716 }
717 618 a->mpq_hash = calloc(a->write_hash_capacity, sizeof(*a->mpq_hash));
718 618 a->mpq_block = calloc(a->write_capacity, sizeof(*a->mpq_block));
719 618 a->mpq_block_ex = calloc(a->write_capacity, sizeof(*a->mpq_block_ex));
720 618 a->write_names = calloc(a->write_capacity, sizeof(*a->write_names));
721 618 a->write_locales = calloc(a->write_capacity, sizeof(*a->write_locales));
722 618 a->write_platforms = calloc(a->write_capacity, sizeof(*a->write_platforms));
723
5/10
✓ Branch 0 taken 618 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 618 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 618 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 618 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 618 times.
✗ Branch 9 not taken.
618 if (!a->mpq_hash || !a->mpq_block || !a->mpq_block_ex || !a->write_names || !a->write_locales ||
724
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 618 times.
618 !a->write_platforms) {
725 ✗ fclose(a->fp);
726 ✗ free(a->filename);
727 ✗ free(a->mpq_hash);
728 ✗ free(a->mpq_block);
729 ✗ free(a->mpq_block_ex);
730 ✗ free(a->write_names);
731 ✗ free(a->write_locales);
732 ✗ free(a->write_platforms);
733 ✗ free(a);
734 ✗ return LIBMPQ_ERROR_MALLOC;
735 }
736
2/2
✓ Branch 0 taken 74600 times.
✓ Branch 1 taken 618 times.
75218 for (i = 0; i < a->write_hash_capacity; i++)
737 74600 a->mpq_hash[i].block_table_index = LIBMPQ_HASH_FREE;
738 618 zero = calloc(1, (size_t)(offset > 4096 ? 4096 : offset));
739
1/2
✓ Branch 0 taken 618 times.
✗ Branch 1 not taken.
618 if (zero == NULL || fwrite(zero, 1, (size_t)(offset > 4096 ? 4096 : offset), a->fp) !=
740
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 618 times.
618 (size_t)(offset > 4096 ? 4096 : offset)) {
741 ✗ free(zero);
742 ✗ fclose(a->fp);
743 ✗ free(a->filename);
744 ✗ free(a->mpq_hash);
745 ✗ free(a->mpq_block);
746 ✗ free(a->mpq_block_ex);
747 ✗ free(a->write_names);
748 ✗ free(a->write_locales);
749 ✗ free(a->write_platforms);
750 ✗ free(a);
751 ✗ return LIBMPQ_ERROR_WRITE;
752 }
753 618 free(zero);
754
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 618 times.
618 if (libmpq__file_seek(a->fp, offset, SEEK_SET) < 0) {
755 ✗ fclose(a->fp);
756 ✗ free(a->filename);
757 ✗ free(a->mpq_hash);
758 ✗ free(a->mpq_block);
759 ✗ free(a->mpq_block_ex);
760 ✗ free(a->write_names);
761 ✗ free(a->write_locales);
762 ✗ free(a->write_platforms);
763 ✗ free(a);
764 ✗ return LIBMPQ_ERROR_SEEK;
765 }
766 618 *out = a;
767 618 return 0;
768 }
769
770 /* Create an ordinary seekable MPQ directly at its requested destination. */
771 int32_t
772 428 libmpq__writer_archive_create(
773 mpq_archive_s **out, const char *path, const mpq_archive_create_options_s *options
774 )
775 {
776
4/4
✓ Branch 0 taken 427 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 426 times.
428 if (out == NULL || path == NULL)
777 2 return LIBMPQ_ERROR_EXIST;
778 426 *out = NULL;
779
2/2
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 421 times.
426 if (writer_validate_options(options) != 0)
780 5 return LIBMPQ_ERROR_FORMAT;
781 421 return writer_archive_create_handle(out, path, NULL, options);
782 }
783
784 int32_t
785 112 libmpq__writer_archive_create_file(
786 mpq_archive_s **out, const char *path, FILE *file, const mpq_archive_create_options_s *options
787 )
788 {
789 112 return writer_archive_create_handle(out, path, file, options);
790 }
791
792 /*
793 * Create raw and encrypted private files, then initialize the unchanged MPQ writer on raw output.
794 */
795 int32_t
796 90 libmpq__writer_archive_create_mpqe(
797 mpq_archive_s **out, const char *path, const uint8_t *auth_code, size_t auth_code_size,
798 const mpq_archive_create_options_s *options
799 )
800 {
801 90 FILE *raw = NULL;
802 90 FILE *encrypted = NULL;
803 90 char *raw_path = NULL;
804 90 char *encrypted_path = NULL;
805 90 char *destination = NULL;
806 uint8_t key[LIBMPQ_MPQE_CHUNK_SIZE];
807 90 mpq_directory_s *directory = NULL;
808 int32_t result;
809
810
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 90 times.
90 if (out == NULL)
811 ✗ return LIBMPQ_ERROR_EXIST;
812 90 *out = NULL;
813 90 result = libmpq__mpqe_key(key, auth_code, auth_code_size);
814
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 87 times.
90 if (result != 0)
815 3 return result;
816 87 result = writer_validate_options(options);
817
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 85 times.
87 if (result != 0) {
818 2 libmpq__mpqe_clear(key, sizeof(key));
819 2 return result;
820 }
821 85 result = libmpq__directory_open(path, &directory, &destination);
822
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 if (result == 0)
823 85 result = libmpq__directory_temporary(
824 directory, ".libmpq-raw-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 1, &raw_path, &raw
825 );
826
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 if (result == 0)
827 85 result = libmpq__directory_temporary(
828 directory, ".libmpq-mpqe-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 0, &encrypted_path,
829 &encrypted
830 );
831
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 if (result == 0) {
832 85 result = writer_archive_create_handle(out, raw_path, raw, options);
833 85 raw = NULL;
834 }
835
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 85 times.
85 if (result != 0) {
836 ✗ if (raw != NULL)
837 ✗ (void)fclose(raw);
838 ✗ if (encrypted != NULL)
839 ✗ (void)fclose(encrypted);
840 ✗ if (directory != NULL && raw_path != NULL)
841 ✗ (void)libmpq__directory_remove(directory, raw_path);
842 ✗ if (directory != NULL && encrypted_path != NULL)
843 ✗ (void)libmpq__directory_remove(directory, encrypted_path);
844 ✗ if (directory != NULL)
845 ✗ (void)libmpq__directory_close(directory);
846 ✗ free(raw_path);
847 ✗ free(encrypted_path);
848 ✗ free(destination);
849 ✗ libmpq__mpqe_clear(key, sizeof(key));
850 ✗ return result;
851 }
852 85 (*out)->write_mpqe = 1;
853 85 memcpy((*out)->write_mpqe_key, key, sizeof(key));
854 85 (*out)->write_mpqe_output = encrypted;
855 85 (*out)->write_mpqe_directory = directory;
856 85 (*out)->write_mpqe_destination = destination;
857 85 (*out)->write_mpqe_output_path = encrypted_path;
858 85 (*out)->write_mpqe_ops = &default_mpqe_ops;
859 85 encrypted = NULL;
860 85 encrypted_path = NULL;
861 85 destination = NULL;
862 85 directory = NULL;
863 85 free(raw_path);
864 85 libmpq__mpqe_clear(key, sizeof(key));
865 85 return 0;
866 }
867
868 /*
869 * Begin one file, optionally reserving a plaintext patch prefix before its
870 * normally encoded body. Option and duplicate-name validation precedes the
871 * prefix write; the eventual block entry remains unpublished until finish.
872 */
873 static int32_t
874 3147 writer_file_begin(
875 mpq_archive_s *a, const char *name, libmpq__off_t size, const mpq_file_options_s *options,
876 const uint8_t *prefix, uint32_t prefix_size, libmpq__off_t result_size, mpq_writer_s **out
877 )
878 {
879 3147 mpq_file_options_s defaults = { 0, 0, 0, 0, 0 };
880 mpq_writer_s *w;
881
13/14
✓ Branch 0 taken 3146 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 3146 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3145 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 3144 times.
✓ Branch 7 taken 1 times.
✓ Branch 8 taken 3142 times.
✓ Branch 9 taken 2 times.
✓ Branch 10 taken 3141 times.
✓ Branch 11 taken 1 times.
✓ Branch 12 taken 22 times.
✓ Branch 13 taken 3119 times.
3147 if (!a || !a->write_mode || !name || !out || size < 0 || a->write_current ||
882
3/6
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
22 (prefix_size != 0 && (!a->write_patch_mode || prefix == NULL || result_size < 0 ||
883
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 (uint64_t)result_size > UINT32_MAX)))
884 6 return LIBMPQ_ERROR_FORMAT;
885
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 3120 times.
3141 if (a->write_signature) {
886 uint32_t h1;
887 uint32_t h2;
888 uint32_t h3;
889 uint32_t s1;
890 uint32_t s2;
891 uint32_t s3;
892 21 libmpq__file_hash(name, &h1, &h2, &h3);
893 21 libmpq__file_hash(LIBMPQ_SIGNATURE_NAME, &s1, &s2, &s3);
894
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 20 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
21 if (h2 == s2 && h3 == s3)
895 1 return LIBMPQ_ERROR_FORMAT;
896 }
897
2/2
✓ Branch 0 taken 346 times.
✓ Branch 1 taken 2794 times.
3140 if (options == NULL)
898 346 options = &defaults;
899
2/2
✓ Branch 0 taken 23 times.
✓ Branch 1 taken 3117 times.
3140 if ((options->flags & LIBMPQ_FILE_FLAG_DELETE_MARKER) != 0 &&
900
4/6
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 10 times.
23 (!a->write_patch_mode || options->flags != LIBMPQ_FILE_FLAG_DELETE_MARKER || size != 0))
901 13 return LIBMPQ_ERROR_FORMAT;
902
2/2
✓ Branch 1 taken 994 times.
✓ Branch 2 taken 2133 times.
3127 if (libmpq__attributes_write_flags(a) != 0) {
903
2/2
✓ Branch 0 taken 405 times.
✓ Branch 1 taken 589 times.
994 uint32_t reserved = 1u + ((a->write_flags & LIBMPQ_ARCHIVE_CREATE_LISTFILE) != 0);
904 uint32_t hash1;
905 uint32_t hash2;
906 uint32_t hash3;
907 uint32_t internal1;
908 uint32_t internal2;
909 uint32_t internal3;
910
2/2
✓ Branch 0 taken 553 times.
✓ Branch 1 taken 441 times.
994 if (!a->write_internal &&
911
3/4
✓ Branch 0 taken 553 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✓ Branch 3 taken 493 times.
553 (reserved > a->write_capacity || a->write_next_block >= a->write_capacity - reserved))
912 90 return LIBMPQ_ERROR_SIZE;
913 934 libmpq__file_hash(name, &hash1, &hash2, &hash3);
914 934 libmpq__file_hash(LIBMPQ_ATTRIBUTES_NAME, &internal1, &internal2, &internal3);
915
6/8
✓ Branch 0 taken 493 times.
✓ Branch 1 taken 441 times.
✓ Branch 2 taken 30 times.
✓ Branch 3 taken 463 times.
✓ Branch 4 taken 30 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 30 times.
✗ Branch 7 not taken.
934 if (!a->write_internal && hash1 == internal1 && hash2 == internal2 && hash3 == internal3)
916 30 return LIBMPQ_ERROR_FORMAT;
917
2/2
✓ Branch 0 taken 350 times.
✓ Branch 1 taken 554 times.
904 if (a->write_attributes == NULL) {
918 if ((uint64_t)a->write_capacity * sizeof(*a->write_attributes) > SIZE_MAX)
919 return LIBMPQ_ERROR_SIZE;
920 350 a->write_attributes = calloc(a->write_capacity, sizeof(*a->write_attributes));
921
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 350 times.
350 if (a->write_attributes == NULL)
922 ✗ return LIBMPQ_ERROR_MALLOC;
923 }
924 }
925 3037 w = calloc(1, sizeof(*w));
926
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3037 times.
3037 if (w == NULL)
927 ✗ return LIBMPQ_ERROR_MALLOC;
928
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3037 times.
3037 if ((uint64_t)size > SIZE_MAX || a->write_next_block >= a->write_capacity) {
929 ✗ free(w);
930 ✗ return LIBMPQ_ERROR_SIZE;
931 }
932
2/2
✓ Branch 0 taken 117 times.
✓ Branch 1 taken 2920 times.
3037 if ((options->flags & LIBMPQ_FILE_FLAG_IMPLODE) &&
933
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 115 times.
117 (options->flags & LIBMPQ_FILE_FLAG_COMPRESS)) {
934 2 free(w);
935 2 return LIBMPQ_ERROR_FORMAT;
936 }
937
4/4
✓ Branch 0 taken 2091 times.
✓ Branch 1 taken 944 times.
✓ Branch 2 taken 2052 times.
✓ Branch 3 taken 39 times.
5126 if ((options->flags & LIBMPQ_FILE_FLAG_COMPRESS) &&
938 2091 (!libmpq__compression_allowed(
939 2091 a->mpq_header.version, options->compression_first, compression_policy(a)
940
2/2
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 2015 times.
2052 ) ||
941 2052 !libmpq__compression_allowed(
942 2052 a->mpq_header.version, options->compression_next, compression_policy(a)
943 ))) {
944 76 free(w);
945 76 return LIBMPQ_ERROR_FORMAT;
946 }
947
2/2
✓ Branch 0 taken 772 times.
✓ Branch 1 taken 2187 times.
2959 if ((options->flags & LIBMPQ_FILE_FLAG_SINGLE) &&
948
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 772 times.
772 ((options->compression_first | options->compression_next) &
949 (LIBMPQ_COMPRESSION_WAVE_MONO | LIBMPQ_COMPRESSION_WAVE_STEREO))) {
950 ✗ free(w);
951 ✗ return LIBMPQ_ERROR_FORMAT;
952 }
953
954 /* Existing writer entries already occupy their MPQ hash probe sequence. */
955 {
956 uint32_t h1;
957 uint32_t h2;
958 uint32_t h3;
959 uint32_t i;
960 2959 libmpq__file_hash(name, &h1, &h2, &h3);
961
1/2
✓ Branch 0 taken 3233 times.
✗ Branch 1 not taken.
3233 for (i = 0; i < a->write_hash_capacity; i++) {
962 3233 const mpq_hash_s *entry = &a->mpq_hash[(h1 + i) & (a->write_hash_capacity - 1)];
963
964
2/2
✓ Branch 0 taken 2956 times.
✓ Branch 1 taken 277 times.
3233 if (entry->block_table_index == LIBMPQ_HASH_FREE)
965 2956 break;
966
5/6
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 271 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 2 times.
277 if (entry->hash_a == h2 && entry->hash_b == h3 && entry->locale == options->locale &&
967
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 entry->platform == options->platform) {
968 uint32_t existing1;
969 uint32_t existing2;
970 uint32_t existing3;
971
972 /* Hash entries omit h1, so confirm it for matching identities. */
973 3 libmpq__file_hash(
974 3 a->write_names[entry->block_table_index], &existing1, &existing2, &existing3
975 );
976
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (existing1 == h1) {
977 3 free(w);
978 3 return LIBMPQ_ERROR_EXIST;
979 }
980 }
981 }
982 }
983
1/2
✓ Branch 0 taken 2956 times.
✗ Branch 1 not taken.
2956 w->data = malloc(a->write_sector_size ? a->write_sector_size : 1);
984 2956 w->name = libmpq__string_duplicate(name);
985
2/4
✓ Branch 0 taken 2956 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2956 times.
2956 if (w->data == NULL || w->name == NULL) {
986 ✗ free(w->data);
987 ✗ free(w->name);
988 ✗ free(w);
989 ✗ return LIBMPQ_ERROR_MALLOC;
990 }
991 2956 w->archive = a;
992 2956 w->expected = size;
993 2956 w->options = *options;
994 2956 w->prefix_size = prefix_size;
995 2956 w->patch_file = prefix_size != 0;
996 2956 w->patch_result_size = (uint32_t)result_size;
997
4/4
✓ Branch 0 taken 2868 times.
✓ Branch 1 taken 88 times.
✓ Branch 2 taken 2117 times.
✓ Branch 3 taken 751 times.
2956 if (size == 0 || (w->options.flags & LIBMPQ_FILE_FLAG_SINGLE) != 0 ||
998
2/2
✓ Branch 0 taken 386 times.
✓ Branch 1 taken 1731 times.
2117 (w->options.flags & (LIBMPQ_FILE_FLAG_COMPRESS | LIBMPQ_FILE_FLAG_IMPLODE)) == 0)
999 1225 w->options.flags &= ~LIBMPQ_FILE_FLAG_SECTOR_CRC;
1000 2956 w->options.compression_first &=
1001 ~(LIBMPQ_COMPRESSION_WAVE_MONO | LIBMPQ_COMPRESSION_WAVE_STEREO);
1002
1003 /* STANDARD v2 keeps the first WAVE sector lossless without standalone Huffman. */
1004
2/2
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2954 times.
5912 if (!libmpq__compression_allowed(
1005 2956 a->mpq_header.version, w->options.compression_first, compression_policy(a)
1006 ))
1007 2 w->options.compression_first = LIBMPQ_COMPRESSION_ZLIB;
1008
2/2
✓ Branch 0 taken 867 times.
✓ Branch 1 taken 2089 times.
2956 if (w->options.compression_next == 0)
1009 867 w->options.compression_next = w->options.compression_first;
1010
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2956 times.
2956 if (a->write_sector_size == 0) {
1011 ✗ free(w->name);
1012 ✗ free(w->data);
1013 ✗ free(w);
1014 ✗ return LIBMPQ_ERROR_FORMAT;
1015 }
1016 5912 w->block_count = (w->options.flags & LIBMPQ_FILE_FLAG_SINGLE)
1017 ? 1
1018
2/2
✓ Branch 0 taken 2184 times.
✓ Branch 1 taken 772 times.
2956 : (uint32_t)((size + a->write_sector_size - 1) / a->write_sector_size);
1019
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 2889 times.
2956 if (w->block_count == 0)
1020 67 w->block_count = 1;
1021 2956 w->payload_offset = (uint64_t)libmpq__file_tell(a->fp);
1022
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2956 times.
2956 if (w->payload_offset > (uint64_t)INT64_MAX - prefix_size) {
1023 ✗ free(w->data);
1024 ✗ free(w->name);
1025 ✗ free(w);
1026 ✗ return LIBMPQ_ERROR_SEEK;
1027 }
1028
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 2934 times.
2956 if (prefix_size != 0) {
1029 22 int32_t result = write_at(a->fp, w->payload_offset, prefix, prefix_size);
1030
1031
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (result != 0) {
1032 ✗ free(w->data);
1033 ✗ free(w->name);
1034 ✗ free(w);
1035 ✗ return result;
1036 }
1037 22 w->payload_offset += prefix_size;
1038 }
1039
1040 /* Reserve offset-table space before the first packed sector is written. */
1041
2/2
✓ Branch 0 taken 2184 times.
✓ Branch 1 taken 772 times.
2956 if (!(w->options.flags & LIBMPQ_FILE_FLAG_SINGLE) &&
1042
2/2
✓ Branch 0 taken 1739 times.
✓ Branch 1 taken 445 times.
2184 (w->options.flags & (LIBMPQ_FILE_FLAG_COMPRESS | LIBMPQ_FILE_FLAG_IMPLODE))) {
1043 1739 uint32_t crc = (w->options.flags & LIBMPQ_FILE_FLAG_SECTOR_CRC) != 0;
1044 1739 uint64_t entries = (uint64_t)w->block_count + 1 + crc;
1045
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 1697 times.
1739 uint64_t allocated = entries + (crc ? w->block_count : 0);
1046 size_t table_size;
1047
2/4
✓ Branch 0 taken 1739 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1739 times.
1739 if (entries > UINT32_MAX / 4 || allocated > SIZE_MAX / sizeof(*w->offsets)) {
1048 ✗ free(w->data);
1049 ✗ free(w->name);
1050 ✗ free(w);
1051 ✗ return LIBMPQ_ERROR_SIZE;
1052 }
1053 1739 table_size = (size_t)entries * 4;
1054 1739 w->offsets = calloc((size_t)allocated, sizeof(*w->offsets));
1055
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1739 times.
1739 if (w->offsets == NULL) {
1056 ✗ free(w->offsets);
1057 ✗ free(w->data);
1058 ✗ free(w->name);
1059 ✗ free(w);
1060 ✗ return LIBMPQ_ERROR_MALLOC;
1061 }
1062 1739 w->offsets[0] = (uint32_t)table_size;
1063
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 1697 times.
1739 if (crc)
1064 42 w->checksums = w->offsets + (size_t)entries;
1065
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 1739 times.
1739 if (libmpq__file_seek(a->fp, (w->payload_offset + table_size), SEEK_SET) < 0) {
1066 ✗ free(w->offsets);
1067 ✗ free(w->data);
1068 ✗ free(w->name);
1069 ✗ free(w);
1070 ✗ return LIBMPQ_ERROR_SEEK;
1071 }
1072 }
1073 2956 a->write_current = w;
1074 2956 w->attributes.flags = libmpq__attributes_write_flags(a);
1075
1076 /* Include the plaintext patch prefix before normal body writes extend CRC32. */
1077
3/4
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 2934 times.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
2956 if (prefix_size != 0 && (w->attributes.flags & LIBMPQ_ATTRIBUTE_CRC32) != 0)
1078 22 w->attributes.crc32 = (uint32_t)crc32(w->attributes.crc32, prefix, prefix_size);
1079
2/2
✓ Branch 0 taken 792 times.
✓ Branch 1 taken 2164 times.
2956 if (w->attributes.flags & LIBMPQ_ATTRIBUTE_MD5)
1080 792 libmpq__md5_init(&w->md5);
1081 2956 *out = w;
1082 2956 return 0;
1083 }
1084
1085 /*
1086 * Begin an ordinary streamed member with no plaintext prefix or patch flags.
1087 * This wrapper preserves the public file-writer path unchanged.
1088 */
1089 int32_t
1090 3125 libmpq__writer_file_begin(
1091 mpq_archive_s *archive, const char *name, libmpq__off_t size, const mpq_file_options_s *options,
1092 mpq_writer_s **out
1093 )
1094 {
1095 3125 return writer_file_begin(archive, name, size, options, NULL, 0, 0, out);
1096 }
1097
1098 /*
1099 * Append caller-provided bytes and flush complete sectors immediately.
1100 * The input is copied into a single sector buffer, so memory use is bounded
1101 * independently of the total file size.
1102 */
1103 int32_t
1104 2995 libmpq__writer_file_write(mpq_writer_s *w, const uint8_t *buffer, libmpq__off_t size)
1105 {
1106
9/10
✓ Branch 0 taken 2993 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 59 times.
✓ Branch 3 taken 2934 times.
✓ Branch 4 taken 58 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 2992 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✓ Branch 9 taken 2989 times.
2995 if (!w || (!buffer && size != 0) || size < 0 || size > w->expected - w->written)
1107 6 return LIBMPQ_ERROR_SIZE;
1108
1109 /* Fill the current sector, flushing exactly when it reaches capacity. */
1110
2/2
✓ Branch 0 taken 5879 times.
✓ Branch 1 taken 2988 times.
8867 while (size != 0) {
1111 5879 uint32_t room = w->archive->write_sector_size - w->data_size;
1112
2/2
✓ Branch 0 taken 2462 times.
✓ Branch 1 taken 3417 times.
5879 uint32_t take = (uint32_t)((size < room) ? size : room);
1113
2/2
✓ Branch 0 taken 1238 times.
✓ Branch 1 taken 4641 times.
5879 if (w->attributes.flags & LIBMPQ_ATTRIBUTE_CRC32)
1114 1238 w->attributes.crc32 = (uint32_t)crc32(w->attributes.crc32, buffer, take);
1115
2/2
✓ Branch 0 taken 1238 times.
✓ Branch 1 taken 4641 times.
5879 if (w->attributes.flags & LIBMPQ_ATTRIBUTE_MD5)
1116 1238 libmpq__md5_update(&w->md5, buffer, take);
1117 5879 memcpy(w->data + w->data_size, buffer, take);
1118 5879 w->data_size += take;
1119 5879 w->written += take;
1120 5879 buffer += take;
1121 5879 size -= take;
1122
2/2
✓ Branch 0 taken 3417 times.
✓ Branch 1 taken 2462 times.
5879 if (w->data_size == w->archive->write_sector_size) {
1123 3417 int32_t result = stream_flush_sector(w);
1124
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3416 times.
3417 if (result < 0)
1125 1 return result;
1126 }
1127 }
1128 2988 return 0;
1129 }
1130
1131 /*
1132 * Assign FILETIME without importing filesystem state or altering file bytes.
1133 * Only the currently active writer can accept this metadata.
1134 */
1135 int32_t
1136 33 libmpq__writer_file_timestamp(mpq_writer_s *writer, uint64_t filetime)
1137 {
1138
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 32 times.
33 if (writer == NULL)
1139 1 return LIBMPQ_ERROR_EXIST;
1140
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 32 times.
32 if (writer->archive->write_current != writer)
1141 ✗ return LIBMPQ_ERROR_NOT_INITIALIZED;
1142
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 16 times.
32 if (!(writer->attributes.flags & LIBMPQ_ATTRIBUTE_FILETIME))
1143 16 return LIBMPQ_ERROR_FORMAT;
1144 16 writer->attributes.filetime = filetime;
1145 16 return 0;
1146 }
1147
1148 /*
1149 * Flush the final sector and commit the file's block and hash-table entries.
1150 * Cleanup occurs on both success and failure so the archive never retains an
1151 * active writer after this function returns.
1152 */
1153 int32_t
1154 2955 libmpq__writer_file_finish(mpq_writer_s *w)
1155 {
1156 int32_t result;
1157
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2954 times.
2955 if (!w)
1158 1 return LIBMPQ_ERROR_EXIST;
1159
4/4
✓ Branch 0 taken 528 times.
✓ Branch 1 taken 2426 times.
✓ Branch 2 taken 88 times.
✓ Branch 3 taken 440 times.
2954 if (w->data_size != 0 || w->expected == 0) {
1160 2514 result = stream_flush_sector(w);
1161 } else {
1162 440 result = 0;
1163 }
1164
1/2
✓ Branch 0 taken 2954 times.
✗ Branch 1 not taken.
2954 if (result == 0)
1165 2954 result = stream_finish(w);
1166 2954 w->archive->write_current = NULL;
1167 2954 free(w->name);
1168 2954 free(w->data);
1169 2954 free(w->offsets);
1170 2954 free(w);
1171 2954 return result;
1172 }
1173
1174 /*
1175 * Discard an unfinished file after a write failure or archive close.
1176 * Any partially written payload remains unreachable because no block or hash
1177 * table entry is committed until writer_file_finish succeeds.
1178 */
1179 void
1180 619 libmpq__writer_file_abort(mpq_writer_s *writer)
1181 {
1182
2/2
✓ Branch 0 taken 617 times.
✓ Branch 1 taken 2 times.
619 if (writer == NULL)
1183 617 return;
1184
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (writer->archive->write_current == writer)
1185 2 writer->archive->write_current = NULL;
1186 2 free(writer->name);
1187 2 free(writer->data);
1188 2 free(writer->offsets);
1189 2 free(writer);
1190 }
1191
1192 /*
1193 * Add an in-memory file through the streaming writer interface.
1194 * This convenience wrapper uses the same sector pipeline as explicit begin,
1195 * write, and finish calls and cleans up an aborted stream.
1196 */
1197 int32_t
1198 2983 libmpq__writer_file_add(
1199 mpq_archive_s *a, const char *name, const uint8_t *data, libmpq__off_t size,
1200 const mpq_file_options_s *options
1201 )
1202 {
1203 mpq_writer_s *w;
1204 2983 int32_t result = libmpq__writer_file_begin(a, name, size, options, &w);
1205
2/2
✓ Branch 0 taken 185 times.
✓ Branch 1 taken 2798 times.
2983 if (result < 0)
1206 185 return result;
1207 2798 result = libmpq__writer_file_write(w, data, size);
1208
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2797 times.
2798 if (result < 0) {
1209 1 libmpq__writer_file_abort(w);
1210 1 return result;
1211 }
1212 2797 return libmpq__writer_file_finish(w);
1213 }
1214
1215 /*
1216 * Store one patch member with a plaintext patch prefix followed by a
1217 * normally encoded PTCH body. The writer state owns the composite block
1218 * offset, packed size, flags, and final reconstructed size from the outset.
1219 */
1220 int32_t
1221 22 libmpq__writer_patch_file_add(
1222 mpq_archive_s *archive, const char *name, const uint8_t *prefix, uint32_t prefix_size,
1223 const uint8_t *body, libmpq__off_t body_size, libmpq__off_t result_size,
1224 const mpq_file_options_s *options
1225 )
1226 {
1227 mpq_writer_s *writer;
1228 int32_t result;
1229
1230
5/10
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 22 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 22 times.
22 if (archive == NULL || !archive->write_patch_mode || name == NULL || prefix == NULL ||
1231 ✗ (body == NULL && body_size != 0))
1232 ✗ return LIBMPQ_ERROR_EXIST;
1233
3/6
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22 times.
✗ Branch 5 not taken.
22 if (body_size < 0 || result_size < 0 || (uint64_t)body_size > UINT32_MAX ||
1234
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 (uint64_t)result_size > UINT32_MAX)
1235 ✗ return LIBMPQ_ERROR_SIZE;
1236
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 3 taken 22 times.
✗ Branch 4 not taken.
22 if (prefix_size < LIBMPQ_PATCH_INFO_SIZE || libmpq__load_le32(prefix) != prefix_size ||
1237
1/2
✓ Branch 1 taken 22 times.
✗ Branch 2 not taken.
22 libmpq__load_le32(prefix + 8) != (uint32_t)body_size ||
1238
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
22 (libmpq__load_le32(prefix + 4) & 0x80000000u) == 0)
1239 ✗ return LIBMPQ_ERROR_FORMAT;
1240 22 result = writer_file_begin(
1241 archive, name, body_size, options, prefix, prefix_size, result_size, &writer
1242 );
1243
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (result != 0)
1244 ✗ return result;
1245 22 result = libmpq__writer_file_write(writer, body, body_size);
1246
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (result != 0) {
1247 ✗ libmpq__writer_file_abort(writer);
1248 ✗ return result;
1249 }
1250 22 return libmpq__writer_file_finish(writer);
1251 }
1252
1253 /* Supply marker storage flags at begin, before its block and hash entries exist. */
1254 int32_t
1255 10 libmpq__writer_patch_delete_marker(
1256 mpq_archive_s *archive, const char *name, uint16_t locale, uint16_t platform
1257 )
1258 {
1259 10 mpq_file_options_s options = { LIBMPQ_FILE_FLAG_DELETE_MARKER, 0, 0, locale, platform };
1260
1261
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 10 times.
10 if (archive == NULL || !archive->write_patch_mode)
1262 ✗ return LIBMPQ_ERROR_FORMAT;
1263 10 return libmpq__writer_file_add(archive, name, NULL, 0, &options);
1264 }
1265
1266 /*
1267 * Add a filesystem file while keeping only one input sector in memory.
1268 * The source length is determined first so the streaming writer can enforce
1269 * its declared size while reading bounded chunks from disk.
1270 */
1271 int32_t
1272 5 libmpq__writer_file_add_path(
1273 mpq_archive_s *a, const char *name, const char *source, const mpq_file_options_s *options
1274 )
1275 {
1276 FILE *fp;
1277 libmpq__off_t size;
1278 uint8_t buffer[4096];
1279 size_t got;
1280 int32_t result;
1281 mpq_writer_s *writer;
1282 5 fp = libmpq__file_open(source, "rb");
1283
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (!fp)
1284 1 return LIBMPQ_ERROR_OPEN;
1285
3/6
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 4 times.
8 if (libmpq__file_seek(fp, 0, SEEK_END) < 0 || (size = libmpq__file_tell(fp)) < 0 ||
1286 4 libmpq__file_seek(fp, 0, SEEK_SET) < 0) {
1287 ✗ fclose(fp);
1288 ✗ return LIBMPQ_ERROR_SEEK;
1289 }
1290 4 result = libmpq__writer_file_begin(a, name, size, options, &writer);
1291
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (result < 0) {
1292 ✗ fclose(fp);
1293 ✗ return result;
1294 }
1295
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)) {
1296 4 got = fread(buffer, 1, sizeof(buffer), fp);
1297
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (got == 0)
1298 ✗ break;
1299 4 result = libmpq__writer_file_write(writer, buffer, (libmpq__off_t)got);
1300
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (result < 0)
1301 ✗ break;
1302 }
1303
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
4 if (ferror(fp))
1304 ✗ result = LIBMPQ_ERROR_READ;
1305 4 fclose(fp);
1306
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (result == 0)
1307 4 result = libmpq__writer_file_finish(writer);
1308 else
1309 ✗ libmpq__writer_file_abort(writer);
1310 4 return result;
1311 }
1312
1313 /*
1314 * Finalize a writer archive and make it readable by libmpq.
1315 * The public close path uses this internal wrapper to keep serialization in
1316 * the writer module while preserving the archive handle lifecycle.
1317 */
1318 int32_t
1319 618 libmpq__writer_finalize(mpq_archive_s *a)
1320 {
1321
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 618 times.
618 if (a == NULL)
1322 ✗ return LIBMPQ_ERROR_EXIST;
1323
3/4
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 533 times.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
618 if (a->write_mpqe && a->write_mpqe_ops != NULL)
1324 85 return a->write_mpqe_ops->finalize(a);
1325 533 return finalize_archive(a);
1326 }
1327