/src/zxc/src/lib/zxc_seekable.c
Line | Count | Source |
1 | | /* |
2 | | * ZXC - High-performance lossless compression |
3 | | * |
4 | | * Copyright (c) 2025-2026 Bertrand Lebonnois and contributors. |
5 | | * SPDX-License-Identifier: BSD-3-Clause |
6 | | */ |
7 | | |
8 | | /** |
9 | | * @file zxc_seekable.c |
10 | | * @brief Seekable archive reader (random-access decompression) and seek table writer. |
11 | | * |
12 | | * The seek table is a standard ZXC block (type = ZXC_BLOCK_SEK) appended |
13 | | * between the EOF block and the file footer. It records the compressed size |
14 | | * of every block (decompressed sizes are derived from the header's block_size), |
15 | | * enabling O(1) lookup + O(block_size) decompression for any byte range. |
16 | | * |
17 | | * On-disk layout of a SEK block: |
18 | | * |
19 | | * [Block Header (8B)] block_type=SEK, block_flags=0, comp_size=N*4 |
20 | | * [N x Entry (4B)] comp_size(u32 LE) per block |
21 | | * |
22 | | * Detection from end of file: |
23 | | * 1. Read file header (first 16 bytes) => block_size |
24 | | * 2. Read file footer (last 12 bytes) => total_decompressed_size |
25 | | * 3. Derive num_blocks = ceil(total_decomp / block_size) |
26 | | * 4. Compute seek block size, read backward to the block header |
27 | | * 5. Validate block_type == ZXC_BLOCK_SEK |
28 | | */ |
29 | | |
30 | | #include "../../include/zxc_seekable.h" |
31 | | |
32 | | #include "../../include/zxc_dict.h" |
33 | | #include "../../include/zxc_error.h" |
34 | | #include "zxc_internal.h" |
35 | | #include "zxc_threads.h" |
36 | | |
37 | | // ========================================================================= |
38 | | // Seek Table Writer |
39 | | // ========================================================================= |
40 | | |
41 | | /** |
42 | | * @brief Byte size of a seek table holding @p num_blocks entries. |
43 | | * |
44 | | * Public API (declared in @c zxc_seekable.h): one block header plus |
45 | | * @p num_blocks fixed-size entries. Use it to size the destination buffer |
46 | | * before @ref zxc_write_seek_table. |
47 | | */ |
48 | 9.45k | size_t zxc_seek_table_size(const uint32_t num_blocks) { |
49 | 9.45k | return ZXC_BLOCK_HEADER_SIZE + (size_t)num_blocks * ZXC_SEEK_ENTRY_SIZE; |
50 | 9.45k | } |
51 | | |
52 | | /** |
53 | | * @brief Serialises a seek table (a @c ZXC_BLOCK_SEK block) into @p dst. |
54 | | * |
55 | | * Public API; full contract in @c zxc_seekable.h. Emits the standard ZXC block |
56 | | * header followed by one little-endian @c u32 compressed-size entry per block. |
57 | | */ |
58 | | int64_t zxc_write_seek_table(uint8_t* dst, const size_t dst_capacity, const uint32_t* comp_sizes, |
59 | 9.45k | const uint32_t num_blocks) { |
60 | 9.45k | if (UNLIKELY(num_blocks > UINT32_MAX / ZXC_SEEK_ENTRY_SIZE)) return ZXC_ERROR_OVERFLOW; |
61 | | |
62 | 9.45k | const size_t total = zxc_seek_table_size(num_blocks); |
63 | 9.45k | if (UNLIKELY(dst_capacity < total)) return ZXC_ERROR_DST_TOO_SMALL; |
64 | 9.45k | if (UNLIKELY(!dst || !comp_sizes)) return ZXC_ERROR_NULL_INPUT; |
65 | | |
66 | 9.45k | const uint32_t payload_size = num_blocks * ZXC_SEEK_ENTRY_SIZE; |
67 | | |
68 | | // Write standard ZXC block header |
69 | 9.45k | const zxc_block_header_t bh = { |
70 | 9.45k | .block_type = ZXC_BLOCK_SEK, .block_flags = 0, .reserved = 0, .comp_size = payload_size}; |
71 | 9.45k | const int hdr_res = zxc_write_block_header(dst, dst_capacity, &bh); |
72 | 9.45k | if (UNLIKELY(hdr_res < 0)) return hdr_res; |
73 | 9.45k | uint8_t* p = dst + hdr_res; |
74 | | |
75 | | // Write entries: comp_size(4) only |
76 | 20.1k | for (uint32_t i = 0; i < num_blocks; i++) { |
77 | 10.6k | zxc_store_le32(p, comp_sizes[i]); |
78 | 10.6k | p += sizeof(uint32_t); |
79 | 10.6k | } |
80 | | |
81 | 9.45k | return (int64_t)(p - dst); |
82 | 9.45k | } |
83 | | |
84 | | // ========================================================================= |
85 | | // Seekable Reader (Opaque Handle) |
86 | | // ========================================================================= |
87 | | |
88 | | struct zxc_seekable_s { |
89 | | // Source - exactly one of {src, reader.read_at} is set. The FILE* variant |
90 | | // wraps pread() in its own reader ctx, indistinguishable from here. |
91 | | const uint8_t* src; |
92 | | uint64_t src_size; |
93 | | zxc_reader_t reader; /* user-supplied callback reader; read_at == NULL when unused */ |
94 | | |
95 | | // Reader context owned by the handle and freed in zxc_seekable_free, set by |
96 | | // the thin wrappers. NULL when the caller owns reader.ctx itself. |
97 | | void* owned_reader_ctx; |
98 | | |
99 | | // Parsed seek table |
100 | | uint32_t num_blocks; |
101 | | uint32_t* comp_sizes; /* array[num_blocks] */ |
102 | | uint64_t* comp_offsets; /* prefix-sum: byte offset in compressed file per block */ |
103 | | uint64_t total_decomp; /* total decompressed size (from footer) */ |
104 | | uint32_t max_comp_size; /* largest entry of comp_sizes, from the same walk */ |
105 | | |
106 | | // File header info - block_size is always a power of 2 in [4KB, 2MB], |
107 | | // fits in 21 bits. |
108 | | uint32_t block_size; |
109 | | int file_has_checksums; |
110 | | uint32_t expected_dict_id; /* dict_id from the file header; 0 = no dictionary */ |
111 | | |
112 | | // Reusable decompression context and compressed-block scratch. Both belong |
113 | | // to the single-threaded path, which is already not reentrant per handle; |
114 | | // the multi-threaded path gives each worker its own. |
115 | | zxc_cctx_t dctx; |
116 | | int dctx_initialized; |
117 | | uint8_t* read_buf; |
118 | | size_t read_buf_cap; |
119 | | |
120 | | // Dictionary (owned copy, freed in zxc_seekable_free). |
121 | | uint8_t* dict; |
122 | | size_t dict_size; |
123 | | // Shared literal Huffman table (owned copy; meaningful when has_dict_huf). |
124 | | uint8_t dict_huf[ZXC_HUF_TABLE_SIZE]; |
125 | | int has_dict_huf; |
126 | | }; |
127 | | |
128 | | /** |
129 | | * @struct zxc_seek_source_t |
130 | | * @brief Where the archive bytes come from during parsing. |
131 | | * |
132 | | * The two public entry points differ only in this: @ref zxc_seekable_open holds |
133 | | * the whole archive in memory, @ref zxc_seekable_open_reader reaches it through |
134 | | * a positioned callback. Everything after the first read is common, so the |
135 | | * parser below takes a source instead of being written twice. |
136 | | */ |
137 | | typedef struct { |
138 | | const uint8_t* data; /* in-memory archive, NULL in callback mode */ |
139 | | const zxc_reader_t* rdr; /* callback mode, NULL in buffer mode */ |
140 | | uint64_t size; /* archive size, both modes */ |
141 | | } zxc_seek_source_t; |
142 | | |
143 | | /** |
144 | | * @brief Reads a byte range from the archive, whatever backs it. |
145 | | * |
146 | | * Returns 1 on success, 0 if the range falls outside the archive or the |
147 | | * caller's reader came up short: every bounds check on a parsed offset goes |
148 | | * through here. |
149 | | */ |
150 | | static int zxc_seek_source_read(const zxc_seek_source_t* src, void* dst, const size_t len, |
151 | 27.4k | const uint64_t off) { |
152 | 27.4k | if (UNLIKELY(off > src->size || (uint64_t)len > src->size - off)) return 0; |
153 | 27.4k | if (src->data) { |
154 | 27.4k | ZXC_MEMCPY(dst, src->data + off, len); |
155 | 27.4k | return 1; |
156 | 27.4k | } |
157 | 0 | return src->rdr->read_at(src->rdr->ctx, dst, len, off) == (int64_t)len; |
158 | 27.4k | } |
159 | | |
160 | | /** |
161 | | * @brief Parses and validates the seek table at the end of the archive. |
162 | | * |
163 | | * Detection (backward from end): |
164 | | * 1. Read file header => block_size |
165 | | * 2. Read file footer => total_decomp_size |
166 | | * 3. Derive num_blocks = ceil(total_decomp_size / block_size) |
167 | | * 4. Compute expected seek block position, validate block_type == SEK |
168 | | * 5. Read comp_sizes, build the compressed-offset prefix sums, and check the |
169 | | * layout lands exactly on the EOF block |
170 | | * |
171 | | * Returns a handle to free via @ref zxc_seekable_free, or NULL if the archive |
172 | | * is too small or the seek table is missing / malformed. |
173 | | */ |
174 | 18.9k | static zxc_seekable* zxc_seekable_parse(const zxc_seek_source_t* src) { |
175 | | // Minimum: file_header(16) + eof_block(8) + seek_block_header(8) |
176 | | // + file_footer(12) = 44 |
177 | 18.9k | const uint64_t MIN_SEEKABLE_SIZE = |
178 | 18.9k | ZXC_FILE_HEADER_SIZE + ZXC_BLOCK_HEADER_SIZE + ZXC_BLOCK_HEADER_SIZE + ZXC_FILE_FOOTER_SIZE; |
179 | 18.9k | if (UNLIKELY(src->size < MIN_SEEKABLE_SIZE)) return NULL; |
180 | | |
181 | | // Step 1: validate file header => block_size |
182 | 18.0k | uint8_t header[ZXC_FILE_HEADER_SIZE]; |
183 | 18.0k | if (UNLIKELY(!zxc_seek_source_read(src, header, sizeof(header), 0))) return NULL; |
184 | | |
185 | 18.0k | size_t block_size_sz = 0; |
186 | 18.0k | int file_has_chk = 0; |
187 | 18.0k | uint32_t header_dict_id = 0; |
188 | 18.0k | if (UNLIKELY(zxc_read_file_header(header, sizeof(header), &block_size_sz, &file_has_chk, |
189 | 18.0k | &header_dict_id) != ZXC_OK)) |
190 | 8.56k | return NULL; // LCOV_EXCL_LINE |
191 | 9.45k | const uint32_t block_size = (uint32_t)block_size_sz; |
192 | 9.45k | if (UNLIKELY(block_size == 0)) return NULL; // LCOV_EXCL_LINE |
193 | | |
194 | | // Step 2: read total decompressed size from the file footer |
195 | 9.45k | uint8_t footer[ZXC_FILE_FOOTER_SIZE]; |
196 | 9.45k | if (UNLIKELY( |
197 | 9.45k | !zxc_seek_source_read(src, footer, sizeof(footer), src->size - ZXC_FILE_FOOTER_SIZE))) |
198 | 0 | return NULL; |
199 | 9.45k | const uint64_t total_decomp = zxc_le64(footer); |
200 | | |
201 | | // A value of 0 means empty file - no seek table |
202 | 9.45k | if (UNLIKELY(total_decomp == 0)) return NULL; |
203 | | |
204 | | // Step 3: derive num_blocks = ceil(total_decomp / block_size) |
205 | 9.45k | const uint64_t num_blocks_64 = total_decomp / block_size + (total_decomp % block_size != 0); |
206 | 9.45k | if (UNLIKELY(num_blocks_64 > UINT32_MAX)) return NULL; |
207 | 9.45k | const uint32_t num_blocks = (uint32_t)num_blocks_64; |
208 | | |
209 | | // Step 4: locate and validate the seek block. Two headers of margin, not one: |
210 | | // the tail read below spans the EOF block, so tail_total could wrap on 32 bits. |
211 | 9.45k | const uint64_t entries_total = num_blocks_64 * ZXC_SEEK_ENTRY_SIZE; |
212 | 9.45k | if (UNLIKELY(entries_total > SIZE_MAX - 2 * ZXC_BLOCK_HEADER_SIZE)) return NULL; |
213 | | |
214 | 9.45k | const size_t seek_block_total = ZXC_BLOCK_HEADER_SIZE + (size_t)entries_total; |
215 | 9.45k | if (UNLIKELY((uint64_t)seek_block_total + ZXC_FILE_FOOTER_SIZE > src->size)) return NULL; |
216 | | |
217 | 9.45k | const uint64_t seek_off = src->size - ZXC_FILE_FOOTER_SIZE - (uint64_t)seek_block_total; |
218 | 9.45k | if (UNLIKELY(seek_off < ZXC_BLOCK_HEADER_SIZE)) return NULL; |
219 | | |
220 | | // The EOF block sits immediately before the seek block, so one read covers |
221 | | // both: [EOF 8][SEK header 8][entries]. Keeps a reader-backed open at three |
222 | | // reads (header, footer, tail) while validating the same layout the |
223 | | // in-memory path does. |
224 | 9.45k | const size_t tail_total = ZXC_BLOCK_HEADER_SIZE + seek_block_total; |
225 | 9.45k | const uint64_t tail_off = seek_off - ZXC_BLOCK_HEADER_SIZE; |
226 | | |
227 | 9.45k | uint8_t* tail = NULL; |
228 | 9.45k | const uint8_t* tail_view; |
229 | 9.45k | zxc_seekable* s = NULL; |
230 | 9.45k | if (src->data) { |
231 | 9.45k | tail_view = src->data + tail_off; |
232 | 9.45k | } else { |
233 | 0 | tail = (uint8_t*)ZXC_MALLOC(tail_total); |
234 | 0 | if (UNLIKELY(!tail)) return NULL; // LCOV_EXCL_LINE |
235 | 0 | if (UNLIKELY(!zxc_seek_source_read(src, tail, tail_total, tail_off))) goto fail; |
236 | 0 | tail_view = tail; |
237 | 0 | } |
238 | | |
239 | 9.45k | const uint8_t* const eof_hdr = tail_view; |
240 | 9.45k | const uint8_t* const seek_blk = tail_view + ZXC_BLOCK_HEADER_SIZE; |
241 | | |
242 | 9.45k | zxc_block_header_t bh; |
243 | | // The SEK header stores the table size in a 32-bit field, so reject any larger value before |
244 | | // reading it. |
245 | 9.45k | if (UNLIKELY(entries_total > UINT32_MAX)) goto fail; |
246 | 9.45k | if (UNLIKELY(zxc_read_block_header(seek_blk, seek_block_total, &bh) != ZXC_OK || |
247 | 9.45k | bh.block_type != ZXC_BLOCK_SEK || bh.comp_size != entries_total)) |
248 | 0 | goto fail; |
249 | | |
250 | | // Step 5: allocate the handle and parse the entries |
251 | 9.45k | s = (zxc_seekable*)ZXC_CALLOC(1, sizeof(zxc_seekable)); |
252 | 9.45k | if (UNLIKELY(!s)) goto fail; // LCOV_EXCL_LINE |
253 | | |
254 | 9.45k | if (src->rdr) s->reader = *src->rdr; |
255 | 9.45k | s->src = src->data; |
256 | 9.45k | s->src_size = src->size; |
257 | 9.45k | s->num_blocks = num_blocks; |
258 | 9.45k | s->block_size = block_size; |
259 | 9.45k | s->file_has_checksums = file_has_chk; |
260 | 9.45k | s->expected_dict_id = header_dict_id; |
261 | 9.45k | s->total_decomp = total_decomp; |
262 | | |
263 | 9.45k | s->comp_sizes = (uint32_t*)ZXC_CALLOC(num_blocks, sizeof(uint32_t)); |
264 | 9.45k | s->comp_offsets = (uint64_t*)ZXC_CALLOC((size_t)num_blocks + 1, sizeof(uint64_t)); |
265 | 9.45k | if (UNLIKELY(!s->comp_sizes || !s->comp_offsets)) goto fail; // LCOV_EXCL_LINE |
266 | | |
267 | | // Parse comp_sizes and build compressed prefix sums. Every entry is checked |
268 | | // against the archive size, so the prefix sum can neither overflow nor |
269 | | // point a later read out of bounds. |
270 | 9.45k | { |
271 | 9.45k | const uint8_t* ep = seek_blk + ZXC_BLOCK_HEADER_SIZE; |
272 | 9.45k | uint64_t comp_acc = ZXC_FILE_HEADER_SIZE; /* blocks start after file header */ |
273 | 20.1k | for (uint32_t i = 0; i < num_blocks; i++) { |
274 | 10.6k | s->comp_sizes[i] = zxc_le32(ep); |
275 | 10.6k | ep += sizeof(uint32_t); |
276 | | |
277 | | // Reject entries below minimum (block header) or larger than the file |
278 | 10.6k | if (UNLIKELY(s->comp_sizes[i] < ZXC_BLOCK_HEADER_SIZE || s->comp_sizes[i] > src->size)) |
279 | 0 | goto fail; |
280 | 10.6k | if (s->comp_sizes[i] > s->max_comp_size) s->max_comp_size = s->comp_sizes[i]; |
281 | 10.6k | s->comp_offsets[i] = comp_acc; |
282 | 10.6k | comp_acc += s->comp_sizes[i]; |
283 | | // Reject if cumulative offset exceeds file size (inconsistent table) |
284 | 10.6k | if (UNLIKELY(comp_acc > src->size)) goto fail; // LCOV_EXCL_LINE |
285 | 10.6k | } |
286 | 9.45k | s->comp_offsets[num_blocks] = comp_acc; |
287 | | |
288 | | // Verify the prefix sum lands exactly on the EOF block, and that an EOF |
289 | | // block really sits there. Expected layout: |
290 | | // [header 16][data blocks][EOF 8][SEK block][footer 12] |
291 | 9.45k | zxc_block_header_t eof_bh; |
292 | 9.45k | if (UNLIKELY(comp_acc != seek_off - ZXC_BLOCK_HEADER_SIZE || |
293 | 9.45k | zxc_read_block_header(eof_hdr, ZXC_BLOCK_HEADER_SIZE, &eof_bh) != ZXC_OK || |
294 | 9.45k | eof_bh.block_type != ZXC_BLOCK_EOF)) |
295 | 0 | goto fail; |
296 | 9.45k | } |
297 | | |
298 | 9.45k | ZXC_FREE(tail); |
299 | 9.45k | return s; |
300 | | |
301 | 0 | fail: |
302 | 0 | ZXC_FREE(tail); |
303 | 0 | zxc_seekable_free(s); |
304 | 0 | return NULL; |
305 | 9.45k | } |
306 | | |
307 | | /** |
308 | | * @brief Opens a seekable archive held entirely in a memory buffer. |
309 | | * |
310 | | * Public API; see @c zxc_seekable.h. Thin guard around |
311 | | * @ref zxc_seekable_parse, which detects and validates the trailing seek table. |
312 | | */ |
313 | 18.9k | zxc_seekable* zxc_seekable_open(const void* src, const size_t src_size) { |
314 | 18.9k | if (UNLIKELY(!src || src_size == 0)) return NULL; |
315 | 18.9k | const zxc_seek_source_t source = {(const uint8_t*)src, NULL, (uint64_t)src_size}; |
316 | 18.9k | return zxc_seekable_parse(&source); |
317 | 18.9k | } |
318 | | |
319 | | // zxc_seekable_open_file lives elsewhere: it builds a zxc_reader_t over pread() |
320 | | // and delegates below, keeping this TU free of <stdio.h>. |
321 | | |
322 | | /** |
323 | | * @brief Opens a seekable archive over a caller-supplied random-access reader. |
324 | | * |
325 | | * Public API; see @c zxc_seekable.h. Reads the file header, footer and seek |
326 | | * block through @p r->read_at (the FILE* variant wraps @c pread this way), |
327 | | * validates the SEK block, and builds the per-block compressed-offset prefix |
328 | | * sums. Unlike @ref zxc_seekable_open the archive is never mapped whole; only |
329 | | * the metadata is read up front. |
330 | | */ |
331 | 0 | zxc_seekable* zxc_seekable_open_reader(const zxc_reader_t* r) { |
332 | 0 | if (UNLIKELY(!r || !r->read_at || r->size == 0)) return NULL; |
333 | 0 | const zxc_seek_source_t source = {NULL, r, r->size}; |
334 | 0 | return zxc_seekable_parse(&source); |
335 | 0 | } |
336 | | |
337 | | /** |
338 | | * @brief Number of blocks in the archive. |
339 | | */ |
340 | 18.9k | uint32_t zxc_seekable_get_num_blocks(const zxc_seekable* s) { return s ? s->num_blocks : 0; } |
341 | | |
342 | | /** |
343 | | * @brief Total decompressed size of the archive. |
344 | | */ |
345 | 18.9k | uint64_t zxc_seekable_get_decompressed_size(const zxc_seekable* s) { |
346 | 18.9k | return s ? s->total_decomp : 0; |
347 | 18.9k | } |
348 | | |
349 | | /** |
350 | | * @brief Compressed byte size of a given block. |
351 | | */ |
352 | 20.1k | uint32_t zxc_seekable_get_block_comp_size(const zxc_seekable* s, const uint32_t block_idx) { |
353 | 20.1k | if (UNLIKELY(!s || block_idx >= s->num_blocks)) return 0; |
354 | 10.6k | return s->comp_sizes[block_idx]; |
355 | 20.1k | } |
356 | | |
357 | | /** |
358 | | * @brief Decompressed byte size of a given block. |
359 | | * |
360 | | * Every block decompresses to @c block_size except the last, which holds the |
361 | | * remainder of @c total_decomp. |
362 | | */ |
363 | 20.1k | uint32_t zxc_seekable_get_block_decomp_size(const zxc_seekable* s, const uint32_t block_idx) { |
364 | 20.1k | if (UNLIKELY(!s || block_idx >= s->num_blocks)) return 0; |
365 | 10.6k | const uint64_t start = (uint64_t)block_idx * (uint64_t)s->block_size; |
366 | 10.6k | const uint64_t remaining = s->total_decomp - start; |
367 | 10.6k | return (remaining >= (uint64_t)s->block_size) ? s->block_size : (uint32_t)remaining; |
368 | 20.1k | } |
369 | | |
370 | | // ========================================================================= |
371 | | // Random-Access Decompression |
372 | | // ========================================================================= |
373 | | |
374 | | /** |
375 | | * @brief Maps a decompressed @p offset to its containing block index (O(1)). |
376 | | * @param[in] block_size Fixed decompressed block size (a power of two). |
377 | | * @param[in] offset Absolute decompressed byte offset. |
378 | | * @return Zero-based index of the block that holds @p offset. |
379 | | */ |
380 | 40.0k | static uint32_t zxc_seek_find_block(const uint32_t block_size, const uint64_t offset) { |
381 | 40.0k | return (uint32_t)(offset / (uint64_t)block_size); |
382 | 40.0k | } |
383 | | |
384 | | /** |
385 | | * @brief Decompressed start offset of block @p idx (O(1)). |
386 | | * @param[in] block_size Fixed decompressed block size. |
387 | | * @param[in] idx Zero-based block index. |
388 | | * @return Absolute decompressed byte offset where block @p idx begins. |
389 | | */ |
390 | 20.2k | static uint64_t zxc_seek_decomp_offset(const uint32_t block_size, const uint32_t idx) { |
391 | 20.2k | return (uint64_t)idx * (uint64_t)block_size; |
392 | 20.2k | } |
393 | | |
394 | | /** |
395 | | * @brief Decompressed size of block @p idx (O(1)). |
396 | | * |
397 | | * Returns @p block_size for every block except the last, which holds the |
398 | | * remainder of @p total_decomp. |
399 | | * |
400 | | * @param[in] block_size Fixed decompressed block size. |
401 | | * @param[in] total_decomp Total decompressed archive size. |
402 | | * @param[in] idx Zero-based block index. |
403 | | * @return Decompressed byte size of block @p idx. |
404 | | */ |
405 | | static uint32_t zxc_seek_decomp_size(const uint32_t block_size, const uint64_t total_decomp, |
406 | 1.20k | const uint32_t idx) { |
407 | 1.20k | const uint64_t start = (uint64_t)idx * (uint64_t)block_size; |
408 | 1.20k | const uint64_t remaining = total_decomp - start; |
409 | 1.20k | return (remaining >= (uint64_t)block_size) ? block_size : (uint32_t)remaining; |
410 | 1.20k | } |
411 | | |
412 | | /** |
413 | | * @brief Reads a compressed block into @p buf from the memory buffer or reader. |
414 | | * |
415 | | * Copies from @c s->src in buffer mode, otherwise calls @c s->reader.read_at |
416 | | * (which also backs the FILE* variant). |
417 | | * |
418 | | * @param[in] s Seekable handle. |
419 | | * @param[in] block_idx Zero-based block index to read. |
420 | | * @param[out] buf Destination buffer. |
421 | | * @param[in] buf_cap Capacity of @p buf in bytes. |
422 | | * @return The block's compressed byte count on success, or a negative |
423 | | * @ref zxc_error_t (@ref ZXC_ERROR_DST_TOO_SMALL, |
424 | | * @ref ZXC_ERROR_SRC_TOO_SMALL, @ref ZXC_ERROR_IO). |
425 | | */ |
426 | | static int zxc_seek_read_block(const zxc_seekable* s, const uint32_t block_idx, uint8_t* buf, |
427 | 20.2k | const size_t buf_cap) { |
428 | 20.2k | const uint64_t off = s->comp_offsets[block_idx]; |
429 | 20.2k | const uint32_t csz = s->comp_sizes[block_idx]; |
430 | 20.2k | if (UNLIKELY(csz > buf_cap)) return ZXC_ERROR_DST_TOO_SMALL; |
431 | | |
432 | 20.2k | if (s->src) { |
433 | | // Buffer mode |
434 | 20.2k | if (UNLIKELY(off + csz > s->src_size)) return ZXC_ERROR_SRC_TOO_SMALL; |
435 | 20.2k | ZXC_MEMCPY(buf, s->src + off, csz); |
436 | 20.2k | } else if (s->reader.read_at) { |
437 | | // Caller-supplied reader (also covers the FILE* variant, which |
438 | | // provides a pread-backed callback from zxc_seekable_file.c). |
439 | 0 | const int64_t r = s->reader.read_at(s->reader.ctx, buf, csz, off); |
440 | 0 | if (UNLIKELY(r != (int64_t)csz)) return (r < 0) ? (int)r : ZXC_ERROR_IO; |
441 | 1 | } else { |
442 | 1 | return ZXC_ERROR_NULL_INPUT; // LCOV_EXCL_LINE |
443 | 1 | } |
444 | 20.2k | return (int)csz; |
445 | 20.2k | } |
446 | | |
447 | | /** |
448 | | * @brief Decompresses the byte range [@p offset, @p offset + @p len) into @p dst. |
449 | | * |
450 | | * Public API; full contract in @c zxc_seekable.h. Maps the range to its block |
451 | | * span via O(1) division, decodes each covered block through a reusable, |
452 | | * lazily-initialised, dictionary-aware context, and copies out only the |
453 | | * requested sub-range. Single-threaded; see @ref zxc_seekable_decompress_range_mt |
454 | | * for the parallel variant. |
455 | | */ |
456 | | int64_t zxc_seekable_decompress_range(zxc_seekable* s, void* dst, const size_t dst_capacity, |
457 | 37.1k | const uint64_t offset, const size_t len) { |
458 | 37.1k | if (UNLIKELY(len == 0)) return 0; |
459 | 27.7k | if (UNLIKELY(!s || !dst)) return ZXC_ERROR_NULL_INPUT; |
460 | 27.7k | if (UNLIKELY(dst_capacity < len)) return ZXC_ERROR_DST_TOO_SMALL; |
461 | 27.7k | if (UNLIKELY(offset + len > s->total_decomp)) return ZXC_ERROR_SRC_TOO_SMALL; |
462 | 18.2k | if (UNLIKELY(s->expected_dict_id != 0 && (!s->dict || s->dict_size == 0))) |
463 | 0 | return ZXC_ERROR_DICT_REQUIRED; |
464 | | |
465 | | // Initialize decompression context on first use |
466 | 18.2k | if (!s->dctx_initialized) { |
467 | | // LCOV_EXCL_START |
468 | 9.45k | if (UNLIKELY(zxc_cctx_init(&s->dctx, (size_t)s->block_size, 0, 0, 0, s->dict_size) != |
469 | 9.45k | ZXC_OK)) |
470 | 0 | return ZXC_ERROR_MEMORY; |
471 | | // LCOV_EXCL_STOP |
472 | 9.45k | if (UNLIKELY(zxc_cctx_attach_dict_huf(&s->dctx, s->has_dict_huf ? s->dict_huf : NULL) != |
473 | 9.45k | ZXC_OK)) { |
474 | | // LCOV_EXCL_START |
475 | 0 | zxc_cctx_free(&s->dctx); |
476 | 0 | return ZXC_ERROR_CORRUPT_DATA; |
477 | | // LCOV_EXCL_STOP |
478 | 0 | } |
479 | 9.45k | s->dctx_initialized = 1; |
480 | 9.45k | if (s->dict_size > 0) ZXC_MEMCPY(s->dctx.dict_buffer, s->dict, s->dict_size); |
481 | 9.45k | } |
482 | 18.2k | s->dctx.dict_size = s->dict_size; |
483 | | |
484 | | // work_buf is pre-sized to block_size + ZXC_DECOMPRESS_TAIL_PAD by the |
485 | | // matching zxc_cctx_init above. |
486 | 18.2k | const size_t work_sz = (size_t)s->block_size + ZXC_DECOMPRESS_TAIL_PAD; |
487 | | |
488 | | // Find block range - O(1) division |
489 | 18.2k | const uint32_t blk_start = zxc_seek_find_block(s->block_size, offset); |
490 | 18.2k | const uint32_t blk_end = zxc_seek_find_block(s->block_size, offset + len - 1); |
491 | | |
492 | 18.2k | uint8_t* out = (uint8_t*)dst; |
493 | 18.2k | size_t remaining = len; |
494 | | |
495 | | // Compressed-block scratch, sized once for the largest block of the archive |
496 | | // and kept on the handle: a range read is often one of many. |
497 | 18.2k | const size_t read_cap = (size_t)s->max_comp_size + ZXC_PAD_SIZE; |
498 | 18.2k | if (s->read_buf_cap < read_cap) { |
499 | 9.45k | uint8_t* const nb = (uint8_t*)ZXC_REALLOC(s->read_buf, read_cap); |
500 | 9.45k | if (UNLIKELY(!nb)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_LINE |
501 | 9.45k | s->read_buf = nb; |
502 | 9.45k | s->read_buf_cap = read_cap; |
503 | 9.45k | } |
504 | 18.2k | uint8_t* const read_buf = s->read_buf; |
505 | | |
506 | 37.3k | for (uint32_t bi = blk_start; bi <= blk_end; bi++) { |
507 | | // Read compressed block data |
508 | 19.0k | const int read_res = zxc_seek_read_block(s, bi, read_buf, read_cap); |
509 | 19.0k | if (UNLIKELY(read_res < 0)) return read_res; // LCOV_EXCL_LINE |
510 | | |
511 | | // Decompress the block: when a dictionary is active, decode into the |
512 | | // cctx-owned dict_buffer (which has dict content prepended) so that |
513 | | // match copies referencing dictionary bytes resolve naturally. |
514 | 19.0k | uint8_t* dec_dst = |
515 | 19.0k | s->dctx.dict_buffer ? s->dctx.dict_buffer + s->dict_size : s->dctx.work_buf; |
516 | 19.0k | const int dec_res = |
517 | 19.0k | zxc_decompress_chunk_wrapper(&s->dctx, read_buf, (size_t)read_res, dec_dst, work_sz); |
518 | 19.0k | if (UNLIKELY(dec_res < 0)) return dec_res; // LCOV_EXCL_LINE |
519 | | |
520 | | // Calculate which portion of this block's decompressed data we need |
521 | 19.0k | const uint64_t blk_decomp_start = zxc_seek_decomp_offset(s->block_size, bi); |
522 | 19.0k | const size_t skip = (offset > blk_decomp_start) ? (size_t)(offset - blk_decomp_start) : 0; |
523 | 19.0k | if (UNLIKELY((size_t)dec_res < skip)) return ZXC_ERROR_CORRUPT_DATA; // LCOV_EXCL_LINE |
524 | 19.0k | const size_t avail = (size_t)dec_res - skip; |
525 | 19.0k | const size_t copy = (avail < remaining) ? avail : remaining; |
526 | | |
527 | 19.0k | ZXC_MEMCPY(out, dec_dst + skip, copy); |
528 | 19.0k | out += copy; |
529 | 19.0k | remaining -= copy; |
530 | 19.0k | } |
531 | | |
532 | 18.2k | return (int64_t)len; |
533 | 18.2k | } |
534 | | |
535 | | // ========================================================================= |
536 | | // Multi-Threaded Random-Access Decompression (Fork-Join) |
537 | | // ========================================================================= |
538 | | |
539 | | /** |
540 | | * @brief Per-block job descriptor for multi-threaded decompression. |
541 | | * |
542 | | * Each worker thread receives a pointer to one of these, performs the read + |
543 | | * decompress + memcpy sequence, and writes the result code into @c result. |
544 | | * The main thread inspects @c result after join. |
545 | | */ |
546 | | typedef struct { |
547 | | const zxc_seekable* s; /* shared handle (read-only) */ |
548 | | uint32_t block_idx; /* block to decompress */ |
549 | | uint8_t* dst; /* output pointer within caller's buffer */ |
550 | | size_t skip; /* bytes to skip at start of decompressed block */ |
551 | | size_t copy_len; /* bytes to copy into dst */ |
552 | | int result; /* 0 = OK, < 0 = error */ |
553 | | } zxc_seek_mt_job_t; |
554 | | |
555 | | /** |
556 | | * @struct zxc_seek_mt_stripe_t |
557 | | * @brief Per-thread stripe descriptor for multi-threaded decompression. |
558 | | * |
559 | | * Each worker owns the job subset {first, first+stride, first+2*stride, ...} |
560 | | * of the shared @c jobs array and reuses one decompression context, one |
561 | | * dictionary copy and one read buffer across all of them, amortising what |
562 | | * would otherwise be per-block costs (context init, dict memcpy, malloc and |
563 | | * a thread spawn per block). Stripes are pairwise disjoint, so workers |
564 | | * touch distinct jobs and distinct output ranges and need no |
565 | | * synchronisation beyond the final join. |
566 | | * |
567 | | * Written by @ref zxc_seekable_decompress_range_mt before the fork phase and |
568 | | * read-only for the worker (@ref zxc_seek_mt_worker); results travel through |
569 | | * the jobs themselves (@c zxc_seek_mt_job_t::result). |
570 | | * |
571 | | * @var zxc_seek_mt_stripe_t::jobs |
572 | | * Job array shared by all workers; this worker only reads/writes the |
573 | | * entries of its own stripe. |
574 | | * @var zxc_seek_mt_stripe_t::num_jobs |
575 | | * Total number of jobs in @c jobs (stripe iteration bound). |
576 | | * @var zxc_seek_mt_stripe_t::first |
577 | | * Index of this worker's first job (equals its worker index, in |
578 | | * [0, @c stride)). |
579 | | * @var zxc_seek_mt_stripe_t::stride |
580 | | * Stripe step between consecutive jobs of this worker; equals the |
581 | | * worker-thread count. |
582 | | */ |
583 | | typedef struct { |
584 | | zxc_seek_mt_job_t* jobs; |
585 | | uint32_t num_jobs; |
586 | | uint32_t first; |
587 | | uint32_t stride; |
588 | | } zxc_seek_mt_stripe_t; |
589 | | |
590 | | /** |
591 | | * @brief Marks every job of a stripe with @p code (setup-failure path). |
592 | | * |
593 | | * @param[in,out] st Stripe whose jobs to mark. |
594 | | * @param[in] code Negative @ref zxc_error_t value. |
595 | | */ |
596 | 0 | static void zxc_seek_mt_fail_stripe(zxc_seek_mt_stripe_t* st, const int code) { |
597 | 0 | for (uint32_t i = st->first; i < st->num_jobs; i += st->stride) st->jobs[i].result = code; |
598 | 0 | } |
599 | | |
600 | | /** |
601 | | * @brief Worker thread entry point for multi-threaded seekable decompression. |
602 | | * |
603 | | * Sets up its context, dictionary copy and read buffer once, then for each |
604 | | * block of its stripe: read (thread-safe pread), decompress, copy the |
605 | | * requested sub-range into the caller's output. The dict prefix survives |
606 | | * across blocks because the decoder never writes below its dst. |
607 | | * |
608 | | * Each job's outcome goes into its @c result (read by the main thread after |
609 | | * join); on error the worker abandons the rest of its stripe. |
610 | | * |
611 | | * @param[in,out] arg Pointer to this worker's `zxc_seek_mt_stripe_t`. |
612 | | * @return Always NULL (result codes are reported via the jobs). |
613 | | */ |
614 | 1.19k | static void* zxc_seek_mt_worker(void* arg) { |
615 | 1.19k | zxc_seek_mt_stripe_t* const st = (zxc_seek_mt_stripe_t*)arg; |
616 | 1.19k | zxc_seek_mt_job_t* const jobs = st->jobs; |
617 | 1.19k | const zxc_seekable* const s = jobs[st->first].s; |
618 | | |
619 | | // Thread-local decompression context (mode=0 for decompress-only) |
620 | 1.19k | zxc_cctx_t dctx; |
621 | 1.19k | if (UNLIKELY(zxc_cctx_init(&dctx, (size_t)s->block_size, 0, 0, 0, s->dict_size) != ZXC_OK)) { |
622 | | // LCOV_EXCL_START |
623 | 0 | zxc_seek_mt_fail_stripe(st, ZXC_ERROR_MEMORY); |
624 | 0 | return NULL; |
625 | | // LCOV_EXCL_STOP |
626 | 0 | } |
627 | | |
628 | 1.19k | if (UNLIKELY(zxc_cctx_attach_dict_huf(&dctx, s->has_dict_huf ? s->dict_huf : NULL) != ZXC_OK)) { |
629 | | // LCOV_EXCL_START |
630 | 0 | zxc_cctx_free(&dctx); |
631 | 0 | zxc_seek_mt_fail_stripe(st, ZXC_ERROR_CORRUPT_DATA); |
632 | 0 | return NULL; |
633 | | // LCOV_EXCL_STOP |
634 | 0 | } |
635 | 1.19k | const size_t work_sz = (size_t)s->block_size + ZXC_DECOMPRESS_TAIL_PAD; |
636 | | |
637 | 1.19k | uint8_t* const dict_work = dctx.dict_buffer; |
638 | 1.19k | if (dict_work) ZXC_MEMCPY(dict_work, s->dict, s->dict_size); |
639 | | |
640 | | // Read buffer sized for the largest compressed block of the stripe. |
641 | 1.19k | size_t max_csz = 0; |
642 | 2.39k | for (uint32_t i = st->first; i < st->num_jobs; i += st->stride) { |
643 | 1.20k | const uint32_t csz = s->comp_sizes[jobs[i].block_idx]; |
644 | 1.20k | if (csz > max_csz) max_csz = csz; |
645 | 1.20k | } |
646 | 1.19k | uint8_t* const read_buf = (uint8_t*)ZXC_MALLOC(max_csz + ZXC_PAD_SIZE); |
647 | 1.19k | if (UNLIKELY(!read_buf)) { |
648 | | // LCOV_EXCL_START |
649 | 0 | zxc_cctx_free(&dctx); |
650 | 0 | zxc_seek_mt_fail_stripe(st, ZXC_ERROR_MEMORY); |
651 | 0 | return NULL; |
652 | | // LCOV_EXCL_STOP |
653 | 0 | } |
654 | | |
655 | 2.40k | for (uint32_t i = st->first; i < st->num_jobs; i += st->stride) { |
656 | 1.20k | zxc_seek_mt_job_t* const job = &jobs[i]; |
657 | | |
658 | 1.20k | const int read_res = |
659 | 1.20k | zxc_seek_read_block(s, job->block_idx, read_buf, max_csz + ZXC_PAD_SIZE); |
660 | 1.20k | if (UNLIKELY(read_res < 0)) { |
661 | | // LCOV_EXCL_START |
662 | 0 | job->result = read_res; |
663 | 0 | break; |
664 | | // LCOV_EXCL_STOP |
665 | 0 | } |
666 | | |
667 | | // Decompress: use dict bounce buffer when dictionary is active |
668 | 1.20k | uint8_t* dec_dst = dict_work ? dict_work + s->dict_size : dctx.work_buf; |
669 | 1.20k | const int dec_res = |
670 | 1.20k | zxc_decompress_chunk_wrapper(&dctx, read_buf, (size_t)read_res, dec_dst, work_sz); |
671 | | |
672 | 1.20k | if (UNLIKELY(dec_res < 0)) { |
673 | | // LCOV_EXCL_START |
674 | 0 | job->result = dec_res; |
675 | 0 | break; |
676 | | // LCOV_EXCL_STOP |
677 | 0 | } |
678 | 1.20k | if (UNLIKELY((size_t)dec_res < job->skip + job->copy_len)) { |
679 | | // LCOV_EXCL_START |
680 | 0 | job->result = ZXC_ERROR_CORRUPT_DATA; |
681 | 0 | break; |
682 | | // LCOV_EXCL_STOP |
683 | 0 | } |
684 | | |
685 | | // Copy the requested portion directly into the caller's output buffer |
686 | 1.20k | ZXC_MEMCPY(job->dst, dec_dst + job->skip, job->copy_len); |
687 | 1.20k | job->result = 0; |
688 | 1.20k | } |
689 | | |
690 | 1.19k | ZXC_FREE(read_buf); |
691 | 1.19k | zxc_cctx_free(&dctx); |
692 | 1.19k | return NULL; |
693 | 1.19k | } |
694 | | |
695 | | /** |
696 | | * @brief Multi-threaded variant of @ref zxc_seekable_decompress_range. |
697 | | * |
698 | | * Public API; full contract in @c zxc_seekable.h. Plans one job per covered |
699 | | * block (each with its own thread-local context and read buffer) and runs them |
700 | | * fork-join in waves of up to @p n_threads. Falls back to the single-threaded |
701 | | * path for trivial spans. @p n_threads == 0 auto-detects the core count. |
702 | | */ |
703 | | int64_t zxc_seekable_decompress_range_mt(zxc_seekable* s, void* dst, const size_t dst_capacity, |
704 | 1.72k | const uint64_t offset, const size_t len, int n_threads) { |
705 | 1.72k | if (UNLIKELY(len == 0)) return 0; |
706 | 1.72k | if (UNLIKELY(!s || !dst)) return ZXC_ERROR_NULL_INPUT; |
707 | 1.72k | if (UNLIKELY(dst_capacity < len)) return ZXC_ERROR_DST_TOO_SMALL; |
708 | 1.72k | if (UNLIKELY(offset + len > s->total_decomp)) return ZXC_ERROR_SRC_TOO_SMALL; |
709 | 1.72k | if (UNLIKELY(s->expected_dict_id != 0 && (!s->dict || s->dict_size == 0))) |
710 | 0 | return ZXC_ERROR_DICT_REQUIRED; |
711 | | |
712 | | // Find block range - O(1) division |
713 | 1.72k | const uint32_t blk_start = zxc_seek_find_block(s->block_size, offset); |
714 | 1.72k | const uint32_t blk_end = zxc_seek_find_block(s->block_size, offset + len - 1); |
715 | 1.72k | const uint32_t num_jobs = blk_end - blk_start + 1; |
716 | | |
717 | | // Auto-detect thread count (0 = use all available cores) |
718 | 1.72k | if (n_threads == 0) n_threads = zxc_num_procs(); |
719 | | |
720 | | // Fallback to single-threaded path for trivial cases |
721 | 1.72k | if (n_threads <= 1 || num_jobs <= 1) { |
722 | 1.12k | return zxc_seekable_decompress_range(s, dst, dst_capacity, offset, len); |
723 | 1.12k | } |
724 | | |
725 | | // Cap threads to number of blocks and max limit |
726 | 601 | if ((uint32_t)n_threads > num_jobs) n_threads = (int)num_jobs; |
727 | 601 | if (n_threads > ZXC_MAX_THREADS) n_threads = ZXC_MAX_THREADS; |
728 | | |
729 | | // Allocate job descriptors |
730 | 601 | zxc_seek_mt_job_t* const jobs = |
731 | 601 | (zxc_seek_mt_job_t*)ZXC_CALLOC(num_jobs, sizeof(zxc_seek_mt_job_t)); |
732 | 601 | if (UNLIKELY(!jobs)) return ZXC_ERROR_MEMORY; // LCOV_EXCL_LINE |
733 | | |
734 | | // Plan jobs: compute skip, copy_len, and dst pointer for each block |
735 | 601 | uint8_t* out = (uint8_t*)dst; |
736 | 601 | size_t remaining = len; |
737 | 1.80k | for (uint32_t i = 0; i < num_jobs; i++) { |
738 | 1.20k | const uint32_t bi = blk_start + i; |
739 | 1.20k | const uint64_t blk_decomp_start = zxc_seek_decomp_offset(s->block_size, bi); |
740 | 1.20k | const size_t skip = (offset > blk_decomp_start) ? (size_t)(offset - blk_decomp_start) : 0; |
741 | 1.20k | const size_t blk_decomp_sz = zxc_seek_decomp_size(s->block_size, s->total_decomp, bi); |
742 | 1.20k | if (UNLIKELY(blk_decomp_sz < skip)) { |
743 | | // LCOV_EXCL_START |
744 | 0 | ZXC_FREE(jobs); |
745 | 0 | return ZXC_ERROR_CORRUPT_DATA; |
746 | | // LCOV_EXCL_STOP |
747 | 0 | } |
748 | 1.20k | const size_t avail = blk_decomp_sz - skip; |
749 | 1.20k | const size_t copy = (avail < remaining) ? avail : remaining; |
750 | | |
751 | 1.20k | jobs[i].s = s; |
752 | 1.20k | jobs[i].block_idx = bi; |
753 | 1.20k | jobs[i].dst = out; |
754 | 1.20k | jobs[i].skip = skip; |
755 | 1.20k | jobs[i].copy_len = copy; |
756 | 1.20k | jobs[i].result = 0; |
757 | | |
758 | 1.20k | out += copy; |
759 | 1.20k | remaining -= copy; |
760 | 1.20k | } |
761 | | |
762 | | // Launch one persistent worker per thread |
763 | 601 | pthread_t* const threads = (pthread_t*)ZXC_MALLOC((size_t)n_threads * sizeof(pthread_t)); |
764 | 601 | zxc_seek_mt_stripe_t* const stripes = |
765 | 601 | (zxc_seek_mt_stripe_t*)ZXC_MALLOC((size_t)n_threads * sizeof(zxc_seek_mt_stripe_t)); |
766 | 601 | if (UNLIKELY(!threads || !stripes)) { |
767 | | // LCOV_EXCL_START |
768 | 0 | ZXC_FREE(threads); |
769 | 0 | ZXC_FREE(stripes); |
770 | 0 | ZXC_FREE(jobs); |
771 | 0 | return ZXC_ERROR_MEMORY; |
772 | | // LCOV_EXCL_STOP |
773 | 0 | } |
774 | | |
775 | 601 | int launched = 0; |
776 | 1.80k | for (int t = 0; t < n_threads; t++) { |
777 | 1.20k | stripes[t].jobs = jobs; |
778 | 1.20k | stripes[t].num_jobs = num_jobs; |
779 | 1.20k | stripes[t].first = (uint32_t)t; |
780 | 1.20k | stripes[t].stride = (uint32_t)n_threads; |
781 | 1.20k | if (UNLIKELY(pthread_create(&threads[t], NULL, zxc_seek_mt_worker, &stripes[t]) != 0)) { |
782 | | // LCOV_EXCL_START |
783 | | // Failed to create thread - mark its stripe as errored; already |
784 | | // launched workers keep running and are joined below. |
785 | 0 | zxc_seek_mt_fail_stripe(&stripes[t], ZXC_ERROR_MEMORY); |
786 | 0 | continue; |
787 | | // LCOV_EXCL_STOP |
788 | 0 | } |
789 | | |
790 | 1.20k | launched++; |
791 | 1.20k | threads[launched - 1] = threads[t]; |
792 | 1.20k | } |
793 | | |
794 | | // Join phase |
795 | 1.80k | for (int t = 0; t < launched; t++) pthread_join(threads[t], NULL); |
796 | | |
797 | 601 | ZXC_FREE(threads); |
798 | 601 | ZXC_FREE(stripes); |
799 | | |
800 | | // Report the first error in job order, if any. |
801 | 601 | int64_t result = (int64_t)len; |
802 | 1.80k | for (uint32_t i = 0; i < num_jobs; i++) { |
803 | 1.20k | if (jobs[i].result < 0) { |
804 | 0 | result = (int64_t)jobs[i].result; |
805 | 0 | break; |
806 | 0 | } |
807 | 1.20k | } |
808 | | |
809 | 601 | ZXC_FREE(jobs); |
810 | 601 | return result; |
811 | 601 | } |
812 | | |
813 | | /** |
814 | | * @brief Releases a seekable handle and every resource it owns. |
815 | | * |
816 | | * Public API; see @c zxc_seekable.h. Tears down the reusable context, the seek |
817 | | * arrays (comp sizes / offsets), the owned dictionary copy and any attached |
818 | | * reader context. NULL-safe. |
819 | | */ |
820 | 9.45k | void zxc_seekable_free(zxc_seekable* s) { |
821 | 9.45k | if (UNLIKELY(!s)) return; |
822 | 9.45k | if (s->dctx_initialized) zxc_cctx_free(&s->dctx); |
823 | 9.45k | ZXC_FREE(s->dict); |
824 | 9.45k | ZXC_FREE(s->read_buf); |
825 | 9.45k | ZXC_FREE(s->comp_sizes); |
826 | 9.45k | ZXC_FREE(s->comp_offsets); |
827 | 9.45k | ZXC_FREE(s->owned_reader_ctx); |
828 | 9.45k | ZXC_FREE(s); |
829 | 9.45k | } |
830 | | |
831 | | /** |
832 | | * @brief Installs the dictionary needed to decode a dict-compressed archive. |
833 | | * |
834 | | * Public API; full contract in @c zxc_seekable.h. Validates the dict_id against |
835 | | * the file header, then takes an owned copy of @p dict (and the optional shared |
836 | | * literal Huffman table @p dict_huf). Drops any context already built so the |
837 | | * [dict | decode] bounce buffer is re-carved on the next decompress. |
838 | | */ |
839 | | int zxc_seekable_set_dict(zxc_seekable* s, const void* dict, const size_t dict_size, |
840 | 0 | const void* dict_huf) { |
841 | 0 | if (UNLIKELY(!s || !dict || dict_size == 0)) return ZXC_ERROR_NULL_INPUT; |
842 | 0 | if (UNLIKELY(dict_size > ZXC_DICT_SIZE_MAX)) return ZXC_ERROR_DICT_TOO_LARGE; |
843 | 0 | if (UNLIKELY(s->expected_dict_id != 0 && |
844 | 0 | zxc_dict_id(dict, dict_size, (const uint8_t*)dict_huf) != s->expected_dict_id)) |
845 | 0 | return ZXC_ERROR_DICT_MISMATCH; |
846 | | |
847 | 0 | ZXC_FREE(s->dict); |
848 | 0 | s->dict = NULL; |
849 | 0 | s->dict_size = 0; |
850 | 0 | s->has_dict_huf = 0; |
851 | |
|
852 | 0 | s->dict = (uint8_t*)ZXC_MALLOC(dict_size); |
853 | 0 | if (UNLIKELY(!s->dict)) return ZXC_ERROR_MEMORY; |
854 | 0 | ZXC_MEMCPY(s->dict, dict, dict_size); |
855 | 0 | s->dict_size = dict_size; |
856 | 0 | if (dict_huf) { |
857 | 0 | ZXC_MEMCPY(s->dict_huf, dict_huf, ZXC_HUF_TABLE_SIZE); |
858 | 0 | s->has_dict_huf = 1; |
859 | 0 | } |
860 | | |
861 | | // The [dict | decode] bounce buffer is carved into the dctx workspace. |
862 | | // Drop any context built without it (or for a different dict size) so it is |
863 | | // re-carved with the new dict on the next decompress. |
864 | 0 | if (s->dctx_initialized) { |
865 | 0 | zxc_cctx_free(&s->dctx); |
866 | 0 | s->dctx_initialized = 0; |
867 | 0 | } |
868 | 0 | return ZXC_OK; |
869 | 0 | } |
870 | | |
871 | | /** |
872 | | * @brief Transfers ownership of a heap reader context to the handle. |
873 | | * |
874 | | * Cross-TU hook (declared in @c zxc_internal.h): @p ctx is released via |
875 | | * @c ZXC_FREE when @ref zxc_seekable_free runs. Used by |
876 | | * @ref zxc_seekable_open_file so its allocated reader state outlives the open |
877 | | * call. NULL-safe on @p s. |
878 | | */ |
879 | 0 | void zxc_seekable_attach_owned_ctx(zxc_seekable* s, void* ctx) { |
880 | 0 | if (s) s->owned_reader_ctx = ctx; |
881 | 0 | } |