mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 06:00:23 +02:00
feat(7z): decode folders with our own folder header parser and coder chain
The LZMA SDK's CSzFolder caps a folder at four coders, so anything 7-Zip
writes for BCJ2 (4 packed streams + 5 coders) comes back as
SZ_ERROR_UNSUPPORTED from SzAr_DecodeFolder. The header scanner has no such
limit (64 coders), which makes the failure look like corrupt data instead.
src/sevenz_chain.c parses the folder descriptor itself and drives the codec
chain as a pull pipeline: every decoder hands up to N bytes to its consumer
and asks upstream for more when it needs it, so a folder is decoded straight
into the caller's sink without staging the whole thing. Covered here:
- Copy / LZMA / LZMA2 / PPMd, Delta, and the x86, PPC, IA64, ARM, ARMT,
SPARC branch converters
- BCJ2, whose three side streams have to be materialised while MAIN keeps
streaming
- per coder output sizes taken from UNPACK_INFO, which is what keeps the
chains exact
tests/sevenz_chain_e2e.c decodes each folder once and splits the byte stream
across the entries that share it, mirroring the real extraction path, and
checks the per-entry and per-folder CRCs on the way past.
tests/make_sevenz_fixtures.py builds the fixture matrix (including
non-solid `-ms=off` folders), and tests/run-sevenz-tests.sh runs it while
keeping a KNOWN_GAPS list that fails loudly once a gap closes.
Two real bugs surfaced while bringing the matrix up:
- build_coder() gave every coder node the *folder's* unpack size instead of
its own entry in UNPACK_INFO. In a BCJ2 folder MAIN is regularly larger
than the folder's final size (300066 vs 300000 in the fixture), so the
node silently truncated its own output and the chain came up short by 21
bytes per LZMA2 layer. Only archives where a coder is bigger than the
folder tripped it, which is why single-folder BCJ2 passed and the same
archive split into per-file folders did not.
- the e2e driver kept `written` across folders, so a folder that failed
mid-entry made every later entry look half-written and the failures
cascaded.
Scripts no longer delete anything: objects are overwritten and each case gets
a fresh output directory, so the suite runs under a guarded shell.
Matrix: 10/10 single-volume fixtures byte-identical (store, lzma2, lzma,
ppmd, bcj, delta, utf8, bcj2, solidoff, bcj2off). Still open and listed as
known gaps: 7zAES (aes, aeshe) and multi-volume input (vol.7z.001).
This commit is contained in:
1 parent
8bee84bd49
commit
2748b383bd
5 files changed
+2266
-16
No files matched your search
+1600
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,157 @@
|
||||
/* sevenz_chain -- 7z folder (coder chain) decoder.
|
||||
part of ps5-web-file-manager
|
||||
|
||||
A 7z archive stores its data as *folders*. One folder is a small directed
|
||||
graph of coders fed by N packed streams and producing a single unpacked
|
||||
stream; entries are slices of the folder output (a folder holding several
|
||||
entries is what makes an archive "solid").
|
||||
|
||||
The bundled LZMA SDK can decode a folder, but only through `CSzFolder`,
|
||||
which is a fixed-size structure capped at 4 coders / 3 bonds. 7-Zip's own
|
||||
BCJ2 chain uses 5 coders (BCJ2 plus four LZMA2 streams), so the SDK rejects
|
||||
it -- while still listing the archive fine, because its *header* scanner is
|
||||
a different, looser parser (64 coders). The C half of the SDK also has no
|
||||
7zAES coder at all.
|
||||
|
||||
This module therefore parses the folder descriptor itself (dynamic arrays,
|
||||
up to 64 coders, mirroring the SDK's header scanner) and drives the coder
|
||||
graph itself. Two properties matter:
|
||||
|
||||
* Streaming. The decoded bytes are pushed into a sink as they are
|
||||
produced; a folder is never materialised as a whole, so a multi-gigabyte
|
||||
solid block is workable. The only buffers sized from the archive are
|
||||
the LZMA/LZMA2 dictionary, the PPMd model and the three side streams of
|
||||
BCJ2 -- each capped by sz_chain_limits_t.
|
||||
|
||||
* Precision. Every rejection names the coder and the method, and the
|
||||
resource limits report the value the archive asked for and the value
|
||||
that was allowed, so the UI can say something useful instead of
|
||||
"corrupt archive".
|
||||
|
||||
Not supported here (by design, see sz_chain_check):
|
||||
* 7zAES (method 0x06F10701) -- needs its own implementation, it is not in
|
||||
the SDK's C decoder. Reported as SZ_CHAIN_ERR_METHOD.
|
||||
*/
|
||||
|
||||
#ifndef SEVENZ_CHAIN_H
|
||||
#define SEVENZ_CHAIN_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/* The SDK's header scanner accepts up to 64 coders per folder; folders in the
|
||||
wild have 1-5. Keeping the same ceiling means "the SDK could list it" and
|
||||
"we can decode it" accept the same archives. */
|
||||
#define SZ_CHAIN_MAX_CODERS 64
|
||||
#define SZ_CHAIN_MAX_STREAMS 64
|
||||
|
||||
/* Ceilings for the buffers whose size comes from the (attacker controlled)
|
||||
archive header. */
|
||||
typedef struct {
|
||||
uint64_t max_dict_bytes; /* LZMA / LZMA2 window */
|
||||
uint64_t max_ppmd_bytes; /* PPMd model */
|
||||
uint64_t max_side_bytes; /* BCJ2 CALL + JUMP + RC together */
|
||||
} sz_chain_limits_t;
|
||||
|
||||
#define SZ_CHAIN_LIMITS_DEFAULT 0
|
||||
#define SZ_CHAIN_LIMITS_LARGE 1
|
||||
const sz_chain_limits_t *sz_chain_limits_profile(int profile);
|
||||
const sz_chain_limits_t *sz_chain_default_limits(void);
|
||||
|
||||
typedef enum {
|
||||
SZ_CHAIN_OK = 0,
|
||||
SZ_CHAIN_ERR_PARAM, /* bad arguments from the caller */
|
||||
SZ_CHAIN_ERR_MEM, /* allocation failed */
|
||||
SZ_CHAIN_ERR_HEADER, /* malformed folder descriptor */
|
||||
SZ_CHAIN_ERR_METHOD, /* coder method not supported */
|
||||
SZ_CHAIN_ERR_LAYOUT, /* coder graph shape not supported */
|
||||
SZ_CHAIN_ERR_LIMIT, /* a sz_chain_limits_t ceiling was hit */
|
||||
SZ_CHAIN_ERR_READ, /* the read callback failed */
|
||||
SZ_CHAIN_ERR_WRITE, /* the sink callback failed */
|
||||
SZ_CHAIN_ERR_DATA, /* a decoder rejected the data */
|
||||
SZ_CHAIN_ERR_CANCELED,
|
||||
SZ_CHAIN_ERR_INTERNAL
|
||||
} sz_chain_status_t;
|
||||
|
||||
typedef struct {
|
||||
sz_chain_status_t status;
|
||||
int32_t coder; /* index of the offending coder, -1 when not applicable */
|
||||
uint32_t method; /* its method id, 0 when not applicable */
|
||||
uint64_t offset; /* decoded byte offset at the point of failure */
|
||||
char message[192];
|
||||
} sz_chain_err_t;
|
||||
|
||||
const char *sz_chain_status_string(sz_chain_status_t status);
|
||||
|
||||
/* "LZMA2", "BCJ2", "7zAES", "unknown 0x1234". Never returns NULL. */
|
||||
const char *sz_chain_method_name(uint32_t method);
|
||||
|
||||
/* ---------------------------------------------------------------- folder */
|
||||
|
||||
typedef struct sz_chain sz_chain;
|
||||
|
||||
/* Parses one folder descriptor.
|
||||
|
||||
blob / blob_size
|
||||
the CODERS_INFO bytes of this folder, i.e. the range
|
||||
`CSzAr::CodersData[FoCodersOffsets[i] .. FoCodersOffsets[i + 1])`.
|
||||
pack_positions
|
||||
`CSzAr::PackPositions`, num_pack_streams + 1 entries, offsets of the
|
||||
packed streams relative to the start of the archive's packed-data area.
|
||||
coder_unpack_sizes
|
||||
unpacked size of every coder of this folder, in stored coder order, i.e.
|
||||
`&CSzAr::CoderUnpackSizes[CSzAr::FoToCoderUnpackSizes[i]]`.
|
||||
unpack_size
|
||||
`SzAr_GetFolderUnpackSize(&db, i)`.
|
||||
|
||||
Returns 0 on success. On success *out owns a copy of blob, release it with
|
||||
sz_chain_free(). On failure *out is untouched and err describes the
|
||||
problem. */
|
||||
int sz_chain_parse(sz_chain **out, const uint8_t *blob, size_t blob_size,
|
||||
const uint64_t *pack_positions, uint32_t num_pack_streams,
|
||||
const uint64_t *coder_unpack_sizes, uint64_t unpack_size,
|
||||
const sz_chain_limits_t *limits, sz_chain_err_t *err);
|
||||
|
||||
void sz_chain_free(sz_chain *c);
|
||||
|
||||
uint32_t sz_chain_num_coders(const sz_chain *c);
|
||||
uint32_t sz_chain_num_pack_streams(const sz_chain *c);
|
||||
/* Method id of coder `index`, or -1 when out of range. */
|
||||
int64_t sz_chain_coder_method(const sz_chain *c, uint32_t index);
|
||||
|
||||
/* Writes e.g. "LZMA2 + BCJ2 (5 coders, 4 pack streams)" into buf. */
|
||||
void sz_chain_describe(const sz_chain *c, char *buf, size_t size);
|
||||
|
||||
/* Walks every coder and the graph shape without touching any data, so callers
|
||||
can refuse an archive before creating anything on disk. Fills err with the
|
||||
same precision sz_chain_decode() would. */
|
||||
int sz_chain_check(const sz_chain *c, sz_chain_err_t *err);
|
||||
|
||||
/* -------------------------------------------------------------- decoding */
|
||||
|
||||
/* Fills exactly size bytes at offset inside the packed-data area.
|
||||
Returns 0 on success, non-zero on failure. */
|
||||
typedef int (*sz_chain_read_fn)(void *ctx, uint64_t offset, void *dst,
|
||||
size_t size);
|
||||
|
||||
/* Receives the decoded bytes in order. Returns 0 to continue. */
|
||||
typedef int (*sz_chain_sink_fn)(void *ctx, const void *data, size_t size);
|
||||
|
||||
/* Returns non-zero to abort. May be NULL. */
|
||||
typedef int (*sz_chain_cancel_fn)(void *ctx);
|
||||
|
||||
/* Decodes the whole folder, pushing the result into sink.
|
||||
|
||||
The bytes are delivered strictly in order and the total is the folder's
|
||||
declared unpack size. When crc_out is not NULL it receives the CRC-32 of
|
||||
the delivered bytes, for the caller to compare with the folder CRC.
|
||||
|
||||
Returns 0 on success, -1 on failure with err filled. A failing sink is
|
||||
reported as SZ_CHAIN_ERR_WRITE; the caller is expected to make its own
|
||||
message more specific. */
|
||||
int sz_chain_decode(sz_chain *c, sz_chain_read_fn read_at, void *read_ctx,
|
||||
sz_chain_sink_fn sink, void *sink_ctx,
|
||||
sz_chain_cancel_fn cancel, void *cancel_ctx,
|
||||
uint32_t *crc_out, sz_chain_err_t *err);
|
||||
|
||||
#endif /* SEVENZ_CHAIN_H */
|
||||
@@ -176,6 +176,11 @@ VARIANTS: list[tuple[str, list[str], bool, bool]] = [
|
||||
("bcj2", ["-m0=bcj2", "-m1=lzma2", "-m2=lzma2", "-m3=lzma2", "-m4=lzma2"], False, True),
|
||||
("aes", ["-m0=lzma2", "-mx5"], True, True),
|
||||
("utf8", ["-m0=lzma2", "-mx5"], False, True),
|
||||
# Not solid: one folder per file. That is what exercises the multi-folder
|
||||
# walk and the per-folder slice of the packed-stream table.
|
||||
("solidoff", ["-m0=lzma2", "-mx5", "-ms=off"], False, True),
|
||||
("bcj2off", ["-m0=bcj2", "-m1=lzma2", "-m2=lzma2", "-m3=lzma2",
|
||||
"-m4=lzma2", "-ms=off"], False, True),
|
||||
]
|
||||
|
||||
|
||||
|
||||
+29
-16
@@ -1,11 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# Host test runner for the 7z engine (vendor subset + fixture matrix).
|
||||
# Host test runner for the 7z engine.
|
||||
#
|
||||
# ./tests/run-sevenz-tests.sh
|
||||
#
|
||||
# Builds the vendored LZMA SDK subset and the end-to-end driver, generates real
|
||||
# .7z fixtures with a 7-Zip binary, extracts every fixture and compares the
|
||||
# result byte for byte against the source tree.
|
||||
# Builds the vendored LZMA SDK subset plus the project's own folder decoder
|
||||
# (src/sevenz_chain.c), generates real .7z fixtures with a 7-Zip binary,
|
||||
# extracts every fixture through that decoder and compares the result byte for
|
||||
# byte against the source tree.
|
||||
#
|
||||
# On Windows this expects MinGW gcc in PATH and must be started with the MSYS
|
||||
# bash explicitly (`/usr/bin/bash tests/run-sevenz-tests.sh`) -- a bare `bash`
|
||||
@@ -20,10 +21,9 @@ FIXTURES="$ROOT/tests/fixtures-7z"
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
CC="${CC:-gcc}"
|
||||
|
||||
# Archives that the current engine cannot read yet. Each entry needs a reason;
|
||||
# when one of them starts passing the script says so, so the list cannot rot.
|
||||
KNOWN_GAPS="bcj2 aes aeshe vol.7z.001"
|
||||
# bcj2 - folder has 5 coders; needs the engine's own chain driver
|
||||
# Archives the engine cannot read yet. Each entry needs a reason; when one of
|
||||
# them starts passing the script says so, so the list cannot rot.
|
||||
KNOWN_GAPS="aes aeshe vol.7z.001"
|
||||
# aes - 7zAES coder not implemented yet
|
||||
# aeshe - encrypted header (-mhe=on), same coder
|
||||
# vol.7z.001 - multi-volume input; needs the volume-set stream
|
||||
@@ -35,19 +35,31 @@ mkdir -p "$BUILD"
|
||||
CFLAGS_7Z=(-O2 -w -DZ7_PPMD_SUPPORT -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
|
||||
-DNDEBUG -D_REENTRANT)
|
||||
|
||||
VENDOR_OBJS=()
|
||||
for src in "$SEVENZ_DIR"/*.c; do
|
||||
name="$(basename "$src" .c)"
|
||||
"$CC" -c "${CFLAGS_7Z[@]}" -o "$BUILD/$name.o" "$src"
|
||||
VENDOR_OBJS+=("$BUILD/$name.o")
|
||||
done
|
||||
|
||||
# The engine module is held to the same strictness as the rest of src/.
|
||||
"$CC" -c -O2 -Wall -Wextra -Werror -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE \
|
||||
-I"$SEVENZ_DIR" -o "$BUILD/sevenz_chain.o" "$ROOT/src/sevenz_chain.c"
|
||||
|
||||
# unrar-style extra libs are only needed by the Windows path of 7zFile.c.
|
||||
EXTRA_LIBS=()
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*) EXTRA_LIBS=(-lole32 -loleaut32 -luuid -ladvapi32 -luser32 -lshell32) ;;
|
||||
esac
|
||||
|
||||
"$CC" -O2 -w -I"$SEVENZ_DIR" -I"$ROOT/src" -o "$BUILD/sevenz_chain_e2e" \
|
||||
"$ROOT/tests/sevenz_chain_e2e.c" "$BUILD/sevenz_chain.o" "${VENDOR_OBJS[@]}" \
|
||||
"${EXTRA_LIBS[@]}"
|
||||
|
||||
# The SDK-baseline driver is kept buildable: it is the fastest way to tell an
|
||||
# engine bug from an SDK one when a fixture starts failing.
|
||||
"$CC" -O2 -w -I"$SEVENZ_DIR" -o "$BUILD/sevenz_e2e" \
|
||||
"$ROOT/tests/sevenz_e2e.c" "$BUILD"/*.o "${EXTRA_LIBS[@]}"
|
||||
"$ROOT/tests/sevenz_e2e.c" "${VENDOR_OBJS[@]}" "${EXTRA_LIBS[@]}"
|
||||
|
||||
# ---------------------------------------------------------------- fixtures
|
||||
if [ ! -f "$FIXTURES/lzma2.7z" ]; then
|
||||
@@ -71,13 +83,14 @@ is_gap() {
|
||||
|
||||
run_case() {
|
||||
local name="$1" archive="$2"
|
||||
local out="$BUILD/out/$name"
|
||||
local log="$BUILD/out/$name.log"
|
||||
local out log
|
||||
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out"
|
||||
# A fresh directory per case keeps the run repeatable without deleting a tree
|
||||
# of previous results, which guarded shells refuse to do.
|
||||
out="$(mktemp -d "$BUILD/out/XXXXXX")" || return
|
||||
log="$out.log"
|
||||
|
||||
if "$BUILD/sevenz_e2e" "$archive" "$out" >"$log" 2>&1 &&
|
||||
if "$BUILD/sevenz_chain_e2e" "$archive" "$out" >"$log" 2>&1 &&
|
||||
diff -r "$FIXTURES/_src" "$out/_src" >/dev/null 2>&1; then
|
||||
if is_gap "$name"; then
|
||||
printf ' %-12s GAP CLOSED (remove from KNOWN_GAPS)\n' "$name"
|
||||
@@ -99,8 +112,8 @@ run_case() {
|
||||
fi
|
||||
}
|
||||
|
||||
echo "== 7z fixture matrix =="
|
||||
for a in store lzma2 lzma ppmd bcj delta utf8 bcj2 aes aeshe; do
|
||||
echo "== 7z fixture matrix (engine: src/sevenz_chain.c) =="
|
||||
for a in store lzma2 lzma ppmd bcj delta utf8 bcj2 solidoff bcj2off aes aeshe; do
|
||||
[ -f "$FIXTURES/$a.7z" ] || continue
|
||||
run_case "$a" "$FIXTURES/$a.7z"
|
||||
done
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
* End-to-end driver for the project's own 7z folder decoder (src/sevenz_chain.c).
|
||||
*
|
||||
* sevenz_chain_e2e <archive.7z> <out-dir>
|
||||
*
|
||||
* Unlike tests/sevenz_e2e.c -- which drives the LZMA SDK's own CSzFolder based
|
||||
* path and therefore cannot handle BCJ2 or anything else the SDK caps at four
|
||||
* coders -- this one decodes *every* folder through src/sevenz_chain.c.
|
||||
*
|
||||
* It also mirrors the shape of the real extraction path: each folder is
|
||||
* decoded once, streamed to a sink, and the sink splits the byte stream across
|
||||
* the entries that live in that folder (that is what makes an archive solid).
|
||||
* Per-entry and per-folder CRCs are verified as the bytes go past.
|
||||
*
|
||||
* Exit status is non-zero if any folder or entry failed.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <direct.h>
|
||||
#include <wchar.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "7z.h"
|
||||
#include "7zAlloc.h"
|
||||
#include "7zCrc.h"
|
||||
#include "7zFile.h"
|
||||
|
||||
#include "sevenz_chain.h"
|
||||
|
||||
#define INPUT_BUF_SIZE (1u << 18)
|
||||
#define MAX_PATH_LEN 4096
|
||||
|
||||
static ISzAlloc g_alloc = { SzAlloc, SzFree };
|
||||
static ISzAlloc g_temp = { SzAlloc, SzFree };
|
||||
|
||||
static int g_failures = 0;
|
||||
|
||||
/* ---------------------------------------------------------------- paths */
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
static void utf8_to_utf16(const char *src, wchar_t *dst, size_t cap) {
|
||||
size_t out = 0;
|
||||
|
||||
while(*src && out + 2 < cap) {
|
||||
unsigned char c = (unsigned char)*src++;
|
||||
UInt32 cp;
|
||||
|
||||
if(c < 0x80) {
|
||||
cp = c;
|
||||
} else if((c & 0xE0) == 0xC0 && (src[0] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x1F) << 6) | (UInt32)(*src++ & 0x3F);
|
||||
} else if((c & 0xF0) == 0xE0 && (src[0] & 0xC0) == 0x80 &&
|
||||
(src[1] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x0F) << 12) | ((UInt32)(src[0] & 0x3F) << 6) |
|
||||
(UInt32)(src[1] & 0x3F);
|
||||
src += 2;
|
||||
} else if((c & 0xF8) == 0xF0 && (src[0] & 0xC0) == 0x80 &&
|
||||
(src[1] & 0xC0) == 0x80 && (src[2] & 0xC0) == 0x80) {
|
||||
cp = ((UInt32)(c & 0x07) << 18) | ((UInt32)(src[0] & 0x3F) << 12) |
|
||||
((UInt32)(src[1] & 0x3F) << 6) | (UInt32)(src[2] & 0x3F);
|
||||
src += 3;
|
||||
} else {
|
||||
cp = '?';
|
||||
}
|
||||
|
||||
if(cp >= 0x10000) {
|
||||
cp -= 0x10000;
|
||||
dst[out++] = (wchar_t)(0xD800 | (cp >> 10));
|
||||
dst[out++] = (wchar_t)(0xDC00 | (cp & 0x3FF));
|
||||
} else {
|
||||
dst[out++] = (wchar_t)cp;
|
||||
}
|
||||
}
|
||||
dst[out] = 0;
|
||||
}
|
||||
|
||||
static void path_mkdir(const char *path) {
|
||||
wchar_t wide[MAX_PATH_LEN];
|
||||
utf8_to_utf16(path, wide, MAX_PATH_LEN);
|
||||
_wmkdir(wide);
|
||||
}
|
||||
|
||||
static FILE *path_fopen_write(const char *path) {
|
||||
wchar_t wide[MAX_PATH_LEN];
|
||||
utf8_to_utf16(path, wide, MAX_PATH_LEN);
|
||||
return _wfopen(wide, L"wb");
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static void path_mkdir(const char *path) { mkdir(path, 0755); }
|
||||
static FILE *path_fopen_write(const char *path) { return fopen(path, "wb"); }
|
||||
|
||||
#endif
|
||||
|
||||
static void make_dirs(const char *path) {
|
||||
char tmp[MAX_PATH_LEN];
|
||||
size_t i, n = strlen(path);
|
||||
|
||||
if(n + 1 > sizeof(tmp)) return;
|
||||
memcpy(tmp, path, n + 1);
|
||||
|
||||
for(i = 1; i < n; i++) {
|
||||
if(tmp[i] == '/' || tmp[i] == '\\') {
|
||||
char c = tmp[i];
|
||||
tmp[i] = 0;
|
||||
path_mkdir(tmp);
|
||||
tmp[i] = c;
|
||||
}
|
||||
}
|
||||
path_mkdir(tmp);
|
||||
}
|
||||
|
||||
/* UTF-16 (LE, as stored by the 7z name table) to UTF-8. */
|
||||
static void utf16_to_utf8(const UInt16 *src, char *dst, size_t dst_size) {
|
||||
size_t out = 0;
|
||||
|
||||
while(*src) {
|
||||
UInt32 c = *src++;
|
||||
|
||||
if(c >= 0xD800 && c <= 0xDBFF && *src >= 0xDC00 && *src <= 0xDFFF)
|
||||
c = 0x10000 + ((c - 0xD800) << 10) + (*src++ - 0xDC00);
|
||||
if(out + 5 >= dst_size) break;
|
||||
|
||||
if(c < 0x80) {
|
||||
dst[out++] = (char)c;
|
||||
} else if(c < 0x800) {
|
||||
dst[out++] = (char)(0xC0 | (c >> 6));
|
||||
dst[out++] = (char)(0x80 | (c & 0x3F));
|
||||
} else if(c < 0x10000) {
|
||||
dst[out++] = (char)(0xE0 | (c >> 12));
|
||||
dst[out++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
dst[out++] = (char)(0x80 | (c & 0x3F));
|
||||
} else {
|
||||
dst[out++] = (char)(0xF0 | (c >> 18));
|
||||
dst[out++] = (char)(0x80 | ((c >> 12) & 0x3F));
|
||||
dst[out++] = (char)(0x80 | ((c >> 6) & 0x3F));
|
||||
dst[out++] = (char)(0x80 | (c & 0x3F));
|
||||
}
|
||||
}
|
||||
dst[out] = 0;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- reader */
|
||||
|
||||
typedef struct {
|
||||
CSzFile *file;
|
||||
UInt64 base; /* db.dataPos: packed offsets are relative to it */
|
||||
} reader_ctx;
|
||||
|
||||
static int reader_at(void *ctx, uint64_t offset, void *dst, size_t size) {
|
||||
reader_ctx *r = (reader_ctx *)ctx;
|
||||
UInt64 pos = r->base + offset;
|
||||
size_t done = 0;
|
||||
|
||||
if(File_Seek(r->file, (Int64 *)&pos, SZ_SEEK_SET) != 0) return -1;
|
||||
while(done < size) {
|
||||
size_t want = size - done;
|
||||
if(File_Read(r->file, (Byte *)dst + done, &want) != 0) return -1;
|
||||
if(want == 0) return -1;
|
||||
done += want;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------ folder -> sink */
|
||||
|
||||
typedef struct {
|
||||
UInt32 file_index;
|
||||
uint64_t size;
|
||||
} plan_entry;
|
||||
|
||||
typedef struct {
|
||||
const CSzArEx *db;
|
||||
char out_dir[MAX_PATH_LEN];
|
||||
char name[MAX_PATH_LEN];
|
||||
|
||||
plan_entry *plan;
|
||||
size_t plan_len;
|
||||
size_t plan_pos;
|
||||
uint64_t written;
|
||||
FILE *fh;
|
||||
|
||||
uint32_t entry_crc;
|
||||
uint64_t bytes_total;
|
||||
uint64_t bytes_ok;
|
||||
int failures;
|
||||
} sink_ctx;
|
||||
|
||||
static void plan_free(sink_ctx *s) {
|
||||
free(s->plan);
|
||||
s->plan = NULL;
|
||||
s->plan_len = s->plan_pos = 0;
|
||||
}
|
||||
|
||||
/* Builds the ordered list of non-empty entries living in folder `folder`. */
|
||||
static int plan_build(sink_ctx *s, UInt32 folder, sz_chain_err_t *cerr) {
|
||||
const CSzArEx *db = s->db;
|
||||
UInt32 first = db->FolderToFile[folder];
|
||||
UInt32 last = db->FolderToFile[(size_t)folder + 1];
|
||||
UInt32 i;
|
||||
|
||||
plan_free(s);
|
||||
/* A folder that failed mid-entry leaves `written` pointing into an entry it
|
||||
never finished; carrying that into the next folder makes every later entry
|
||||
look "already part written" and produces a cascade of bogus failures. */
|
||||
s->written = 0;
|
||||
if(last <= first) return 0;
|
||||
s->plan = (plan_entry *)malloc(sizeof(plan_entry) * (size_t)(last - first));
|
||||
if(!s->plan) return -1;
|
||||
|
||||
for(i = first; i < last; i++) {
|
||||
UInt64 size = db->UnpackPositions[(size_t)i + 1] - db->UnpackPositions[i];
|
||||
if(db->FileToFolder[i] != folder) continue;
|
||||
if(size == 0) continue;
|
||||
s->plan[s->plan_len].file_index = i;
|
||||
s->plan[s->plan_len].size = (uint64_t)size;
|
||||
s->plan_len++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void sink_close_entry(sink_ctx *s) {
|
||||
if(s->fh) {
|
||||
fclose(s->fh);
|
||||
s->fh = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static int sink_open_entry(sink_ctx *s, UInt32 file_index) {
|
||||
const CSzArEx *db = s->db;
|
||||
UInt16 *name16 = NULL;
|
||||
size_t len = SzArEx_GetFileNameUtf16(db, file_index, NULL);
|
||||
char rel[MAX_PATH_LEN];
|
||||
char full[MAX_PATH_LEN];
|
||||
char *slash;
|
||||
|
||||
name16 = (UInt16 *)malloc((len + 1) * sizeof(UInt16));
|
||||
if(!name16) return -1;
|
||||
SzArEx_GetFileNameUtf16(db, file_index, name16);
|
||||
utf16_to_utf8(name16, rel, sizeof(rel));
|
||||
free(name16);
|
||||
|
||||
if(snprintf(full, sizeof(full), "%s/%s", s->out_dir, rel) >=
|
||||
(int)sizeof(full)) {
|
||||
printf(" FAIL (path too long) %s\n", rel);
|
||||
return -1;
|
||||
}
|
||||
slash = strrchr(full, '/');
|
||||
if(slash) {
|
||||
*slash = 0;
|
||||
make_dirs(full);
|
||||
*slash = '/';
|
||||
}
|
||||
s->fh = path_fopen_write(full);
|
||||
if(!s->fh) {
|
||||
printf(" FAIL (cannot create) %s\n", rel);
|
||||
return -1;
|
||||
}
|
||||
s->entry_crc = CRC_INIT_VAL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int sink_write(void *ctx, const void *data, size_t size) {
|
||||
sink_ctx *s = (sink_ctx *)ctx;
|
||||
const Byte *p = (const Byte *)data;
|
||||
|
||||
while(size > 0) {
|
||||
plan_entry *e;
|
||||
uint64_t remain;
|
||||
size_t take;
|
||||
char rel[MAX_PATH_LEN];
|
||||
|
||||
if(s->plan_pos >= s->plan_len) {
|
||||
printf(" FAIL folder produced %llu bytes more than its entries hold\n",
|
||||
(unsigned long long)size);
|
||||
s->failures++;
|
||||
return -1;
|
||||
}
|
||||
e = &s->plan[s->plan_pos];
|
||||
remain = e->size - s->written;
|
||||
take = (size_t)((uint64_t)size < remain ? (uint64_t)size : remain);
|
||||
|
||||
if(!s->fh) {
|
||||
if(sink_open_entry(s, e->file_index) != 0) {
|
||||
s->failures++;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if(take && fwrite(p, 1, take, s->fh) != take) {
|
||||
printf(" FAIL (write error)\n");
|
||||
s->failures++;
|
||||
return -1;
|
||||
}
|
||||
s->entry_crc = CrcUpdate(s->entry_crc, p, take);
|
||||
s->written += take;
|
||||
s->bytes_total += take;
|
||||
p += take;
|
||||
size -= take;
|
||||
|
||||
if(s->written == e->size) {
|
||||
UInt16 *name16;
|
||||
size_t len;
|
||||
sink_close_entry(s);
|
||||
len = SzArEx_GetFileNameUtf16(s->db, e->file_index, NULL);
|
||||
name16 = (UInt16 *)malloc((len + 1) * sizeof(UInt16));
|
||||
if(name16) {
|
||||
SzArEx_GetFileNameUtf16(s->db, e->file_index, name16);
|
||||
utf16_to_utf8(name16, rel, sizeof(rel));
|
||||
free(name16);
|
||||
if(SzBitWithVals_Check(&s->db->CRCs, e->file_index) &&
|
||||
CRC_GET_DIGEST(s->entry_crc) != s->db->CRCs.Vals[e->file_index]) {
|
||||
printf(" FAIL %s (crc mismatch)\n", rel);
|
||||
s->failures++;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
s->bytes_ok += e->size;
|
||||
s->plan_pos++;
|
||||
s->written = 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ main */
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
CFileInStream archive_stream;
|
||||
CLookToRead2 look_stream;
|
||||
CSzArEx db;
|
||||
SRes res;
|
||||
UInt32 folder;
|
||||
reader_ctx reader;
|
||||
sink_ctx sink;
|
||||
int rc = 0;
|
||||
|
||||
if(argc < 3) {
|
||||
fprintf(stderr, "usage: %s <archive.7z> <out-dir>\n", argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if(InFile_Open(&archive_stream.file, argv[1]) != 0) {
|
||||
fprintf(stderr, "cannot open %s\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
FileInStream_CreateVTable(&archive_stream);
|
||||
archive_stream.wres = 0;
|
||||
|
||||
LookToRead2_CreateVTable(&look_stream, 0);
|
||||
look_stream.buf = (Byte *)ISzAlloc_Alloc(&g_alloc, INPUT_BUF_SIZE);
|
||||
if(!look_stream.buf) {
|
||||
fprintf(stderr, "out of memory\n");
|
||||
return 1;
|
||||
}
|
||||
look_stream.bufSize = INPUT_BUF_SIZE;
|
||||
look_stream.realStream = &archive_stream.vt;
|
||||
LookToRead2_INIT(&look_stream)
|
||||
|
||||
CrcGenerateTable();
|
||||
SzArEx_Init(&db);
|
||||
|
||||
res = SzArEx_Open(&db, &look_stream.vt, &g_alloc, &g_temp);
|
||||
if(res != SZ_OK) {
|
||||
fprintf(stderr, "SzArEx_Open failed: res=%d\n", (int)res);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("entries: %u, folders: %u, packed streams: %u\n",
|
||||
(unsigned)db.NumFiles, (unsigned)db.db.NumFolders,
|
||||
(unsigned)db.db.NumPackStreams);
|
||||
|
||||
memset(&sink, 0, sizeof(sink));
|
||||
sink.db = &db;
|
||||
snprintf(sink.out_dir, sizeof(sink.out_dir), "%s", argv[2]);
|
||||
|
||||
reader.file = &archive_stream.file;
|
||||
reader.base = db.dataPos;
|
||||
|
||||
for(folder = 0; folder < db.db.NumFolders; folder++) {
|
||||
const UInt32 pack_first = db.db.FoStartPackStreamIndex[folder];
|
||||
const UInt32 pack_count = db.db.FoStartPackStreamIndex[(size_t)folder + 1] -
|
||||
pack_first;
|
||||
uint64_t pack_positions[SZ_CHAIN_MAX_STREAMS + 1];
|
||||
sz_chain *chain = NULL;
|
||||
sz_chain_err_t cerr;
|
||||
char desc[256];
|
||||
uint32_t folder_crc = 0;
|
||||
UInt32 k;
|
||||
uint64_t unpack_size = SzAr_GetFolderUnpackSize(&db.db, folder);
|
||||
const uint8_t *blob = db.db.CodersData + db.db.FoCodersOffsets[folder];
|
||||
size_t blob_size = db.db.FoCodersOffsets[(size_t)folder + 1] -
|
||||
db.db.FoCodersOffsets[folder];
|
||||
const uint64_t *cu =
|
||||
&db.db.CoderUnpackSizes[db.db.FoToCoderUnpackSizes[folder]];
|
||||
|
||||
for(k = 0; k <= pack_count; k++)
|
||||
pack_positions[k] = db.db.PackPositions[pack_first + k];
|
||||
|
||||
sink.bytes_total = 0;
|
||||
sink.bytes_ok = 0;
|
||||
if(plan_build(&sink, folder, &cerr) != 0) {
|
||||
fprintf(stderr, "folder %u: cannot build the entry plan\n",
|
||||
(unsigned)folder);
|
||||
rc = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(sz_chain_parse(&chain, blob, blob_size, pack_positions, pack_count, cu,
|
||||
unpack_size, sz_chain_default_limits(), &cerr) != 0) {
|
||||
printf("folder %-2u FAIL parse: %s: %s\n", (unsigned)folder,
|
||||
sz_chain_status_string(cerr.status), cerr.message);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
plan_free(&sink);
|
||||
continue;
|
||||
}
|
||||
sz_chain_describe(chain, desc, sizeof(desc));
|
||||
|
||||
{
|
||||
sz_chain_err_t derr;
|
||||
if(sz_chain_decode(chain, reader_at, &reader, sink_write, &sink, NULL,
|
||||
NULL, &folder_crc, &derr) != 0) {
|
||||
printf("folder %-2u FAIL decode [%s]: %s: %s (offset %llu)\n",
|
||||
(unsigned)folder, desc, sz_chain_status_string(derr.status),
|
||||
derr.message, (unsigned long long)derr.offset);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
} else if(sink.bytes_total != unpack_size) {
|
||||
printf("folder %-2u FAIL size: %llu decoded, %llu declared\n",
|
||||
(unsigned)folder, (unsigned long long)sink.bytes_total,
|
||||
(unsigned long long)unpack_size);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
} else if(SzBitWithVals_Check(&db.db.FolderCRCs, folder) &&
|
||||
folder_crc != db.db.FolderCRCs.Vals[folder]) {
|
||||
printf("folder %-2u FAIL crc: got %08X, expected %08X\n",
|
||||
(unsigned)folder, (unsigned)folder_crc,
|
||||
(unsigned)db.db.FolderCRCs.Vals[folder]);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
} else {
|
||||
printf("folder %-2u ok [%s] %llu bytes, %u entr%s\n",
|
||||
(unsigned)folder, desc, (unsigned long long)sink.bytes_total,
|
||||
(unsigned)sink.plan_len, sink.plan_len == 1 ? "y" : "ies");
|
||||
}
|
||||
}
|
||||
|
||||
if(sink.plan_pos != sink.plan_len) {
|
||||
printf("folder %-2u FAIL only %u of %u entries were produced\n",
|
||||
(unsigned)folder, (unsigned)sink.plan_pos,
|
||||
(unsigned)sink.plan_len);
|
||||
g_failures++;
|
||||
rc = 1;
|
||||
}
|
||||
sink_close_entry(&sink);
|
||||
sz_chain_free(chain);
|
||||
plan_free(&sink);
|
||||
}
|
||||
|
||||
ISzAlloc_Free(&g_alloc, look_stream.buf);
|
||||
SzArEx_Free(&db, &g_alloc);
|
||||
File_Close(&archive_stream.file);
|
||||
|
||||
printf("%s: %d failure(s)\n", argv[1], g_failures);
|
||||
return rc ? 1 : 0;
|
||||
}
|
||||
Reference in new issue
Block a user