Files
LisherSong--ps5-web-file-ma…/tests/sevenz_e2e.c
T
Songlx516 8bee84bd49 feat(7z): vendor LZMA SDK and add the 7z fixture/verification harness
Groundwork for 7z support (the second of the six format x volume
combinations). No engine code yet: this lands the vendor subset, real
fixtures and the end-to-end driver, so the engine has something to be
built and checked against.

Vendor (third_party/7z, LZMA SDK 26.03, public domain):

- decoder-only subset: 7zArcIn/7zDec container, Lzma/Lzma2/Ppmd7/Bcj2/
  Bra/Delta codecs, Aes/Sha256 for the password channel to come
- the encoder half (LzmaEnc, Xz*, Sort, Threads, ...) is deliberately
  not vendored; the engine never encodes
- README.md records two SDK limitations that shape the engine design:
  * CSzFolder caps a folder at 4 coders / 3 bonds, so 7-Zip's own BCJ2
    chain (BCJ2 + 4xLZMA2 = 5 coders) is refused by SzAr_DecodeFolder
    even though SzArEx_Open parses it happily - the engine therefore
    parses folder blobs and drives the codec chain itself
  * the C decoder ships no 7zAES coder at all, so both -p and -mhe=on
    archives are rejected until the engine implements it on top of the
    vendored Aes.c / Sha256.c

Tests:

- make_sevenz_fixtures.py builds real archives with an actual 7-Zip
  binary (7za from the Extra package; falls back to the reduced 7zr,
  which has no PPMd encoder): store/lzma/lzma2/ppmd/bcj/delta/bcj2,
  AES with plain and with encrypted headers, a 7-part -v100k volume
  set, and UTF-8 entry names
- sevenz_e2e.c extracts an archive and validates every entry against
  the stored CRC; it calls the wide-character file APIs on Windows, so
  non-ASCII entry names are really created instead of silently failing
- run-sevenz-tests.sh builds the subset and the driver, then runs the
  fixture matrix with an explicit KNOWN_GAPS list that fails loudly
  when one of the gaps starts passing

Status: store, lzma, lzma2, ppmd, bcj, delta and utf8 all extract
byte-identical to the source tree (7/10). bcj2, aes, aeshe and
vol.7z.001 are the known gaps, each mapped to a specific piece of
engine work still to come.
2026-09-12 17:17:45 +08:00

303 lines
8.0 KiB
C

/*
* Standalone 7z driver for host end-to-end checks.
*
* sevenz_e2e <archive.7z> <out-dir>
*
* Lists every entry, extracts the archive into <out-dir> and prints the
* per-entry result. Exit status is non-zero if any entry fails.
*
* This is a *diagnostic* tool: it exercises the same container layer as the
* real extraction path, so it is the quickest way to tell whether a failure
* comes from the container/codec layer or from the web server around it.
*
* Entry names are UTF-16 inside a 7z archive; on a Windows host the file APIs
* must therefore be called with the wide-character variants or non-ASCII names
* silently fail to be created.
*/
#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"
#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)
/* MinGW's wchar_t is 16 bit, matching UTF-16. */
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 /* POSIX: byte paths are UTF-8 already */
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;
}
}
/* The final component counts too: callers hand us either a directory entry
or the parent directory of a file. */
path_mkdir(tmp);
}
static int write_file(const char *path, const Byte *data, size_t size)
{
FILE *fh = path_fopen_write(path);
if (!fh)
return -1;
if (size && fwrite(data, 1, size, fh) != size) {
fclose(fh);
return -1;
}
return fclose(fh) == 0 ? 0 : -1;
}
/* 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;
}
/* ---------------------------------------------------------------- main */
int main(int argc, char **argv)
{
CFileInStream archive_stream;
CLookToRead2 look_stream;
CSzArEx db;
SRes res;
UInt16 *name16 = NULL;
size_t name16_cap = 0;
UInt32 i;
UInt32 block_index = 0xFFFFFFFF;
Byte *out_buffer = NULL;
size_t out_buffer_size = 0;
const char *out_dir;
if (argc < 3) {
fprintf(stderr, "usage: %s <archive.7z> <out-dir>\n", argv[0]);
return 2;
}
out_dir = argv[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);
if (res == SZ_ERROR_UNSUPPORTED)
fprintf(stderr, " (unsupported coder - encrypted header or exotic method)\n");
return 1;
}
printf("entries: %u\n", (unsigned)db.NumFiles);
for (i = 0; i < db.NumFiles; i++) {
const int is_dir = SzArEx_IsDir(&db, i);
const size_t len = SzArEx_GetFileNameUtf16(&db, i, NULL);
char rel[MAX_PATH_LEN];
char full[MAX_PATH_LEN];
size_t offset = 0;
size_t processed = 0;
if (len + 1 > name16_cap) {
UInt16 *grown = (UInt16 *)realloc(name16, (len + 1) * sizeof(UInt16));
if (!grown) {
fprintf(stderr, "out of memory\n");
return 1;
}
name16 = grown;
name16_cap = len + 1;
}
SzArEx_GetFileNameUtf16(&db, i, name16);
utf16_to_utf8(name16, rel, sizeof(rel));
if (snprintf(full, sizeof(full), "%s/%s", out_dir, rel) >= (int)sizeof(full)) {
printf(" SKIP (path too long) %s\n", rel);
g_failures++;
continue;
}
if (is_dir) {
make_dirs(full);
printf(" dir %s\n", rel);
continue;
}
res = SzArEx_Extract(&db, &look_stream.vt, i, &block_index, &out_buffer,
&out_buffer_size, &offset, &processed, &g_alloc, &g_temp);
if (res != SZ_OK) {
printf(" FAIL %s (res=%d)\n", rel, (int)res);
g_failures++;
/* SzArEx_Extract leaves the block cache primed with a half-decoded
buffer when the folder decode fails; drop it so the next entry
reports its own error instead of a bogus CRC mismatch. */
block_index = 0xFFFFFFFF;
continue;
}
{
char *slash = strrchr(full, '/');
if (slash) {
*slash = 0;
make_dirs(full);
*slash = '/';
}
}
if (write_file(full, out_buffer + offset, processed) != 0) {
printf(" FAIL %s (write error)\n", rel);
g_failures++;
continue;
}
{
UInt32 crc = 0;
if (SzBitWithVals_Check(&db.CRCs, i)) {
crc = CrcCalc(out_buffer + offset, processed);
if (crc != db.CRCs.Vals[i]) {
printf(" FAIL %s (crc mismatch)\n", rel);
g_failures++;
continue;
}
}
}
printf(" ok %s (%lu bytes)\n", rel, (unsigned long)processed);
}
ISzAlloc_Free(&g_alloc, out_buffer);
ISzAlloc_Free(&g_alloc, look_stream.buf);
SzArEx_Free(&db, &g_alloc);
File_Close(&archive_stream.file);
free(name16);
printf("%s: %d failure(s)\n", argv[1], g_failures);
return g_failures ? 1 : 0;
}