mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 06:00:23 +02:00
feat(7z): extraction facade + format dispatch + UTF-8 host shim
The 7z chain decoder (commit 2748b38) handles bytes; what the /api/extract
task needed was a third sibling of zip_extract.c and rar_extract.c that
runs that chain over a real archive, mirrors their staging / publish /
rollback / name-validation machinery, and fills in zipx_result_t the same
way so the dispatcher and the web UI can treat every format alike.
* src/sevenz_extract.[ch] -- one pass over the archive (scan: validate every
name, sum bytes, drop archives the chain cannot drive), then folder by
folder through the chain. The publish phase recurses with OVERWRITE and
MERGE alike for matching directories, and only differs at the file
leaves -- a strict non-recursive OVERWRITE would have made the policy
useless for any archive that overlaps a directory already on disk.
* src/zipx_common.c -- the limits profiles and zipx_status_string() that
the three engines share. Pulled out of zip_extract.c so every consumer
gets the same error text and the same MAX_* numbers.
* src/zip_extract.[ch] -- ZIPX_ERR_PASSWORD joined the contract (7zAES).
* tests/run-sevenz-tests.sh -- the engine matrix now also runs the facade
over every fixture, plus 22 error and policy checks against
test_sevenz_extract. KNOWN_GAPS holds only "aeshe" (encrypted header);
the plain 7zAES fixture is in the matrix.
* tests/test_sevenz_extract.c -- end-to-end driver for the facade.
The POSIX shim the host test runner injects needed three more pieces so
the tests pass on a CP936 host:
* lstat/stat -> wfm_stat (the MinGW ANSI entry points can't see a UTF-8
filename in CP936; without the redirect a non-ASCII entry causes the
publish phase to lstat() a mangled path and fail with ENOENT).
* opendir/readdir/closedir -> the wide variants, so readdir() hands us
real UTF-8 names instead of CP936 bytes that nothing can round-trip.
* fopen -> _wfopen, for the test helpers that read a non-ASCII file
back to verify it.
ZIP 108 checks / RAR 27 checks / 7z 26 checks, all green; the AES
fixture extracts with the password "Secret123" and rejects wrong / absent
passwords with "password required or wrong" / "wrong password, or the
archive is damaged".
tests/fixtures/ gains the volume-set fixtures (broken.zip.001, gap.zip.*,
disks.*, parts.part*.zip, plain.zip.*, split_single.zip) that were
generated by tests/make_split_fixtures.py in earlier sessions but never
made it into the index.
Next commit wires sevenz_extract() into the dispatch in src/extract.c and
recognises 7z archives in assets/main.js.
This commit is contained in:
1 parent
4da345a6e8
commit
021c9cb339
21 files changed
+2656
-354
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
/* Standalone 7z extraction engine, the third sibling of zip_extract.c and
|
||||
rar_extract.c. Like them it has no HTTP or task dependencies, and it fills
|
||||
in the same zipx_result_t so a caller can treat every format alike.
|
||||
|
||||
Input may be a single `name.7z` or a byte-split set (`name.7z.001`, ...):
|
||||
both reach the decoder through src/sevenz_volstream.c.
|
||||
|
||||
The publish / staging / rollback / name-validation machinery is mirrored
|
||||
from rar_extract.c on purpose -- three self-contained engines is the shape
|
||||
this project has settled on, so that a format's bugs stay inside its file.
|
||||
|
||||
Backend notes (LZMA SDK 26.03 + src/sevenz_chain.c):
|
||||
* Copy / LZMA / LZMA2 / PPMd, the Delta filter and the x86 / PPC / IA64 /
|
||||
ARM / ARMT / SPARC branch converters, BCJ2, and 7zAES.
|
||||
* An encrypted *header* (`-mhe=on`) is not readable: the SDK refuses it
|
||||
before any folder is known, and we report exactly that.
|
||||
*/
|
||||
|
||||
#include "zip_extract.h"
|
||||
|
||||
/* Extract sevenz_path into dst_dir.
|
||||
`password` is the archive password as UTF-8, or NULL / "" when the caller
|
||||
has none. It is only consulted by archives that encrypt their streams.
|
||||
|
||||
Returns ZIPX_OK or an error code; *result is always filled in. A missing or
|
||||
wrong password comes back as ZIPX_ERR_PASSWORD so the caller can ask for one
|
||||
and retry. On any failure the staging directory is removed and dst_dir is
|
||||
left as it was, except for objects already published under the overwrite
|
||||
policy. */
|
||||
zipx_status_t sevenz_extract(const char *sevenz_path, const char *dst_dir,
|
||||
zipx_conflict_t conflict,
|
||||
const zipx_limits_t *limits,
|
||||
zipx_cancel_fn cancel,
|
||||
zipx_progress_fn progress, void *userdata,
|
||||
const char *password, zipx_result_t *result);
|
||||
+3
-88
@@ -36,93 +36,8 @@
|
||||
#define ZIPX_PUBLISH_MAX_DEPTH 128
|
||||
#define ZIPX_SPACE_SLACK_PER_ENTRY 512
|
||||
|
||||
/* Default limits.
|
||||
*
|
||||
* Tuned to cover real-world PS5 workloads without prompting:
|
||||
* - PS5 system backup ZIPs (~200-300 GiB total, individual chunks <64 GiB)
|
||||
* - 3A-game archives with a single ~300 GiB uncompressed file
|
||||
*
|
||||
* Safety against zip bombs is delegated to:
|
||||
* 1. `check_space()` (statvfs-based real disk space check) before extract
|
||||
* 2. `max_ratio` below (declared compression ratio cap)
|
||||
* The size caps here are an early-fail UX guard, not a security boundary.
|
||||
*/
|
||||
static const zipx_limits_t k_default_limits = {
|
||||
.max_entries = 200000,
|
||||
.max_total_bytes = 2ULL * 1024 * 1024 * 1024 * 1024,
|
||||
.max_file_bytes = 512ULL * 1024 * 1024 * 1024,
|
||||
.max_ratio = 500,
|
||||
/* Only entries that would individually materialise >=1 GiB are screened
|
||||
by ratio; anything smaller is harmless (bounded by declared size + the
|
||||
real free-space check) and is commonly highly compressible in
|
||||
legitimate archives. */
|
||||
.ratio_min_bytes = 1ULL * 1024 * 1024 * 1024,
|
||||
.max_depth = 32,
|
||||
.max_name_len = 255,
|
||||
.max_path_len = 1024
|
||||
};
|
||||
|
||||
/* Large profile for archives that exceed the default cap.
|
||||
*
|
||||
* - max_file_bytes = 1 TiB (single uncompressed file)
|
||||
* - max_total_bytes = 4 TiB (whole archive)
|
||||
* - max_ratio = 1000 (relaxed ratio cap; check_space still applies)
|
||||
*
|
||||
* Requires the user to opt in via the web UI (large=1) before these take
|
||||
* effect. Default limits must always be strictly smaller than large so the
|
||||
* large profile is unambiguously a relaxation.
|
||||
*/
|
||||
static const zipx_limits_t k_large_limits = {
|
||||
.max_entries = 500000,
|
||||
.max_total_bytes = 4ULL * 1024 * 1024 * 1024 * 1024,
|
||||
.max_file_bytes = 1ULL * 1024 * 1024 * 1024 * 1024,
|
||||
.max_ratio = 1000,
|
||||
.ratio_min_bytes = 1ULL * 1024 * 1024 * 1024,
|
||||
.max_depth = 32,
|
||||
.max_name_len = 255,
|
||||
.max_path_len = 1024
|
||||
};
|
||||
|
||||
const zipx_limits_t *
|
||||
zipx_default_limits(void) {
|
||||
return &k_default_limits;
|
||||
}
|
||||
|
||||
const zipx_limits_t *
|
||||
zipx_limits_profile(int profile) {
|
||||
switch(profile) {
|
||||
case ZIPX_LIMITS_LARGE:
|
||||
return &k_large_limits;
|
||||
case ZIPX_LIMITS_DEFAULT:
|
||||
default:
|
||||
return &k_default_limits;
|
||||
}
|
||||
}
|
||||
|
||||
const char *
|
||||
zipx_status_string(zipx_status_t status) {
|
||||
switch(status) {
|
||||
case ZIPX_OK: return "ok";
|
||||
case ZIPX_ERR_CANCELED: return "canceled";
|
||||
case ZIPX_ERR_OPEN: return "cannot open archive";
|
||||
case ZIPX_ERR_FORMAT: return "corrupt archive";
|
||||
case ZIPX_ERR_UNSUPPORTED: return "unsupported archive";
|
||||
case ZIPX_ERR_UNSAFE_NAME: return "unsafe entry name";
|
||||
case ZIPX_ERR_SPECIAL: return "unsupported entry type";
|
||||
case ZIPX_ERR_DUPLICATE: return "duplicate entry name";
|
||||
case ZIPX_ERR_LIMIT_ENTRIES: return "too many entries";
|
||||
case ZIPX_ERR_LIMIT_FILE: return "entry too large";
|
||||
case ZIPX_ERR_LIMIT_TOTAL: return "archive contents too large";
|
||||
case ZIPX_ERR_LIMIT_RATIO: return "compression ratio too high";
|
||||
case ZIPX_ERR_LIMIT_DEPTH: return "path too deep";
|
||||
case ZIPX_ERR_LIMIT_NAME: return "path too long";
|
||||
case ZIPX_ERR_CONFLICT: return "target already exists";
|
||||
case ZIPX_ERR_SPACE: return "not enough space";
|
||||
case ZIPX_ERR_IO: return "read or write failed";
|
||||
case ZIPX_ERR_CRC: return "crc mismatch";
|
||||
default: return "internal error";
|
||||
}
|
||||
}
|
||||
/* The limit profiles and zipx_status_string() are format independent and live
|
||||
in src/zipx_common.c, which every engine links. */
|
||||
|
||||
/**************************************************************************
|
||||
* small helpers
|
||||
@@ -1354,7 +1269,7 @@ zipx_extract(const char *zip_path, const char *dst_dir,
|
||||
memset(result, 0, sizeof(*result));
|
||||
c->result = result;
|
||||
c->conflict = conflict;
|
||||
c->limits = limits ? *limits : k_default_limits;
|
||||
c->limits = limits ? *limits : *zipx_default_limits();
|
||||
c->cancel = cancel;
|
||||
c->progress = progress;
|
||||
c->userdata = userdata;
|
||||
|
||||
@@ -31,6 +31,8 @@ typedef enum {
|
||||
ZIPX_ERR_LIMIT_DEPTH,
|
||||
ZIPX_ERR_LIMIT_NAME,
|
||||
ZIPX_ERR_CONFLICT, /* target already exists for the chosen policy */
|
||||
ZIPX_ERR_PASSWORD, /* the archive is encrypted and the password is missing
|
||||
or wrong; the caller can prompt and retry */
|
||||
ZIPX_ERR_SPACE,
|
||||
ZIPX_ERR_IO,
|
||||
ZIPX_ERR_CRC,
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/* Bits of the zipx_* contract that are not specific to a container format.
|
||||
|
||||
The limit profiles and the status-to-text mapping describe the *engine
|
||||
family*, not ZIP, so they live here rather than inside zip_extract.c. All
|
||||
three engines (ZIP, RAR, 7z) link this one object; keeping them in the ZIP
|
||||
file would force the RAR and 7z test builds to drag in minizip-ng and zlib
|
||||
for the sake of three functions. */
|
||||
|
||||
#include "zip_extract.h"
|
||||
|
||||
/* Default limits.
|
||||
*
|
||||
* Tuned to cover real-world PS5 workloads without prompting:
|
||||
* - PS5 system backup archives (~200-300 GiB total, individual chunks
|
||||
* well under 64 GiB)
|
||||
* - 3A-game archives with a single ~300 GiB uncompressed file
|
||||
*
|
||||
* Safety against decompression bombs is delegated to:
|
||||
* 1. `check_space()` (statvfs-based real disk space check) before extract
|
||||
* 2. `max_ratio` below (declared compression ratio cap)
|
||||
* The size caps here are an early-fail UX guard, not a security boundary.
|
||||
*/
|
||||
static const zipx_limits_t k_default_limits = {
|
||||
.max_entries = 200000,
|
||||
.max_total_bytes = 2ULL * 1024 * 1024 * 1024 * 1024,
|
||||
.max_file_bytes = 512ULL * 1024 * 1024 * 1024,
|
||||
.max_ratio = 500,
|
||||
/* Only entries that would individually materialise >=1 GiB are screened
|
||||
by ratio; anything smaller is harmless (bounded by declared size + the
|
||||
real free-space check) and is commonly highly compressible in
|
||||
legitimate archives. */
|
||||
.ratio_min_bytes = 1ULL * 1024 * 1024 * 1024,
|
||||
.max_depth = 32,
|
||||
.max_name_len = 255,
|
||||
.max_path_len = 1024
|
||||
};
|
||||
|
||||
/* Large profile for archives that exceed the default cap.
|
||||
*
|
||||
* - max_file_bytes = 1 TiB (single uncompressed file)
|
||||
* - max_total_bytes = 4 TiB (whole archive)
|
||||
* - max_ratio = 1000 (relaxed ratio cap; check_space still applies)
|
||||
*
|
||||
* Requires the user to opt in via the web UI (large=1) before these take
|
||||
* effect. Default limits must always be strictly smaller than large so the
|
||||
* large profile is unambiguously a relaxation.
|
||||
*/
|
||||
static const zipx_limits_t k_large_limits = {
|
||||
.max_entries = 500000,
|
||||
.max_total_bytes = 4ULL * 1024 * 1024 * 1024 * 1024,
|
||||
.max_file_bytes = 1ULL * 1024 * 1024 * 1024 * 1024,
|
||||
.max_ratio = 1000,
|
||||
.ratio_min_bytes = 1ULL * 1024 * 1024 * 1024,
|
||||
.max_depth = 32,
|
||||
.max_name_len = 255,
|
||||
.max_path_len = 1024
|
||||
};
|
||||
|
||||
const zipx_limits_t *
|
||||
zipx_default_limits(void) {
|
||||
return &k_default_limits;
|
||||
}
|
||||
|
||||
const zipx_limits_t *
|
||||
zipx_limits_profile(int profile) {
|
||||
switch(profile) {
|
||||
case ZIPX_LIMITS_LARGE:
|
||||
return &k_large_limits;
|
||||
case ZIPX_LIMITS_DEFAULT:
|
||||
default:
|
||||
return &k_default_limits;
|
||||
}
|
||||
}
|
||||
|
||||
const char *
|
||||
zipx_status_string(zipx_status_t status) {
|
||||
switch(status) {
|
||||
case ZIPX_OK: return "ok";
|
||||
case ZIPX_ERR_CANCELED: return "canceled";
|
||||
case ZIPX_ERR_OPEN: return "cannot open archive";
|
||||
case ZIPX_ERR_FORMAT: return "corrupt archive";
|
||||
case ZIPX_ERR_UNSUPPORTED: return "unsupported archive";
|
||||
case ZIPX_ERR_UNSAFE_NAME: return "unsafe entry name";
|
||||
case ZIPX_ERR_SPECIAL: return "unsupported entry type";
|
||||
case ZIPX_ERR_DUPLICATE: return "duplicate entry name";
|
||||
case ZIPX_ERR_LIMIT_ENTRIES: return "too many entries";
|
||||
case ZIPX_ERR_LIMIT_FILE: return "entry too large";
|
||||
case ZIPX_ERR_LIMIT_TOTAL: return "archive contents too large";
|
||||
case ZIPX_ERR_LIMIT_RATIO: return "compression ratio too high";
|
||||
case ZIPX_ERR_LIMIT_DEPTH: return "path too deep";
|
||||
case ZIPX_ERR_LIMIT_NAME: return "path too long";
|
||||
case ZIPX_ERR_CONFLICT: return "target already exists";
|
||||
case ZIPX_ERR_PASSWORD: return "password required or wrong";
|
||||
case ZIPX_ERR_SPACE: return "not enough space";
|
||||
case ZIPX_ERR_IO: return "read or write failed";
|
||||
case ZIPX_ERR_CRC: return "crc mismatch";
|
||||
default: return "internal error";
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
+474
-262
@@ -1,262 +1,474 @@
|
||||
/* Host test shim: lets the POSIX extraction engine build and run on MinGW.
|
||||
Injected with gcc -include for the test build only; never compiled into the
|
||||
PS5 payload. It maps the *at() calls onto plain paths and fakes the few
|
||||
POSIX bits Windows lacks (symlinks and O_NOFOLLOW have no Windows
|
||||
equivalent, which is why the symlink tests are skipped there). */
|
||||
|
||||
#ifndef WFM_TEST_POSIX_COMPAT_H
|
||||
#define WFM_TEST_POSIX_COMPAT_H
|
||||
|
||||
#if defined(__MINGW32__) || defined(_WIN32)
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/types.h>
|
||||
#include <time.h>
|
||||
#include <windows.h>
|
||||
|
||||
#ifndef PATH_MAX
|
||||
#define PATH_MAX 4096
|
||||
#endif
|
||||
|
||||
#define O_NOFOLLOW 0
|
||||
#define O_CLOEXEC 0
|
||||
/* Windows cannot open a directory with _open(); a non-zero sentinel lets the
|
||||
shim detect directory opens and hand back a synthetic dirfd. */
|
||||
#define O_DIRECTORY 0x10000
|
||||
#define AT_SYMLINK_NOFOLLOW 0
|
||||
#define AT_REMOVEDIR 0x0200
|
||||
#ifndef S_IFLNK
|
||||
#define S_IFLNK 0xA000
|
||||
#endif
|
||||
#ifndef S_ISLNK
|
||||
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
|
||||
#endif
|
||||
|
||||
#ifndef CLOCK_MONOTONIC
|
||||
#define CLOCK_MONOTONIC 1
|
||||
#endif
|
||||
|
||||
#define WFM_FD_SLOTS 512
|
||||
|
||||
#define open(...) wfm_open(__VA_ARGS__)
|
||||
|
||||
static struct {
|
||||
int fd;
|
||||
char path[PATH_MAX];
|
||||
} wfm_fd_slots[WFM_FD_SLOTS];
|
||||
|
||||
static void __attribute__((unused))
|
||||
wfm_fd_set(int fd, const char *path) {
|
||||
int i;
|
||||
|
||||
if(fd < 0) {
|
||||
return;
|
||||
}
|
||||
for(i = 0; i < WFM_FD_SLOTS; i++) {
|
||||
if(wfm_fd_slots[i].fd == fd || !wfm_fd_slots[i].path[0]) {
|
||||
wfm_fd_slots[i].fd = fd;
|
||||
snprintf(wfm_fd_slots[i].path, PATH_MAX, "%s", path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void __attribute__((unused))
|
||||
wfm_fd_clear(int fd) {
|
||||
int i;
|
||||
|
||||
for(i = 0; i < WFM_FD_SLOTS; i++) {
|
||||
if(wfm_fd_slots[i].fd == fd) {
|
||||
wfm_fd_slots[i].fd = -1;
|
||||
wfm_fd_slots[i].path[0] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const char *
|
||||
wfm_fd_path(int fd) {
|
||||
int i;
|
||||
|
||||
for(i = 0; i < WFM_FD_SLOTS; i++) {
|
||||
if(wfm_fd_slots[i].fd == fd) {
|
||||
return wfm_fd_slots[i].path;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int
|
||||
wfm_join(int dirfd, const char *rel, char *out, size_t out_size) {
|
||||
const char *base = wfm_fd_path(dirfd);
|
||||
|
||||
if(!base) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
if(snprintf(out, out_size, "%s/%s", base, rel) >= (int)out_size) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_open(const char *path, int flags, ...) {
|
||||
int mode = 0;
|
||||
int fd;
|
||||
|
||||
if(flags & O_CREAT) {
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, flags);
|
||||
mode = va_arg(ap, int);
|
||||
va_end(ap);
|
||||
}
|
||||
/* Directory opens become synthetic fds so openat/mkdirat can resolve them
|
||||
to paths; _open() returns EACCES for directories on Windows. */
|
||||
if(flags & O_DIRECTORY) {
|
||||
static int next_dirfd = 0x10000;
|
||||
|
||||
fd = next_dirfd++;
|
||||
wfm_fd_set(fd, path);
|
||||
return fd;
|
||||
}
|
||||
/* Force O_BINARY: MinGW's _open defaults to text mode, which would
|
||||
translate LF -> CRLF on write and corrupt binary payloads. */
|
||||
fd = _open(path, (flags & ~(O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC)) | O_BINARY,
|
||||
mode);
|
||||
if(fd >= 0) {
|
||||
wfm_fd_set(fd, path);
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_openat(int dirfd, const char *path, int flags, ...) {
|
||||
char full[PATH_MAX];
|
||||
int mode = 0;
|
||||
|
||||
if(wfm_join(dirfd, path, full, sizeof(full))) {
|
||||
return -1;
|
||||
}
|
||||
if(flags & O_CREAT) {
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, flags);
|
||||
mode = va_arg(ap, int);
|
||||
va_end(ap);
|
||||
}
|
||||
return wfm_open(full, flags, mode);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_mkdirat(int dirfd, const char *path, mode_t mode) {
|
||||
char full[PATH_MAX];
|
||||
|
||||
(void)mode;
|
||||
if(wfm_join(dirfd, path, full, sizeof(full))) {
|
||||
return -1;
|
||||
}
|
||||
return mkdir(full);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_renameat(int from_fd, const char *from, int to_fd, const char *to) {
|
||||
char src[PATH_MAX];
|
||||
char dst[PATH_MAX];
|
||||
|
||||
if(wfm_join(from_fd, from, src, sizeof(src)) ||
|
||||
wfm_join(to_fd, to, dst, sizeof(dst))) {
|
||||
return -1;
|
||||
}
|
||||
/* Windows rename() refuses to replace an existing file. */
|
||||
if(_access(dst, 0) == 0) {
|
||||
if(remove(dst)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return rename(src, dst);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_rename(const char *from, const char *to) {
|
||||
/* Windows rename() refuses to replace an existing file, unlike POSIX. */
|
||||
if(_access(to, 0) == 0) {
|
||||
if(remove(to)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return rename(from, to);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_unlinkat(int dirfd, const char *path, int flags) {
|
||||
char full[PATH_MAX];
|
||||
|
||||
if(wfm_join(dirfd, path, full, sizeof(full))) {
|
||||
return -1;
|
||||
}
|
||||
return (flags & AT_REMOVEDIR) ? rmdir(full) : unlink(full);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_mkdir1(const char *path) {
|
||||
return mkdir(path); /* MinGW's mkdir() takes a single argument. */
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_fstatat(int dirfd, const char *path, struct stat *st, int flags) {
|
||||
char full[PATH_MAX];
|
||||
|
||||
(void)flags;
|
||||
if(wfm_join(dirfd, path, full, sizeof(full))) {
|
||||
return -1;
|
||||
}
|
||||
return stat(full, st);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_fsync(int fd) {
|
||||
return _commit(fd);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_fchmod(int fd, mode_t mode) {
|
||||
(void)fd;
|
||||
(void)mode;
|
||||
return 0; /* Windows has no Unix modes; the engine ignores this failure. */
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_close(int fd) {
|
||||
wfm_fd_clear(fd);
|
||||
if(fd >= 0x10000) {
|
||||
return 0; /* synthetic dirfd, nothing to close */
|
||||
}
|
||||
return _close(fd);
|
||||
}
|
||||
|
||||
#define mkdir(p, ...) wfm_mkdir1(p)
|
||||
#define open(...) wfm_open(__VA_ARGS__)
|
||||
#define openat(...) wfm_openat(__VA_ARGS__)
|
||||
#define mkdirat(d, p, m) wfm_mkdirat(d, p, m)
|
||||
#define rename(a, b) wfm_rename(a, b)
|
||||
#define renameat(sd, sp, dd, dp) wfm_renameat(sd, sp, dd, dp)
|
||||
#define unlinkat(d, p, f) wfm_unlinkat(d, p, f)
|
||||
#define fstatat(d, p, s, f) wfm_fstatat(d, p, s, f)
|
||||
#define fsync(fd) wfm_fsync(fd)
|
||||
#define fchmod(fd, mode) wfm_fchmod(fd, mode)
|
||||
#define close(fd) wfm_close(fd)
|
||||
#define lstat(p, s) stat(p, s)
|
||||
|
||||
#endif /* _WIN32 */
|
||||
|
||||
#endif /* WFM_TEST_POSIX_COMPAT_H */
|
||||
/* Host test shim: lets the POSIX extraction engine build and run on MinGW.
|
||||
Injected with gcc -include for the test build only; never compiled into the
|
||||
PS5 payload. It maps the *at() calls onto plain paths and fakes the few
|
||||
POSIX bits Windows lacks (symlinks and O_NOFOLLOW have no Windows
|
||||
equivalent, which is why the symlink tests are skipped there). */
|
||||
|
||||
#ifndef WFM_TEST_POSIX_COMPAT_H
|
||||
#define WFM_TEST_POSIX_COMPAT_H
|
||||
|
||||
/* _wopendir / struct _wdirent require Vista+; pull the SDK level up before any
|
||||
system header touches the type definitions. */
|
||||
#ifndef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0600
|
||||
#endif
|
||||
|
||||
#if defined(__MINGW32__) || defined(_WIN32)
|
||||
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <io.h>
|
||||
#include <direct.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/utime.h>
|
||||
#include <time.h>
|
||||
#include <wchar.h>
|
||||
#include <windows.h>
|
||||
#include <dirent.h>
|
||||
|
||||
#ifndef PATH_MAX
|
||||
#define PATH_MAX 4096
|
||||
#endif
|
||||
|
||||
#define O_NOFOLLOW 0
|
||||
#define O_CLOEXEC 0
|
||||
/* Windows cannot open a directory with _open(); a non-zero sentinel lets the
|
||||
shim detect directory opens and hand back a synthetic dirfd. */
|
||||
#define O_DIRECTORY 0x10000
|
||||
#define AT_SYMLINK_NOFOLLOW 0
|
||||
#define AT_REMOVEDIR 0x0200
|
||||
#ifndef S_IFLNK
|
||||
#define S_IFLNK 0xA000
|
||||
#endif
|
||||
#ifndef S_ISLNK
|
||||
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK)
|
||||
#endif
|
||||
|
||||
#ifndef CLOCK_MONOTONIC
|
||||
#define CLOCK_MONOTONIC 1
|
||||
#endif
|
||||
|
||||
#define WFM_FD_SLOTS 512
|
||||
|
||||
#define open(...) wfm_open(__VA_ARGS__)
|
||||
|
||||
static struct {
|
||||
int fd;
|
||||
char path[PATH_MAX];
|
||||
} wfm_fd_slots[WFM_FD_SLOTS];
|
||||
|
||||
static void __attribute__((unused))
|
||||
wfm_fd_set(int fd, const char *path) {
|
||||
int i;
|
||||
|
||||
if(fd < 0) {
|
||||
return;
|
||||
}
|
||||
for(i = 0; i < WFM_FD_SLOTS; i++) {
|
||||
if(wfm_fd_slots[i].fd == fd || !wfm_fd_slots[i].path[0]) {
|
||||
wfm_fd_slots[i].fd = fd;
|
||||
snprintf(wfm_fd_slots[i].path, PATH_MAX, "%s", path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void __attribute__((unused))
|
||||
wfm_fd_clear(int fd) {
|
||||
int i;
|
||||
|
||||
for(i = 0; i < WFM_FD_SLOTS; i++) {
|
||||
if(wfm_fd_slots[i].fd == fd) {
|
||||
wfm_fd_slots[i].fd = -1;
|
||||
wfm_fd_slots[i].path[0] = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const char *
|
||||
wfm_fd_path(int fd) {
|
||||
int i;
|
||||
|
||||
for(i = 0; i < WFM_FD_SLOTS; i++) {
|
||||
if(wfm_fd_slots[i].fd == fd) {
|
||||
return wfm_fd_slots[i].path;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int
|
||||
wfm_join(int dirfd, const char *rel, char *out, size_t out_size) {
|
||||
const char *base = wfm_fd_path(dirfd);
|
||||
|
||||
if(!base) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
if(snprintf(out, out_size, "%s/%s", base, rel) >= (int)out_size) {
|
||||
errno = ENAMETOOLONG;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* UTF-8 to UTF-16, for the wide entry points below. The engines speak UTF-8
|
||||
(that is what an archive stores), but MinGW's ANSI entry points decode their
|
||||
argument in the system code page: on a CP936 or CP1252 host a name such as
|
||||
"中文-テスト.txt" is either mangled or rejected outright with the
|
||||
unhelpful errno -1. Going through the wide API keeps the on-disk name
|
||||
identical to the archive's. */
|
||||
static void __attribute__((unused))
|
||||
wfm_wide(const char *src, wchar_t *dst, size_t cap) {
|
||||
const unsigned char *p = (const unsigned char *)src;
|
||||
size_t out = 0;
|
||||
|
||||
while(*p && out + 2 < cap) {
|
||||
unsigned long cp = *p++;
|
||||
|
||||
if(cp >= 0x80) {
|
||||
unsigned extra = 0;
|
||||
unsigned i;
|
||||
|
||||
if((cp & 0xE0) == 0xC0) {
|
||||
cp &= 0x1F;
|
||||
extra = 1;
|
||||
} else if((cp & 0xF0) == 0xE0) {
|
||||
cp &= 0x0F;
|
||||
extra = 2;
|
||||
} else if((cp & 0xF8) == 0xF0) {
|
||||
cp &= 0x07;
|
||||
extra = 3;
|
||||
} else {
|
||||
cp = '?';
|
||||
extra = 0;
|
||||
}
|
||||
for(i = 0; i < extra; i++) {
|
||||
if((*p & 0xC0) != 0x80) {
|
||||
cp = '?';
|
||||
break;
|
||||
}
|
||||
cp = (cp << 6) | (unsigned long)(*p++ & 0x3F);
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
/* Defined further down; the *at() shims above call them. */
|
||||
static int wfm_mkdir1(const char *path);
|
||||
static int wfm_unlink(const char *path);
|
||||
static int wfm_rmdir(const char *path);
|
||||
static int wfm_stat(const char *path, struct stat *st);
|
||||
static int wfm_rename(const char *from, const char *to);
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_open(const char *path, int flags, ...) {
|
||||
int mode = 0;
|
||||
int fd;
|
||||
|
||||
if(flags & O_CREAT) {
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, flags);
|
||||
mode = va_arg(ap, int);
|
||||
va_end(ap);
|
||||
}
|
||||
/* Directory opens become synthetic fds so openat/mkdirat can resolve them
|
||||
to paths; _open() returns EACCES for directories on Windows. */
|
||||
if(flags & O_DIRECTORY) {
|
||||
static int next_dirfd = 0x10000;
|
||||
|
||||
fd = next_dirfd++;
|
||||
wfm_fd_set(fd, path);
|
||||
return fd;
|
||||
}
|
||||
/* Force O_BINARY: MinGW's _open defaults to text mode, which would
|
||||
translate LF -> CRLF on write and corrupt binary payloads. The wide
|
||||
call keeps a non-ASCII name intact (see wfm_wide). */
|
||||
{
|
||||
wchar_t wide[PATH_MAX];
|
||||
|
||||
wfm_wide(path, wide, PATH_MAX);
|
||||
fd = _wopen(wide, (flags & ~(O_NOFOLLOW | O_DIRECTORY | O_CLOEXEC)) |
|
||||
O_BINARY,
|
||||
mode);
|
||||
}
|
||||
if(fd >= 0) {
|
||||
wfm_fd_set(fd, path);
|
||||
}
|
||||
return fd;
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_openat(int dirfd, const char *path, int flags, ...) {
|
||||
char full[PATH_MAX];
|
||||
int mode = 0;
|
||||
|
||||
if(wfm_join(dirfd, path, full, sizeof(full))) {
|
||||
return -1;
|
||||
}
|
||||
if(flags & O_CREAT) {
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, flags);
|
||||
mode = va_arg(ap, int);
|
||||
va_end(ap);
|
||||
}
|
||||
return wfm_open(full, flags, mode);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_mkdirat(int dirfd, const char *path, mode_t mode) {
|
||||
char full[PATH_MAX];
|
||||
|
||||
(void)mode;
|
||||
if(wfm_join(dirfd, path, full, sizeof(full))) {
|
||||
return -1;
|
||||
}
|
||||
return wfm_mkdir1(full);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_renameat(int from_fd, const char *from, int to_fd, const char *to) {
|
||||
char src[PATH_MAX];
|
||||
char dst[PATH_MAX];
|
||||
|
||||
if(wfm_join(from_fd, from, src, sizeof(src)) ||
|
||||
wfm_join(to_fd, to, dst, sizeof(dst))) {
|
||||
return -1;
|
||||
}
|
||||
return wfm_rename(src, dst);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_rename(const char *from, const char *to) {
|
||||
wchar_t wfrom[PATH_MAX];
|
||||
wchar_t wto[PATH_MAX];
|
||||
|
||||
wfm_wide(from, wfrom, PATH_MAX);
|
||||
wfm_wide(to, wto, PATH_MAX);
|
||||
/* Windows rename() refuses to replace an existing file, unlike POSIX. */
|
||||
if(_waccess(wto, 0) == 0) {
|
||||
if(_wremove(wto)) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return _wrename(wfrom, wto);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_unlinkat(int dirfd, const char *path, int flags) {
|
||||
char full[PATH_MAX];
|
||||
|
||||
if(wfm_join(dirfd, path, full, sizeof(full))) {
|
||||
return -1;
|
||||
}
|
||||
return (flags & AT_REMOVEDIR) ? wfm_rmdir(full) : wfm_unlink(full);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_mkdir1(const char *path) {
|
||||
wchar_t wide[PATH_MAX];
|
||||
|
||||
wfm_wide(path, wide, PATH_MAX);
|
||||
return _wmkdir(wide); /* MinGW's mkdir() takes a single argument. */
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_unlink(const char *path) {
|
||||
wchar_t wide[PATH_MAX];
|
||||
|
||||
wfm_wide(path, wide, PATH_MAX);
|
||||
return _wunlink(wide);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_rmdir(const char *path) {
|
||||
wchar_t wide[PATH_MAX];
|
||||
|
||||
wfm_wide(path, wide, PATH_MAX);
|
||||
return _wrmdir(wide);
|
||||
}
|
||||
|
||||
/* _wstati64 fills its own struct; the engines only ever read st_mode, st_size
|
||||
and st_mtime, so copying those across is safe and avoids depending on how
|
||||
this toolchain happens to alias `struct stat`. */
|
||||
static int __attribute__((unused))
|
||||
wfm_stat(const char *path, struct stat *st) {
|
||||
wchar_t wide[PATH_MAX];
|
||||
struct _stati64 wst;
|
||||
|
||||
wfm_wide(path, wide, PATH_MAX);
|
||||
if(_wstati64(wide, &wst)) {
|
||||
return -1;
|
||||
}
|
||||
memset(st, 0, sizeof(*st));
|
||||
st->st_mode = (mode_t)wst.st_mode;
|
||||
st->st_size = (off_t)wst.st_size;
|
||||
st->st_mtime = (time_t)wst.st_mtime;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_fstatat(int dirfd, const char *path, struct stat *st, int flags) {
|
||||
char full[PATH_MAX];
|
||||
|
||||
(void)flags;
|
||||
if(wfm_join(dirfd, path, full, sizeof(full))) {
|
||||
return -1;
|
||||
}
|
||||
return wfm_stat(full, st);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_fsync(int fd) {
|
||||
return _commit(fd);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_fchmod(int fd, mode_t mode) {
|
||||
(void)fd;
|
||||
(void)mode;
|
||||
return 0; /* Windows has no Unix modes; the engine ignores this failure. */
|
||||
}
|
||||
|
||||
/* MinGW has no utimes(); _utime() is the same thing with second precision,
|
||||
which is all the 7z engine asks for (it feeds the extractor both fields). */
|
||||
static int __attribute__((unused))
|
||||
wfm_utimes(const char *path, const struct timeval tv[2]) {
|
||||
struct _utimbuf ut;
|
||||
|
||||
ut.actime = tv[0].tv_sec;
|
||||
ut.modtime = tv[1].tv_sec;
|
||||
return _utime(path, &ut);
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_close(int fd) {
|
||||
wfm_fd_clear(fd);
|
||||
if(fd >= 0x10000) {
|
||||
return 0; /* synthetic dirfd, nothing to close */
|
||||
}
|
||||
return _close(fd);
|
||||
}
|
||||
|
||||
/* MinGW's fopen() decodes the path in the system code page, the same way
|
||||
stat() does. Redirect to _wfopen so a UTF-8 path goes through the wide
|
||||
API and round-trips back to the on-disk name regardless of the host's
|
||||
ACP. The translation units that include this shim may not call fopen()
|
||||
themselves, so mark the wrapper as unused to keep -Werror quiet. */
|
||||
__attribute__((unused))
|
||||
static FILE *wfm_fopen(const char *path, const char *mode) {
|
||||
wchar_t wide_path[PATH_MAX];
|
||||
wchar_t wide_mode[16];
|
||||
size_t i;
|
||||
|
||||
wfm_wide(path, wide_path, PATH_MAX);
|
||||
for(i = 0; i + 1 < sizeof(wide_mode) && mode[i]; i++) {
|
||||
wide_mode[i] = (wchar_t)(unsigned char)mode[i];
|
||||
}
|
||||
wide_mode[i] = 0;
|
||||
return _wfopen(wide_path, wide_mode);
|
||||
}
|
||||
|
||||
#define fopen(p, m) wfm_fopen(p, m)
|
||||
|
||||
#define mkdir(p, ...) wfm_mkdir1(p)
|
||||
#define rmdir(p) wfm_rmdir(p)
|
||||
#define unlink(p) wfm_unlink(p)
|
||||
#define open(...) wfm_open(__VA_ARGS__)
|
||||
#define openat(...) wfm_openat(__VA_ARGS__)
|
||||
#define mkdirat(d, p, m) wfm_mkdirat(d, p, m)
|
||||
#define rename(a, b) wfm_rename(a, b)
|
||||
#define renameat(sd, sp, dd, dp) wfm_renameat(sd, sp, dd, dp)
|
||||
#define unlinkat(d, p, f) wfm_unlinkat(d, p, f)
|
||||
#define fstatat(d, p, s, f) wfm_fstatat(d, p, s, f)
|
||||
#define fsync(fd) wfm_fsync(fd)
|
||||
#define fchmod(fd, mode) wfm_fchmod(fd, mode)
|
||||
#define close(fd) wfm_close(fd)
|
||||
/* Direct lstat/stat to the wide-path shim so non-ASCII archive entries survive
|
||||
a CP936 or CP1252 host. MinGW's stat() defaults to the ANSI entry point
|
||||
and silently truncates names it cannot represent. */
|
||||
#define lstat(p, s) wfm_stat(p, s)
|
||||
#define stat(p, s) wfm_stat(p, s)
|
||||
#define utimes(p, tv) wfm_utimes(p, tv)
|
||||
/* opendir/readdir/closedir go through the wide variants so the names we get
|
||||
back are real UTF-8; otherwise MinGW hands us whatever the system code page
|
||||
made of the filename, which round-trips through a non-ASCII UTF-8 entry as
|
||||
a garbage string that no later wfm_stat() call can resolve. */
|
||||
typedef struct {
|
||||
_WDIR *wd;
|
||||
struct dirent de;
|
||||
} WFM_DIR;
|
||||
|
||||
static DIR * __attribute__((unused))
|
||||
wfm_opendir(const char *path) {
|
||||
wchar_t wide[PATH_MAX];
|
||||
WFM_DIR *wfm;
|
||||
|
||||
wfm_wide(path, wide, PATH_MAX);
|
||||
wfm = (WFM_DIR *)malloc(sizeof(*wfm));
|
||||
if(!wfm) {
|
||||
return NULL;
|
||||
}
|
||||
wfm->wd = _wopendir(wide);
|
||||
if(!wfm->wd) {
|
||||
free(wfm);
|
||||
return NULL;
|
||||
}
|
||||
return (DIR *)wfm;
|
||||
}
|
||||
|
||||
static struct dirent * __attribute__((unused))
|
||||
wfm_readdir(DIR *d) {
|
||||
WFM_DIR *wfm = (WFM_DIR *)d;
|
||||
struct _wdirent *we;
|
||||
|
||||
if(!wfm) {
|
||||
return NULL;
|
||||
}
|
||||
we = _wreaddir(wfm->wd);
|
||||
if(!we) {
|
||||
return NULL;
|
||||
}
|
||||
WideCharToMultiByte(CP_UTF8, 0, we->d_name, -1, wfm->de.d_name,
|
||||
sizeof(wfm->de.d_name), NULL, NULL);
|
||||
wfm->de.d_ino = we->d_ino;
|
||||
wfm->de.d_reclen = (unsigned short)strlen(wfm->de.d_name);
|
||||
return &wfm->de;
|
||||
}
|
||||
|
||||
static int __attribute__((unused))
|
||||
wfm_closedir(DIR *d) {
|
||||
WFM_DIR *wfm = (WFM_DIR *)d;
|
||||
|
||||
if(!wfm) {
|
||||
return -1;
|
||||
}
|
||||
_wclosedir(wfm->wd);
|
||||
free(wfm);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#define opendir(p) wfm_opendir(p)
|
||||
#define readdir(d) wfm_readdir(d)
|
||||
#define closedir(d) wfm_closedir(d)
|
||||
|
||||
#endif /* _WIN32 */
|
||||
|
||||
#endif /* WFM_TEST_POSIX_COMPAT_H */
|
||||
@@ -18,6 +18,7 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd -W 2>/dev/null || pwd)"
|
||||
BUILD="$ROOT/.build/sevenz-test"
|
||||
SEVENZ_DIR="$ROOT/third_party/7z"
|
||||
FIXTURES="$ROOT/tests/fixtures-7z"
|
||||
COMPAT_INC="$ROOT/tests/compat"
|
||||
PYTHON="${PYTHON:-python3}"
|
||||
CC="${CC:-gcc}"
|
||||
|
||||
@@ -58,8 +59,21 @@ done
|
||||
"$CC" -c -O2 -Wall -Wextra -Werror -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE \
|
||||
-I"$ROOT/src" -o "$BUILD/zipx_volume.o" "$ROOT/src/zipx_volume.c"
|
||||
|
||||
# The extraction facade is engine code too, so it gets the host POSIX shim as
|
||||
# well as the same strictness (see tests/run-tests.sh for the ZIP/RAR pair).
|
||||
# zipx_common carries the limit profiles and the status text that all three
|
||||
# engines share.
|
||||
"$CC" -c -O2 -Wall -Wextra -Werror -I"$ROOT/src" -o "$BUILD/zipx_common.o" \
|
||||
"$ROOT/src/zipx_common.c"
|
||||
"$CC" -c -O2 -Wall -Wextra -Werror -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE \
|
||||
-I"$SEVENZ_DIR" -I"$ROOT/src" -I"$COMPAT_INC" \
|
||||
-include "$ROOT/tests/posix_compat.h" \
|
||||
-o "$BUILD/sevenz_extract.o" "$ROOT/src/sevenz_extract.c"
|
||||
|
||||
ENGINE_OBJS=("$BUILD/sevenz_chain.o" "$BUILD/sevenz_volstream.o"
|
||||
"$BUILD/zipx_volume.o")
|
||||
"$BUILD/zipx_volume.o" "$BUILD/zipx_common.o")
|
||||
|
||||
FACADE_OBJS=("$BUILD/sevenz_extract.o" "${ENGINE_OBJS[@]}")
|
||||
|
||||
# unrar-style extra libs are only needed by the Windows path of 7zFile.c.
|
||||
EXTRA_LIBS=()
|
||||
@@ -71,6 +85,13 @@ esac
|
||||
"$ROOT/tests/sevenz_chain_e2e.c" "${ENGINE_OBJS[@]}" "${VENDOR_OBJS[@]}" \
|
||||
"${EXTRA_LIBS[@]}"
|
||||
|
||||
# The facade driver: the same strict flags as the engine, plus the POSIX shim,
|
||||
# because a MinGW host has neither statvfs() nor a two-argument mkdir().
|
||||
"$CC" -O2 -Wall -Wextra -I"$SEVENZ_DIR" -I"$ROOT/src" -I"$COMPAT_INC" \
|
||||
-include "$ROOT/tests/posix_compat.h" \
|
||||
-o "$BUILD/test_sevenz_extract" "$ROOT/tests/test_sevenz_extract.c" \
|
||||
"${FACADE_OBJS[@]}" "${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" \
|
||||
@@ -146,6 +167,63 @@ if [ -f "$FIXTURES/vol.7z.001" ]; then
|
||||
run_case "vol.7z.001" "$FIXTURES/vol.7z.001"
|
||||
fi
|
||||
|
||||
# ------------------------------------------------- extraction facade
|
||||
# The same fixtures again, but through src/sevenz_extract.c: staging, publish,
|
||||
# limits, conflict policy and name validation all have to agree with the
|
||||
# decoder before the format is wired into the server.
|
||||
echo
|
||||
echo "== 7z extraction facade (engine: src/sevenz_extract.c) =="
|
||||
mkdir -p "$BUILD/fx"
|
||||
for a in store lzma2 lzma ppmd bcj delta utf8 bcj2 solidoff bcj2off aes; do
|
||||
[ -f "$FIXTURES/$a.7z" ] || continue
|
||||
out="$(mktemp -d "$BUILD/fx/XXXXXX")" || continue
|
||||
target="$out/$a"
|
||||
mkdir -p "$target"
|
||||
rc=0
|
||||
case "$a" in
|
||||
aes) "$BUILD/test_sevenz_extract" "$FIXTURES/$a.7z" "$target" \
|
||||
"$FIXTURE_PASSWORD" >"$out.log" 2>&1 || rc=$? ;;
|
||||
*) "$BUILD/test_sevenz_extract" "$FIXTURES/$a.7z" "$target" \
|
||||
>"$out.log" 2>&1 || rc=$? ;;
|
||||
esac
|
||||
if [ "$rc" -eq 0 ] && diff -r "$FIXTURES/_src" "$target/_src" >/dev/null 2>&1; then
|
||||
printf ' %-12s ok\n' "$a"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
printf ' %-12s FAIL\n' "$a"
|
||||
sed -n '1,20p' "$out.log" | sed 's/^/ /'
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
# A byte-split set goes through the same facade.
|
||||
if [ -f "$FIXTURES/vol.7z.001" ]; then
|
||||
out="$(mktemp -d "$BUILD/fx/XXXXXX")" || out=
|
||||
if [ -n "$out" ]; then
|
||||
rc=0
|
||||
"$BUILD/test_sevenz_extract" "$FIXTURES/vol.7z.001" "$out/vol" \
|
||||
>"$out.log" 2>&1 || rc=$?
|
||||
if [ "$rc" -eq 0 ] && diff -r "$FIXTURES/_src" "$out/vol/_src" >/dev/null 2>&1; then
|
||||
printf ' %-12s ok\n' "vol.7z.001"
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
printf ' %-12s FAIL\n' "vol.7z.001"
|
||||
sed -n '1,20p' "$out.log" | sed 's/^/ /'
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "== 7z error and policy paths =="
|
||||
mkdir -p "$BUILD/cases"
|
||||
cases_work="$(mktemp -d "$BUILD/cases/XXXXXX")"
|
||||
if "$BUILD/test_sevenz_extract" --cases "$FIXTURES" "$cases_work"; then
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "$pass passed, $fail failed"
|
||||
[ "$fail" -eq 0 ]
|
||||
+8
-3
@@ -67,6 +67,11 @@ COMPAT_INC="$ROOT/tests/compat"
|
||||
-include "$ROOT/tests/posix_compat.h" \
|
||||
-o "$BUILD/zip_extract.o" "$ROOT/src/zip_extract.c"
|
||||
|
||||
# Format-independent helpers (limits profiles + status string) live here.
|
||||
"$CC" -c -O2 -Wall -Wextra -Wno-unused-parameter -I"$ROOT/src" \
|
||||
-I"$COMPAT_INC" -include "$ROOT/tests/posix_compat.h" \
|
||||
-o "$BUILD/zipx_common.o" "$ROOT/src/zipx_common.c"
|
||||
|
||||
# Volume support: the concatenating stream and the volume set detector.
|
||||
"$CC" -c -O2 -Wall -Wextra -Wno-unused-parameter \
|
||||
-I"$ROOT/third_party/minizip-ng/include" -I"$ROOT/src" -I"$COMPAT_INC" \
|
||||
@@ -94,14 +99,14 @@ objs=()
|
||||
rar_objs=()
|
||||
for obj in "$BUILD"/*.o; do
|
||||
case "$obj" in
|
||||
*/zip_extract.o|*/rar_extract.o|*/test_zip_extract.o|*/test_rar_extract.o) continue ;;
|
||||
*/zip_extract.o|*/zipx_common.o|*/rar_extract.o|*/test_zip_extract.o|*/test_rar_extract.o) continue ;;
|
||||
*/unrar7_*.o) rar_objs+=("$obj"); continue ;;
|
||||
esac
|
||||
objs+=("$obj")
|
||||
done
|
||||
|
||||
"$CC" -O2 -o "$BUILD/test-zip-extract" \
|
||||
"$BUILD/zip_extract.o" "$BUILD/test_zip_extract.o" "${objs[@]}"
|
||||
"$BUILD/zip_extract.o" "$BUILD/zipx_common.o" "$BUILD/test_zip_extract.o" "${objs[@]}"
|
||||
|
||||
# The RAR test links the unrar7 objects, so it needs the C++ driver.
|
||||
# Windows unrar system.cpp references SetSuspendState (PowrProf).
|
||||
@@ -109,7 +114,7 @@ RAR_LIBS=()
|
||||
[ "$HOST_KIND" = windows ] && RAR_LIBS=(-lpowrprof)
|
||||
"$CXX" -O2 -o "$BUILD/test-rar-extract" \
|
||||
"$BUILD/rar_extract.o" "$BUILD/test_rar_extract.o" \
|
||||
"$BUILD/zip_extract.o" "${objs[@]}" "${rar_objs[@]}" "${RAR_LIBS[@]}"
|
||||
"$BUILD/zip_extract.o" "$BUILD/zipx_common.o" "${objs[@]}" "${rar_objs[@]}" "${RAR_LIBS[@]}"
|
||||
|
||||
"$BUILD/test-zip-extract" "$ROOT/tests/fixtures" "$BUILD/work-zip"
|
||||
# Real RAR fixtures (v6 / multi-volume / encrypted) live in fixtures-real/,
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/*
|
||||
* Test driver for the 7z extraction facade (src/sevenz_extract.c).
|
||||
*
|
||||
* test_sevenz_extract <archive.7z> <out-dir> [password]
|
||||
* Extract one archive. Exit status 0 means ZIPX_OK; the shell compares
|
||||
* the result against the fixture's source tree byte for byte.
|
||||
*
|
||||
* test_sevenz_extract --cases <fixtures-dir> <work-dir>
|
||||
* Exercise the error and policy paths that need no byte comparison:
|
||||
* a missing/wrong password, an encrypted header, conflicts under each
|
||||
* policy, cancellation, limits, a missing destination parent, and the
|
||||
* guarantee that nothing is published and no staging tree survives a
|
||||
* failure.
|
||||
*
|
||||
* The host build injects tests/posix_compat.h (see run-sevenz-tests.sh), so
|
||||
* the engine can stay plain POSIX.
|
||||
*/
|
||||
|
||||
#include <dirent.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "sevenz_extract.h"
|
||||
|
||||
#define PASSWORD "Secret123"
|
||||
#define PATH_MAX_LOCAL 4096
|
||||
|
||||
static int g_checks = 0;
|
||||
static int g_failures = 0;
|
||||
|
||||
static void
|
||||
check(int ok, const char *what) {
|
||||
g_checks++;
|
||||
if(!ok) {
|
||||
g_failures++;
|
||||
printf(" FAIL %s\n", what);
|
||||
}
|
||||
}
|
||||
|
||||
static int
|
||||
cancel_always(void *userdata) {
|
||||
(void)userdata;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
has_staging_leftover(const char *dir) {
|
||||
DIR *d = opendir(dir);
|
||||
struct dirent *ent;
|
||||
int found = 0;
|
||||
|
||||
if(!d) {
|
||||
return 0;
|
||||
}
|
||||
while((ent = readdir(d))) {
|
||||
if(!strncmp(ent->d_name, ".wfm-extract-", 13)) {
|
||||
found = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
closedir(d);
|
||||
return found;
|
||||
}
|
||||
|
||||
static int
|
||||
extract_one(const char *archive, const char *dst, const char *password) {
|
||||
zipx_result_t r;
|
||||
zipx_status_t st = sevenz_extract(archive, dst, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, NULL, NULL,
|
||||
password, &r);
|
||||
|
||||
if(st != ZIPX_OK) {
|
||||
fprintf(stderr, " %s: %s: %s\n", archive, zipx_status_string(st),
|
||||
r.message);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Runs a case that is expected to fail, and checks the status and message. */
|
||||
static void
|
||||
expect_fail(const char *label, const char *archive, const char *dst,
|
||||
const char *password, zipx_conflict_t conflict,
|
||||
const zipx_limits_t *limits, zipx_cancel_fn cancel,
|
||||
zipx_status_t want, const char *needle) {
|
||||
zipx_result_t r;
|
||||
zipx_status_t st;
|
||||
char buf[256];
|
||||
|
||||
st = sevenz_extract(archive, dst, conflict, limits, cancel, NULL, NULL,
|
||||
password, &r);
|
||||
snprintf(buf, sizeof(buf), "%s: status is %s, not %s", label,
|
||||
zipx_status_string(st), zipx_status_string(want));
|
||||
check(st == want, buf);
|
||||
if(needle) {
|
||||
snprintf(buf, sizeof(buf), "%s: message mentions \"%s\" (got \"%s\")",
|
||||
label, needle, r.message);
|
||||
check(strstr(r.message, needle) != NULL, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/* Extracts store.7z into `dst` under the given policy, expecting `want`. */
|
||||
static void
|
||||
policy_case(const char *label, const char *archive, const char *dst,
|
||||
zipx_conflict_t conflict, int run, zipx_status_t want) {
|
||||
zipx_result_t r;
|
||||
zipx_status_t st;
|
||||
char buf[256];
|
||||
|
||||
st = sevenz_extract(archive, dst, conflict, zipx_default_limits(), NULL, NULL,
|
||||
NULL, NULL, &r);
|
||||
snprintf(buf, sizeof(buf), "%s (run %d): status is %s, not %s", label, run,
|
||||
zipx_status_string(st), zipx_status_string(want));
|
||||
check(st == want, buf);
|
||||
}
|
||||
|
||||
static int
|
||||
run_cases(const char *fx, const char *work) {
|
||||
char arc[PATH_MAX_LOCAL];
|
||||
char dst[PATH_MAX_LOCAL];
|
||||
zipx_limits_t tight;
|
||||
|
||||
mkdir(work, 0777);
|
||||
|
||||
/* --- passwords ----------------------------------------------------- */
|
||||
snprintf(arc, sizeof(arc), "%s/aes.7z", fx);
|
||||
snprintf(dst, sizeof(dst), "%s/pw-missing", work);
|
||||
mkdir(dst, 0777);
|
||||
expect_fail("aes with no password", arc, dst, NULL, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, ZIPX_ERR_PASSWORD, "encrypted");
|
||||
|
||||
snprintf(dst, sizeof(dst), "%s/pw-wrong", work);
|
||||
mkdir(dst, 0777);
|
||||
expect_fail("aes with the wrong password", arc, dst, "NotThePassword",
|
||||
ZIPX_CONFLICT_FAIL, zipx_default_limits(), NULL,
|
||||
ZIPX_ERR_PASSWORD, "7zAES");
|
||||
check(!has_staging_leftover(work), "no staging tree survives a wrong password");
|
||||
|
||||
snprintf(dst, sizeof(dst), "%s/pw-ok", work);
|
||||
mkdir(dst, 0777);
|
||||
check(extract_one(arc, dst, PASSWORD) == 0,
|
||||
"aes extracts with the right password");
|
||||
|
||||
/* --- encrypted header ---------------------------------------------- */
|
||||
snprintf(arc, sizeof(arc), "%s/aeshe.7z", fx);
|
||||
snprintf(dst, sizeof(dst), "%s/he", work);
|
||||
mkdir(dst, 0777);
|
||||
expect_fail("aeshe (encrypted header)", arc, dst, PASSWORD, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), NULL, ZIPX_ERR_UNSUPPORTED, "-mhe=on");
|
||||
|
||||
/* --- open failures -------------------------------------------------- */
|
||||
snprintf(dst, sizeof(dst), "%s/missing-file", work);
|
||||
mkdir(dst, 0777);
|
||||
expect_fail("a missing archive", "/no/such/archive.7z", dst, NULL,
|
||||
ZIPX_CONFLICT_FAIL, zipx_default_limits(), NULL, ZIPX_ERR_OPEN,
|
||||
"cannot open");
|
||||
|
||||
snprintf(arc, sizeof(arc), "%s/store.7z", fx);
|
||||
snprintf(dst, sizeof(dst), "%s/no-parent/deeper", work);
|
||||
expect_fail("a destination whose parent is missing", arc, dst, NULL,
|
||||
ZIPX_CONFLICT_FAIL, zipx_default_limits(), NULL, ZIPX_ERR_IO,
|
||||
"destination parent is missing");
|
||||
|
||||
/* --- conflict policies ---------------------------------------------- */
|
||||
snprintf(dst, sizeof(dst), "%s/policy-fail", work);
|
||||
mkdir(dst, 0777);
|
||||
policy_case("fail", arc, dst, ZIPX_CONFLICT_FAIL, 1, ZIPX_OK);
|
||||
policy_case("fail", arc, dst, ZIPX_CONFLICT_FAIL, 2, ZIPX_ERR_CONFLICT);
|
||||
|
||||
snprintf(dst, sizeof(dst), "%s/policy-overwrite", work);
|
||||
mkdir(dst, 0777);
|
||||
policy_case("overwrite", arc, dst, ZIPX_CONFLICT_OVERWRITE, 1, ZIPX_OK);
|
||||
policy_case("overwrite", arc, dst, ZIPX_CONFLICT_OVERWRITE, 2, ZIPX_OK);
|
||||
|
||||
snprintf(dst, sizeof(dst), "%s/policy-merge", work);
|
||||
mkdir(dst, 0777);
|
||||
policy_case("merge", arc, dst, ZIPX_CONFLICT_MERGE, 1, ZIPX_OK);
|
||||
policy_case("merge", arc, dst, ZIPX_CONFLICT_MERGE, 2, ZIPX_OK);
|
||||
|
||||
/* --- cancellation --------------------------------------------------- */
|
||||
snprintf(dst, sizeof(dst), "%s/cancel", work);
|
||||
mkdir(dst, 0777);
|
||||
expect_fail("a canceled extraction", arc, dst, NULL, ZIPX_CONFLICT_FAIL,
|
||||
zipx_default_limits(), cancel_always, ZIPX_ERR_CANCELED, NULL);
|
||||
check(!has_staging_leftover(work), "no staging tree survives a cancellation");
|
||||
|
||||
/* --- limits --------------------------------------------------------- */
|
||||
tight = *zipx_default_limits();
|
||||
tight.max_entries = 1;
|
||||
snprintf(dst, sizeof(dst), "%s/limits", work);
|
||||
mkdir(dst, 0777);
|
||||
expect_fail("an archive over the entry limit", arc, dst, NULL,
|
||||
ZIPX_CONFLICT_FAIL, &tight, NULL, ZIPX_ERR_LIMIT_ENTRIES,
|
||||
"more than 1 entries");
|
||||
|
||||
printf(" cases: %d checks, %d failures\n", g_checks, g_failures);
|
||||
return g_failures == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char **argv) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
if(argc >= 4 && !strcmp(argv[1], "--cases")) {
|
||||
return run_cases(argv[2], argv[3]);
|
||||
}
|
||||
if(argc < 3) {
|
||||
fprintf(stderr,
|
||||
"usage: %s <archive.7z> <out-dir> [password]\n"
|
||||
" %s --cases <fixtures-dir> <work-dir>\n",
|
||||
argv[0], argv[0]);
|
||||
return 2;
|
||||
}
|
||||
return extract_one(argv[1], argv[2], argc > 3 ? argv[3] : NULL);
|
||||
}
|
||||
Reference in new issue
Block a user