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