GCC Code Coverage Report


Directory: src/
File: src/mpq-patch-writer.c
Date: 2026-10-06 13:47:41
Exec Total Coverage
Lines: 399 462 86.4%
Functions: 21 21 100.0%
Branches: 187 320 58.4%

Line Branch Exec Source
1 /*
2 * mpq-patch-writer.c -- private MPQ whole-file patch 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
20 #include "mpq-patch-writer.h"
21 #include "mpq-archive.h"
22 #include "mpq-crypto.h"
23 #include "mpq-endian.h"
24 #include "mpq-file.h"
25 #include "mpq-md5.h"
26 #include "mpq-patch-reader.h"
27 #include "mpq-reader.h"
28 #include "mpq-writer.h"
29
30 #include <limits.h>
31 #include <stdlib.h>
32 #include <string.h>
33
34 #define LIBMPQ_PATCH_COPY_HEADER_SIZE 68u
35 #define LIBMPQ_PATCH_WRITER_CAPACITY 1024u
36
37 #define LIBMPQ_BSD0_HEADER_SIZE 32u
38 #define LIBMPQ_BSD0_CONTROL_SIZE 12u
39
40 /* Encode zero runs and literal runs using the reader's bounded RLE format. */
41 static size_t
42 24 patch_bsd0_rle(const uint8_t *raw, size_t size, uint8_t *output)
43 {
44 24 size_t input = 0;
45 24 size_t used = 4;
46
47 24 memset(output, 0, 4);
48
2/2
✓ Branch 0 taken 1848 times.
✓ Branch 1 taken 24 times.
1872 while (input < size) {
49 1848 size_t count = 0;
50 1848 uint8_t zero = raw[input] == 0;
51
52
6/6
✓ Branch 0 taken 114172 times.
✓ Branch 1 taken 24 times.
✓ Branch 2 taken 113429 times.
✓ Branch 3 taken 743 times.
✓ Branch 4 taken 112348 times.
✓ Branch 5 taken 1081 times.
114196 while (count < size - input && count < 128 && (raw[input + count] == 0) == zero)
53 112348 count++;
54
2/2
✓ Branch 0 taken 630 times.
✓ Branch 1 taken 1218 times.
1848 output[used++] = (uint8_t)(count - 1) | (zero ? 0 : 0x80u);
55
2/2
✓ Branch 0 taken 1218 times.
✓ Branch 1 taken 630 times.
1848 if (!zero) {
56 1218 memcpy(output + used, raw + input, count);
57 1218 used += count;
58 }
59 1848 input += count;
60 }
61 24 return used;
62 }
63
64 /* Successful BSD0 output always uses the reader's RLE wrapper. */
65 static int32_t
66 24 patch_bsd0_pack(uint8_t *raw, size_t raw_size, uint8_t **encoded, size_t *encoded_size)
67 {
68 size_t rle_capacity;
69 size_t rle_size;
70 uint8_t *rle;
71
72
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (raw_size > (SIZE_MAX - 4) / 2) {
73 ✗ free(raw);
74 ✗ return LIBMPQ_ERROR_SIZE;
75 }
76 24 rle_capacity = raw_size * 2 + 4;
77 24 rle = malloc(rle_capacity);
78
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (rle == NULL) {
79 ✗ free(raw);
80 ✗ return LIBMPQ_ERROR_MALLOC;
81 }
82 24 rle_size = patch_bsd0_rle(raw, raw_size, rle);
83 24 free(raw);
84 24 *encoded = rle;
85 24 *encoded_size = rle_size;
86 24 return 0;
87 }
88
89 /* Build the aligned candidate, where unchanged bytes become zero differences. */
90 static uint8_t *
91 22 patch_bsd0_aligned(
92 const uint8_t *before, size_t before_size, const uint8_t *after, size_t after_size,
93 size_t *raw_size
94 )
95 {
96 22 size_t overlap = before_size < after_size ? before_size : after_size;
97 uint8_t *raw;
98
99 22 *raw_size = LIBMPQ_BSD0_HEADER_SIZE + LIBMPQ_BSD0_CONTROL_SIZE + after_size;
100 22 raw = calloc(1, *raw_size);
101
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (raw == NULL)
102 ✗ return NULL;
103 22 memcpy(raw, "BSDIFF40", 8);
104 22 libmpq__store_le64(raw + 8, LIBMPQ_BSD0_CONTROL_SIZE);
105 22 libmpq__store_le64(raw + 16, overlap);
106 22 libmpq__store_le64(raw + 24, after_size);
107 22 libmpq__store_le32(raw + 32, (uint32_t)overlap);
108 22 libmpq__store_le32(raw + 36, (uint32_t)(after_size - overlap));
109
2/2
✓ Branch 0 taken 24803 times.
✓ Branch 1 taken 22 times.
24825 for (size_t i = 0; i < overlap; i++)
110 24803 raw[LIBMPQ_BSD0_HEADER_SIZE + LIBMPQ_BSD0_CONTROL_SIZE + i] =
111 24803 (uint8_t)(after[i] - before[i]);
112
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 3 times.
22 if (after_size != overlap)
113 19 memcpy(
114 19 raw + LIBMPQ_BSD0_HEADER_SIZE + LIBMPQ_BSD0_CONTROL_SIZE + overlap, after + overlap,
115 after_size - overlap
116 );
117 22 return raw;
118 }
119
120 /*
121 * A BSD0 tuple's add count combines base bytes with diff bytes. Its copy count
122 * consumes literal extra-block bytes, not bytes from the base. The first
123 * tuple copies the unchanged prefix literally and seeks to the old middle.
124 * The second diffs overlapping middle bytes, copies inserted bytes from the
125 * extra block, then skips any removed old-middle bytes. The final tuple
126 * copies the unchanged suffix literally. Empty ranges omit their tuple.
127 * This matches libmpq's BSD0 decoder, not a generic bsdiff copy operation.
128 */
129 static uint8_t *
130 2 patch_bsd0_splice(
131 const uint8_t *before, const uint8_t *after, size_t after_size, size_t prefix, size_t suffix,
132 size_t old_middle, size_t *raw_size
133 )
134 {
135 2 size_t new_middle = after_size - prefix - suffix;
136 2 size_t overlap = old_middle < new_middle ? old_middle : new_middle;
137 2 size_t inserted = new_middle - overlap;
138 2 size_t control_count = (prefix != 0) + (overlap + inserted != 0) + (suffix != 0);
139 2 size_t control_size = control_count * LIBMPQ_BSD0_CONTROL_SIZE;
140 2 size_t delta_offset = LIBMPQ_BSD0_HEADER_SIZE + control_size;
141 2 size_t extra_offset = delta_offset + overlap;
142 2 size_t control_offset = LIBMPQ_BSD0_HEADER_SIZE;
143 uint8_t *raw;
144
145 2 *raw_size = LIBMPQ_BSD0_HEADER_SIZE + control_size + after_size;
146 2 raw = calloc(1, *raw_size);
147
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (raw == NULL)
148 ✗ return NULL;
149 2 memcpy(raw, "BSDIFF40", 8);
150 2 libmpq__store_le64(raw + 8, control_size);
151 2 libmpq__store_le64(raw + 16, overlap);
152 2 libmpq__store_le64(raw + 24, after_size);
153
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (prefix != 0) {
154 2 libmpq__store_le32(raw + control_offset + 4, (uint32_t)prefix);
155 2 libmpq__store_le32(raw + control_offset + 8, (uint32_t)prefix);
156 2 control_offset += LIBMPQ_BSD0_CONTROL_SIZE;
157 2 memcpy(raw + extra_offset, after, prefix);
158 }
159
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (overlap + inserted != 0) {
160 2 libmpq__store_le32(raw + control_offset, (uint32_t)overlap);
161 2 libmpq__store_le32(raw + control_offset + 4, (uint32_t)inserted);
162 2 libmpq__store_le32(raw + control_offset + 8, (uint32_t)(old_middle - overlap));
163 2 control_offset += LIBMPQ_BSD0_CONTROL_SIZE;
164 }
165
2/2
✓ Branch 0 taken 96 times.
✓ Branch 1 taken 2 times.
98 for (size_t i = 0; i < overlap; i++)
166 96 raw[delta_offset + i] = (uint8_t)(after[prefix + i] - before[prefix + i]);
167
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (inserted != 0)
168 2 memcpy(raw + extra_offset + prefix, after + prefix + overlap, inserted);
169
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (suffix != 0) {
170 2 libmpq__store_le32(raw + control_offset + 4, (uint32_t)suffix);
171 2 memcpy(raw + extra_offset + prefix + inserted, after + prefix + new_middle, suffix);
172 }
173 2 return raw;
174 }
175
176 /*
177 * PTCH BSD0 uses libmpq's existing RLE-wrapped BSDIFF40-compatible layout.
178 * The splice tuples above retain that decoder contract; the output is not a
179 * generic bsdiff patch file.
180 */
181 int32_t
182 45 libmpq__patch_bsd0_encode(
183 const uint8_t *before, size_t before_size, const uint8_t *after, size_t after_size,
184 uint8_t splice, uint8_t **encoded, size_t *encoded_size, size_t *patch_stream_size
185 )
186 {
187 45 size_t prefix = 0;
188 45 size_t suffix = 0;
189 size_t raw_size;
190 uint8_t *raw;
191
192
3/6
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 45 times.
45 if (encoded == NULL || encoded_size == NULL || patch_stream_size == NULL)
193 ✗ return LIBMPQ_ERROR_EXIST;
194 45 *encoded = NULL;
195 45 *encoded_size = 0;
196 45 *patch_stream_size = 0;
197
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 45 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 45 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
45 if ((before == NULL && before_size != 0) || (after == NULL && after_size != 0))
198 ✗ return LIBMPQ_ERROR_EXIST;
199
3/6
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 45 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 45 times.
✗ Branch 5 not taken.
45 if (before_size > UINT32_MAX || after_size > UINT32_MAX ||
200
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 45 times.
45 after_size > SIZE_MAX - LIBMPQ_BSD0_HEADER_SIZE - 3 * LIBMPQ_BSD0_CONTROL_SIZE ||
201 after_size == 0)
202 ✗ return LIBMPQ_ERROR_SIZE;
203
2/2
✓ Branch 0 taken 22 times.
✓ Branch 1 taken 23 times.
45 if (splice == 0) {
204 22 raw = patch_bsd0_aligned(before, before_size, after, after_size, &raw_size);
205 } else {
206
4/6
✓ Branch 0 taken 5160 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5160 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5137 times.
✓ Branch 5 taken 23 times.
5160 while (prefix < before_size && prefix < after_size && before[prefix] == after[prefix])
207 5137 prefix++;
208
3/4
✓ Branch 0 taken 11191 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 11191 times.
✗ Branch 3 not taken.
11192 while (suffix < before_size - prefix && suffix < after_size - prefix &&
209
2/2
✓ Branch 0 taken 11169 times.
✓ Branch 1 taken 22 times.
11191 before[before_size - suffix - 1] == after[after_size - suffix - 1])
210 11169 suffix++;
211
3/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 21 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
23 if (suffix < 16 || (prefix == 0 && prefix + suffix == after_size) ||
212
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 before_size - prefix - suffix > INT32_MAX)
213 21 return 0;
214 2 raw = patch_bsd0_splice(
215 2 before, after, after_size, prefix, suffix, before_size - prefix - suffix, &raw_size
216 );
217 }
218
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (raw == NULL)
219 ✗ return LIBMPQ_ERROR_MALLOC;
220 24 *patch_stream_size = raw_size;
221 24 return patch_bsd0_pack(raw, raw_size, encoded, encoded_size);
222 }
223
224 struct mpq_patch
225 {
226 mpq_archive_s *base;
227 mpq_archive_s *archive;
228 mpq_directory_s *directory;
229 char *destination;
230 char *temporary;
231 };
232
233 /* The patch archive itself owns no references to the source archive. */
234 static int32_t
235 27 patch_writer_cleanup(mpq_patch_writer_s *state, uint8_t publish)
236 {
237 27 int32_t result = 0;
238 int32_t cleanup;
239
240
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 2 times.
27 if (state->archive != NULL) {
241 25 result = libmpq__archive_close(state->archive);
242 25 state->archive = NULL;
243 }
244
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 if (state->base != NULL) {
245 27 cleanup = libmpq__archive_close(state->base);
246
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 if (result == 0)
247 27 result = cleanup;
248 }
249
3/4
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 6 times.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
27 if (publish && result == 0) {
250 21 result = libmpq__directory_replace(state->directory, state->temporary, state->destination);
251 }
252
3/4
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 6 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 21 times.
27 if (!publish || result != 0) {
253 6 cleanup = libmpq__directory_remove(state->directory, state->temporary);
254
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (result == 0)
255 6 result = cleanup;
256 }
257
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 if (state->directory != NULL)
258 27 libmpq__directory_close(state->directory);
259 27 free(state->destination);
260 27 free(state->temporary);
261 27 free(state);
262 27 return result;
263 }
264
265 /* Resolve a unique named base identity, retaining its locale and platform. */
266 static int32_t
267 35 patch_writer_member(
268 mpq_patch_writer_s *state, const char *name, uint32_t *number, uint16_t *locale,
269 uint16_t *platform
270 )
271 {
272 uint32_t hash1;
273 uint32_t hash_a;
274 uint32_t hash_b;
275 uint32_t block;
276 35 uint32_t found = UINT32_MAX;
277 int32_t result;
278
279
2/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 35 times.
35 if (name == NULL || name[0] == '\0')
280 ✗ return LIBMPQ_ERROR_EXIST;
281 35 result = libmpq__file_number(state->base, name, number);
282
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 34 times.
35 if (result != 0)
283 1 return result;
284 34 block = state->base->mpq_map[*number].block_table_indices;
285 34 libmpq__file_hash(name, &hash1, &hash_a, &hash_b);
286
2/2
✓ Branch 0 taken 1120 times.
✓ Branch 1 taken 34 times.
1154 for (uint32_t i = 0; i < state->base->mpq_header.hash_table_count; i++) {
287 1120 const mpq_hash_s *entry = &state->base->mpq_hash[i];
288
289
3/4
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 1086 times.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
1120 if (entry->block_table_index == block && entry->hash_a == hash_a &&
290
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 entry->hash_b == hash_b) {
291
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
34 if (found != UINT32_MAX)
292 ✗ return LIBMPQ_ERROR_FORMAT;
293 34 found = i;
294 }
295 }
296
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 34 times.
34 if (found == UINT32_MAX)
297 ✗ return LIBMPQ_ERROR_FORMAT;
298 34 *locale = state->base->mpq_hash[found].locale;
299 34 *platform = state->base->mpq_hash[found].platform;
300 34 return 0;
301 }
302
303 /* Read a named base member with its filename-derived decryption key. */
304 static int32_t
305 22 patch_writer_read_base(
306 mpq_patch_writer_s *state, const char *name, uint32_t number, uint8_t **data, size_t *size
307 )
308 {
309 22 libmpq__off_t length = 0;
310 22 libmpq__off_t transferred = 0;
311 22 uint8_t *bytes = NULL;
312 int32_t result;
313
314 22 *data = NULL;
315 22 *size = 0;
316 22 result = libmpq__file_size_unpacked(state->base, number, &length);
317
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (result != 0)
318 ✗ return result;
319
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 22 times.
22 if (length < 0 || (uint64_t)length > UINT32_MAX || (uint64_t)length > SIZE_MAX)
320 ✗ return LIBMPQ_ERROR_SIZE;
321
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 bytes = malloc(length == 0 ? 1 : (size_t)length);
322
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (bytes == NULL)
323 ✗ return LIBMPQ_ERROR_MALLOC;
324 22 result = libmpq__reader_offsets_acquire(state->base, number, name);
325
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (result == 0) {
326 22 result = libmpq__file_read(state->base, number, bytes, length, &transferred);
327
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 22 times.
22 if (result == 0 && transferred != length)
328 ✗ result = LIBMPQ_ERROR_READ;
329 {
330 22 int32_t release = libmpq__reader_offsets_release(state->base, number);
331
332
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (result == 0)
333 22 result = release;
334 }
335 }
336
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (result == 0) {
337 22 *data = bytes;
338 22 *size = (size_t)length;
339 22 bytes = NULL;
340 }
341 22 free(bytes);
342 22 return result;
343 }
344
345 /* Build one complete PTCH candidate without changing its transform afterward. */
346 static int32_t
347 22 patch_writer_build_payload(
348 size_t before_size, size_t after_size, const uint8_t *transform, size_t transform_size,
349 size_t patch_stream_size, uint8_t use_bsd0, const uint8_t before_md5[LIBMPQ_MD5_SIZE],
350 const uint8_t after_md5[LIBMPQ_MD5_SIZE], uint8_t **payload, size_t *payload_size
351 )
352 {
353 static const uint8_t ptch_tag[4] = { 'P', 'T', 'C', 'H' };
354 static const uint8_t md5_tag[4] = { 'M', 'D', '5', '_' };
355 static const uint8_t xfrm_tag[4] = { 'X', 'F', 'R', 'M' };
356 static const uint8_t copy_tag[4] = { 'C', 'O', 'P', 'Y' };
357 static const uint8_t bsd0_tag[4] = { 'B', 'S', 'D', '0' };
358 size_t encoded_size;
359 uint8_t *raw;
360 uint8_t *patch;
361 mpq_md5_s md5;
362
363 22 *payload = NULL;
364 22 *payload_size = 0;
365
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
22 if (transform_size > UINT32_MAX - LIBMPQ_PATCH_COPY_HEADER_SIZE ||
366
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 patch_stream_size > UINT32_MAX - LIBMPQ_PATCH_COPY_HEADER_SIZE ||
367 transform_size > SIZE_MAX - LIBMPQ_PATCH_INFO_SIZE - LIBMPQ_PATCH_COPY_HEADER_SIZE)
368 ✗ return LIBMPQ_ERROR_SIZE;
369 22 encoded_size = LIBMPQ_PATCH_COPY_HEADER_SIZE + transform_size;
370 22 raw = calloc(1, LIBMPQ_PATCH_INFO_SIZE + encoded_size);
371
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 if (raw == NULL)
372 ✗ return LIBMPQ_ERROR_MALLOC;
373 22 patch = raw + LIBMPQ_PATCH_INFO_SIZE;
374 22 libmpq__store_le32(raw, LIBMPQ_PATCH_INFO_SIZE);
375 22 libmpq__store_le32(raw + 4, 0x80000000u);
376 22 libmpq__store_le32(raw + 8, (uint32_t)encoded_size);
377 22 memcpy(patch, ptch_tag, sizeof(ptch_tag));
378 22 libmpq__store_le32(patch + 4, (uint32_t)(LIBMPQ_PATCH_COPY_HEADER_SIZE + patch_stream_size));
379 22 libmpq__store_le32(patch + 8, (uint32_t)before_size);
380 22 libmpq__store_le32(patch + 12, (uint32_t)after_size);
381 22 memcpy(patch + 16, md5_tag, sizeof(md5_tag));
382 22 libmpq__store_le32(patch + 20, 40);
383 22 memcpy(patch + 24, before_md5, LIBMPQ_MD5_SIZE);
384 22 memcpy(patch + 40, after_md5, LIBMPQ_MD5_SIZE);
385 22 memcpy(patch + 56, xfrm_tag, sizeof(xfrm_tag));
386 22 libmpq__store_le32(patch + 60, (uint32_t)transform_size + 12);
387
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 20 times.
22 memcpy(patch + 64, use_bsd0 ? bsd0_tag : copy_tag, sizeof(copy_tag));
388
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (transform_size != 0)
389 22 memcpy(patch + LIBMPQ_PATCH_COPY_HEADER_SIZE, transform, transform_size);
390 22 libmpq__md5_init(&md5);
391 22 libmpq__md5_update(&md5, patch, encoded_size);
392 22 libmpq__md5_final(&md5, raw + 12);
393
394 22 *payload = raw;
395 22 *payload_size = LIBMPQ_PATCH_INFO_SIZE + encoded_size;
396 22 return 0;
397 }
398
399 /* Select BSD0 only when its complete candidate is smaller and decodes correctly. */
400 static int32_t
401 22 patch_writer_payload(
402 const uint8_t *before, size_t before_size, const uint8_t *after, size_t after_size,
403 uint8_t after_md5[LIBMPQ_MD5_SIZE], uint8_t **payload, size_t *payload_size
404 )
405 {
406 uint8_t before_md5[LIBMPQ_MD5_SIZE];
407 mpq_md5_s md5;
408 22 size_t transform_size = 0;
409 22 size_t patch_stream_size = 0;
410 22 size_t verified_size = 0;
411 22 uint8_t *transform = NULL;
412 22 uint8_t *candidate = NULL;
413 22 uint8_t *best = NULL;
414 22 uint8_t *verified = NULL;
415 22 size_t candidate_size = 0;
416 size_t best_size;
417 int32_t result;
418
419 22 *payload = NULL;
420 22 *payload_size = 0;
421
2/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✗ Branch 3 not taken.
22 if (before_size > UINT32_MAX ||
422
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 22 times.
22 after_size > UINT32_MAX - LIBMPQ_PATCH_INFO_SIZE - LIBMPQ_PATCH_COPY_HEADER_SIZE ||
423 after_size > SIZE_MAX - LIBMPQ_PATCH_INFO_SIZE - LIBMPQ_PATCH_COPY_HEADER_SIZE)
424 ✗ return LIBMPQ_ERROR_SIZE;
425 22 best_size = LIBMPQ_PATCH_INFO_SIZE + LIBMPQ_PATCH_COPY_HEADER_SIZE + after_size;
426 22 libmpq__md5_init(&md5);
427 22 libmpq__md5_update(&md5, before, before_size);
428 22 libmpq__md5_final(&md5, before_md5);
429 22 libmpq__md5_init(&md5);
430 22 libmpq__md5_update(&md5, after, after_size);
431 22 libmpq__md5_final(&md5, after_md5);
432
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (after_size != 0) {
433
2/2
✓ Branch 0 taken 44 times.
✓ Branch 1 taken 22 times.
66 for (uint8_t splice = 0; splice < 2; splice++) {
434 44 result = libmpq__patch_bsd0_encode(
435 before, before_size, after, after_size, splice, &transform, &transform_size,
436 &patch_stream_size
437 );
438
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 44 times.
44 if (result != 0) {
439 ✗ free(best);
440 ✗ return result;
441 }
442
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 23 times.
44 if (transform == NULL)
443 21 continue;
444
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 if (patch_stream_size > UINT32_MAX - LIBMPQ_PATCH_COPY_HEADER_SIZE ||
445
1/2
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
23 transform_size > UINT32_MAX - LIBMPQ_PATCH_COPY_HEADER_SIZE ||
446
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 23 times.
23 transform_size >
447 SIZE_MAX - LIBMPQ_PATCH_INFO_SIZE - LIBMPQ_PATCH_COPY_HEADER_SIZE) {
448 ✗ free(transform);
449 ✗ transform = NULL;
450 ✗ continue;
451 }
452
453 /* Equal or larger candidates cannot win; only contenders need validation. */
454
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 2 times.
23 if (LIBMPQ_PATCH_INFO_SIZE + LIBMPQ_PATCH_COPY_HEADER_SIZE + transform_size >=
455 best_size) {
456 21 free(transform);
457 21 transform = NULL;
458 21 continue;
459 }
460 2 result = patch_writer_build_payload(
461 before_size, after_size, transform, transform_size, patch_stream_size, 1,
462 before_md5, after_md5, &candidate, &candidate_size
463 );
464 2 free(transform);
465 2 transform = NULL;
466
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (result != 0) {
467 ✗ free(best);
468 ✗ return result;
469 }
470 2 result = libmpq__patch_apply(
471 2 before, before_size, candidate + LIBMPQ_PATCH_INFO_SIZE,
472 candidate_size - LIBMPQ_PATCH_INFO_SIZE, &verified, &verified_size
473 );
474
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 if (result == 0 && verified_size == after_size && verified != NULL &&
475
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 memcmp(verified, after, after_size) == 0 && candidate_size < best_size) {
476 2 free(best);
477 2 best = candidate;
478 2 best_size = candidate_size;
479 2 candidate = NULL;
480 }
481 2 free(verified);
482 2 verified = NULL;
483 2 free(candidate);
484 2 candidate = NULL;
485 }
486 }
487
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 20 times.
22 if (best != NULL) {
488 2 *payload = best;
489 2 *payload_size = best_size;
490 2 return 0;
491 }
492 20 return patch_writer_build_payload(
493 before_size, after_size, after, after_size, after_size, 0, before_md5, after_md5, payload,
494 payload_size
495 );
496 }
497
498 /* Refuse to publish a patch over its own base archive. */
499 static int32_t
500 27 patch_writer_check_destination(mpq_patch_writer_s *state)
501 {
502 27 FILE *existing = libmpq__directory_file_open(state->directory, state->destination, "rb");
503 uint64_t device;
504 uint64_t inode;
505 27 int32_t result = 0;
506
507
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 if (existing == NULL)
508 27 return 0;
509 ✗ if (state->base->file_identity_valid) {
510 ✗ result = libmpq__file_identity(existing, &device, &inode);
511 ✗ if (result == 0 && device == state->base->file_device && inode == state->base->file_inode)
512 ✗ result = LIBMPQ_ERROR_FORMAT;
513 }
514 ✗ if (fclose(existing) != 0 && result == 0)
515 ✗ result = LIBMPQ_ERROR_CLOSE;
516 ✗ return result;
517 }
518
519 /* Resolve the sibling temporary path for the existing MPQE archive writer. */
520 static char *
521 6 patch_writer_staged_path(const char *absolute, const char *temporary)
522 {
523 6 const char *slash = strrchr(absolute, '/');
524 size_t directory_size;
525 6 size_t name_size = strlen(temporary);
526 char *path;
527
528 #ifdef _WIN32
529 {
530 const char *backslash = strrchr(absolute, '\\');
531
532 if (backslash != NULL && (slash == NULL || backslash > slash))
533 slash = backslash;
534 }
535 #endif
536
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (slash == NULL)
537 ✗ return NULL;
538 6 directory_size = (size_t)(slash - absolute) + 1;
539
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (directory_size > SIZE_MAX - name_size - 1)
540 ✗ return NULL;
541 6 path = malloc(directory_size + name_size + 1);
542
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (path != NULL) {
543 6 memcpy(path, absolute, directory_size);
544 6 memcpy(path + directory_size, temporary, name_size + 1);
545 }
546 6 return path;
547 }
548
549 /* Stage a same-directory patch; MPQE output delegates to the existing writer. */
550 static int32_t
551 31 patch_writer_begin_internal(
552 mpq_patch_writer_s **patch_writer, const char *base_path, const char *patch_path,
553 const uint8_t *auth_code, size_t auth_code_size, uint8_t mpqe
554 )
555 {
556 31 mpq_patch_writer_s *state = NULL;
557 mpq_archive_create_options_s options;
558 31 FILE *temporary_file = NULL;
559 31 char *absolute = NULL;
560 31 char *staged_path = NULL;
561 int32_t result;
562
563
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 29 times.
31 if (patch_writer == NULL)
564 2 return LIBMPQ_ERROR_EXIST;
565 29 *patch_writer = NULL;
566
2/4
✓ Branch 0 taken 29 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 29 times.
29 if (base_path == NULL || patch_path == NULL)
567 ✗ return LIBMPQ_ERROR_EXIST;
568 29 state = calloc(1, sizeof(*state));
569
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 29 times.
29 if (state == NULL)
570 ✗ return LIBMPQ_ERROR_MALLOC;
571 29 result = libmpq__archive_open(&state->base, base_path, 0);
572
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 27 times.
29 if (result != 0)
573 2 goto fail;
574
1/2
✓ Branch 0 taken 27 times.
✗ Branch 1 not taken.
27 if (state->base->archive_offset != 0 ||
575
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 state->base->mpq_header.version > LIBMPQ_ARCHIVE_VERSION_TWO) {
576 ✗ result = LIBMPQ_ERROR_FORMAT;
577 ✗ goto fail;
578 }
579 27 absolute = libmpq__file_absolute_path(patch_path);
580
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (absolute == NULL) {
581 ✗ result = LIBMPQ_ERROR_MALLOC;
582 ✗ goto fail;
583 }
584 27 result = libmpq__directory_open(absolute, &state->directory, &state->destination);
585
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (result != 0)
586 ✗ goto fail;
587 27 result = patch_writer_check_destination(state);
588
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (result != 0)
589 ✗ goto fail;
590 27 result = libmpq__directory_temporary(
591 state->directory, ".libmpq-patch-write-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 1,
592 &state->temporary, &temporary_file
593 );
594
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 27 times.
27 if (result != 0)
595 ✗ goto fail;
596 27 options.version = state->base->mpq_header.version;
597 27 options.max_files = LIBMPQ_PATCH_WRITER_CAPACITY;
598 27 options.sector_size = state->base->block_size;
599 27 options.flags = LIBMPQ_ARCHIVE_CREATE_LISTFILE;
600 27 options.attributes = LIBMPQ_ATTRIBUTE_CRC32 | LIBMPQ_ATTRIBUTE_MD5 | LIBMPQ_ATTRIBUTE_PATCH_BIT;
601
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 21 times.
27 if (mpqe) {
602
1/2
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
6 if (fclose(temporary_file) != 0) {
603 ✗ temporary_file = NULL;
604 ✗ result = LIBMPQ_ERROR_CLOSE;
605 ✗ goto fail;
606 }
607 6 temporary_file = NULL;
608 6 staged_path = patch_writer_staged_path(absolute, state->temporary);
609
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6 times.
6 if (staged_path == NULL) {
610 ✗ result = LIBMPQ_ERROR_MALLOC;
611 ✗ goto fail;
612 }
613 6 result = libmpq__writer_archive_create_mpqe(
614 &state->archive, staged_path, auth_code, auth_code_size, &options
615 );
616 } else {
617 21 result = libmpq__writer_archive_create_file(
618 21 &state->archive, state->temporary, temporary_file, &options
619 );
620 21 temporary_file = NULL;
621 }
622
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 25 times.
27 if (result != 0)
623 2 goto fail;
624 25 state->archive->write_patch_mode = 1;
625 25 free(staged_path);
626 25 free(absolute);
627 25 *patch_writer = state;
628 25 return 0;
629
630 4 fail:
631 4 free(staged_path);
632 4 free(absolute);
633
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (temporary_file != NULL)
634 ✗ (void)fclose(temporary_file);
635
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (state->temporary != NULL)
636 2 (void)patch_writer_cleanup(state, 0);
637 else {
638
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (state->base != NULL)
639 ✗ (void)libmpq__archive_close(state->base);
640
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 if (state->directory != NULL)
641 ✗ libmpq__directory_close(state->directory);
642 2 free(state->destination);
643 2 free(state);
644 }
645 4 return result;
646 }
647
648 /* Preserve ordinary MPQ patch creation and its existing lifecycle. */
649 int32_t
650 24 libmpq__patch_writer_begin(
651 mpq_patch_writer_s **patch_writer, const char *base_path, const char *patch_path
652 )
653 {
654 24 return patch_writer_begin_internal(patch_writer, base_path, patch_path, NULL, 0, 0);
655 }
656
657 /* Stage an MPQE patch using the archive writer's authentication and encryption. */
658 int32_t
659 7 libmpq__patch_writer_begin_mpqe(
660 mpq_patch_writer_s **patch_writer, const char *base_path, const char *patch_path,
661 const uint8_t *auth_code, size_t auth_code_size
662 )
663 {
664 7 return patch_writer_begin_internal(
665 patch_writer, base_path, patch_path, auth_code, auth_code_size, 1
666 );
667 }
668
669 /* Let archive finalization generate the configured weak or strong signature. */
670 int32_t
671 30 libmpq__patch_writer_sign(
672 mpq_patch_writer_s *state, uint32_t signature_type, const uint8_t *private_key,
673 size_t private_key_size
674 )
675 {
676
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 29 times.
30 if (state == NULL)
677 1 return LIBMPQ_ERROR_EXIST;
678 29 return libmpq__archive_sign(state->archive, signature_type, private_key, private_key_size);
679 }
680
681 /* Stage a named replacement, falling back to COPY unless BSD0 is smaller. */
682 int32_t
683 25 libmpq__patch_writer_replace(
684 mpq_patch_writer_s *state, const char *name, const uint8_t *data, libmpq__off_t size,
685 const mpq_file_options_s *options
686 )
687 {
688 25 mpq_file_options_s storage = { LIBMPQ_FILE_FLAG_SINGLE, 0, 0, 0, 0 };
689 25 uint8_t *before = NULL;
690 25 uint8_t *payload = NULL;
691 uint8_t after_md5[LIBMPQ_MD5_SIZE];
692 25 size_t before_size = 0;
693 25 size_t payload_size = 0;
694 uint32_t number;
695 uint32_t block;
696 uint16_t locale;
697 uint16_t platform;
698 int32_t result;
699
700
2/6
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 25 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
25 if (state == NULL || (data == NULL && size != 0))
701 ✗ return LIBMPQ_ERROR_EXIST;
702
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 if (size < 0 ||
703
1/2
✓ Branch 0 taken 25 times.
✗ Branch 1 not taken.
25 (uint64_t)size > UINT32_MAX - LIBMPQ_PATCH_INFO_SIZE - LIBMPQ_PATCH_COPY_HEADER_SIZE ||
704
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25 times.
25 (uint64_t)size > SIZE_MAX - LIBMPQ_PATCH_INFO_SIZE - LIBMPQ_PATCH_COPY_HEADER_SIZE)
705 ✗ return LIBMPQ_ERROR_SIZE;
706 25 result = patch_writer_member(state, name, &number, &locale, &platform);
707
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 24 times.
25 if (result != 0)
708 1 return result;
709
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 15 times.
24 if (options != NULL) {
710
3/4
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
9 if (options->locale != locale || options->platform != platform ||
711
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7 times.
7 (options->flags &
712 ~(LIBMPQ_FILE_FLAG_IMPLODE | LIBMPQ_FILE_FLAG_COMPRESS | LIBMPQ_FILE_FLAG_ENCRYPTED |
713 LIBMPQ_FILE_FLAG_SINGLE | LIBMPQ_FILE_FLAG_SECTOR_CRC)) != 0)
714 2 return LIBMPQ_ERROR_FORMAT;
715 7 storage = *options;
716 } else {
717 15 storage.locale = locale;
718 15 storage.platform = platform;
719 }
720 22 result = patch_writer_read_base(state, name, number, &before, &before_size);
721
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (result == 0)
722 22 result = patch_writer_payload(
723 before, before_size, data, (size_t)size, after_md5, &payload, &payload_size
724 );
725
5/6
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 13 times.
22 if (result == 0 && options == NULL && payload_size > state->archive->write_sector_size)
726 2 storage.flags = 0;
727
728
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (result == 0)
729 22 result = libmpq__writer_patch_file_add(
730 22 state->archive, name, payload, LIBMPQ_PATCH_INFO_SIZE, payload + LIBMPQ_PATCH_INFO_SIZE,
731 22 (libmpq__off_t)(payload_size - LIBMPQ_PATCH_INFO_SIZE), size, &storage
732 );
733
1/2
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
22 if (result == 0) {
734 22 block = state->archive->write_next_block - 1;
735 22 memcpy(state->archive->write_attributes[block].md5, after_md5, LIBMPQ_MD5_SIZE);
736 }
737 22 free(payload);
738 22 free(before);
739 22 return result;
740 }
741
742 /* Read one filesystem replacement and reuse the same staged data path. */
743 int32_t
744 3 libmpq__patch_writer_replace_path(
745 mpq_patch_writer_s *state, const char *name, const char *source_path,
746 const mpq_file_options_s *options
747 )
748 {
749 FILE *input;
750 libmpq__off_t size;
751 3 uint8_t *data = NULL;
752 int32_t result;
753
754
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3 times.
3 if (state == NULL || source_path == NULL || source_path[0] == '\0')
755 ✗ return LIBMPQ_ERROR_EXIST;
756 3 input = libmpq__file_open(source_path, "rb");
757
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (input == NULL)
758 ✗ return LIBMPQ_ERROR_OPEN;
759 3 result = libmpq__file_seek(input, 0, SEEK_END);
760
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (result == 0) {
761 3 size = libmpq__file_tell(input);
762
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (size < 0 ||
763
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 (uint64_t)size > UINT32_MAX - LIBMPQ_PATCH_INFO_SIZE - LIBMPQ_PATCH_COPY_HEADER_SIZE ||
764 (uint64_t)size > SIZE_MAX)
765 ✗ result = LIBMPQ_ERROR_SIZE;
766 }
767
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (result == 0)
768 3 result = libmpq__file_seek(input, 0, SEEK_SET);
769
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (result == 0) {
770
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 data = malloc(size == 0 ? 1 : (size_t)size);
771
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (data == NULL)
772 ✗ result = LIBMPQ_ERROR_MALLOC;
773 }
774
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
3 if (result == 0 && size != 0 && fread(data, 1, (size_t)size, input) != (size_t)size)
775 ✗ result = LIBMPQ_ERROR_READ;
776
1/4
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
3 if (fclose(input) != 0 && result == 0)
777 ✗ result = LIBMPQ_ERROR_CLOSE;
778
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (result == 0)
779 3 result = libmpq__patch_writer_replace(state, name, data, size, options);
780 3 free(data);
781 3 return result;
782 }
783
784 /* Stage a named delete marker without touching the base archive. */
785 int32_t
786 10 libmpq__patch_writer_remove(mpq_patch_writer_s *state, const char *name)
787 {
788 uint16_t locale;
789 uint16_t platform;
790 uint32_t number;
791 int32_t result;
792
793
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (state == NULL)
794 ✗ return LIBMPQ_ERROR_EXIST;
795 10 result = patch_writer_member(state, name, &number, &locale, &platform);
796
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 10 times.
10 if (result != 0)
797 ✗ return result;
798 10 return libmpq__writer_patch_delete_marker(state->archive, name, locale, platform);
799 }
800
801 /* Publish the finalized patch only after its writer has closed successfully. */
802 int32_t
803 23 libmpq__patch_writer_finish(mpq_patch_writer_s *state)
804 {
805
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 21 times.
23 if (state == NULL)
806 2 return LIBMPQ_ERROR_EXIST;
807 21 return patch_writer_cleanup(state, 1);
808 }
809
810 /* Discard a private patch writer and its unpublished same-directory file. */
811 int32_t
812 5 libmpq__patch_writer_abort(mpq_patch_writer_s *state)
813 {
814
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4 times.
5 if (state == NULL)
815 1 return LIBMPQ_ERROR_EXIST;
816 4 return patch_writer_cleanup(state, 0);
817 }
818