mirror of
https://github.com/LisherSong/ps5-web-file-manager.git
synced 2026-10-06 13:00:23 +02:00
feat(zip): extract multi-file ZIP volume sets
A split ZIP is several files that only form an archive together. Until now a `.zip.001` was rejected as an unsupported format and a `.z01`/`.partN.zip` member could not be resolved, so these downloads were unusable. Three naming conventions are recognised, from any member of the set: name.zip.001, name.zip.002, ... byte split (7-Zip) name.part1.zip, name.part2.zip byte split (WinRAR) name.z01, ..., name.zip zip split disks (Info-ZIP / PKZIP) The parts are served through a new stream that makes them look like one archive, so nothing is copied to disk first (a 160 GiB set would otherwise need a second 160 GiB scratch copy): - src/zipx_volstream.c implements an mz_stream over the ordered part list. Byte splits keep absolute offsets; zip split disks carry offsets relative to the disk an entry starts on, so that mode honours MZ_STREAM_PROP_DISK_NUMBER the way mz_zip_entry_seek_local_header drives it and starts on the last volume, where the central directory lives. - src/zipx_volume.c groups the set, verifies the numbering is complete and reports exactly which volume is missing. - zip_extract.c tries the layout implied by the naming first and the other one as a fallback, so tools that disagree about offset bases still work. - extract.c dispatches volume members to the engines and, when the caller asked for the source to be deleted, removes every volume instead of leaving orphaned parts behind. Error messages are specific on purpose: an incomplete set reports the missing file name (e.g. "gap.zip.002 is missing"), a lone first volume says the remaining volumes are missing, `.rar.001` sets explain the rename that makes them work, and 7z volumes say 7z is not supported yet. Tests: tests/make_split_fixtures.py builds all three layouts (the disk set is written with per-disk offsets exactly as APPNOTE 4.4.11 describes) plus two broken sets; test_zip_extract.c extracts each layout from a different member and byte-compares the nested binary entries. 108 ZIP + 27 RAR checks, 0 failures.
This commit is contained in:
1 parent
01e27f3825
commit
da565ccb7c
11 files changed
+1690
-24
No files matched your search
@@ -24,7 +24,7 @@ HOST_PKG_CONFIG ?= pkg-config
|
||||
|
||||
BIN := web-file-mgr.elf
|
||||
LINUX_BIN := web-file-mgr-linux
|
||||
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c src/pkg_info.c src/extract.c src/zip_extract.c src/rar_extract.c
|
||||
COMMON_SRCS := src/main.c src/websrv.c src/filemgr.c src/file_response.c src/task.c src/upload.c src/download.c src/text.c src/list.c src/space.c src/fs_util.c src/json_util.c src/path_util.c src/asset.c src/mime.c src/notify.c src/pkg_installer.c src/pkg_info.c src/extract.c src/zip_extract.c src/rar_extract.c src/zipx_volume.c src/zipx_volstream.c
|
||||
PS5_SRCS := $(COMMON_SRCS) src/app_installer.c src/cpu_support_stub.c
|
||||
LINUX_SRCS := $(COMMON_SRCS)
|
||||
BASE_ASSETS := $(filter-out %.dds,$(wildcard assets/*))
|
||||
|
||||
@@ -773,9 +773,21 @@ function isRarSubVolume(item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function isZipSplitVolume(item) {
|
||||
if (item.type !== "-") return false;
|
||||
const name = item.name;
|
||||
// 分卷 ZIP 的任意一卷都能解压(引擎会自己找齐同目录的其余分卷):
|
||||
// name.zip.001…(7-Zip)/ name.part1.zip…(WinRAR)/ name.z01…+name.zip(Info-ZIP)
|
||||
if (/\.zip\.0*\d+$/i.test(name)) return true;
|
||||
if (/\.part0*\d+\.zip$/i.test(name)) return true;
|
||||
if (/\.z0*\d+$/i.test(name)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function isExtractableArchive(item) {
|
||||
if (item.type !== "-") return false;
|
||||
if (/\.zip$/i.test(item.name)) return true;
|
||||
if (isZipSplitVolume(item)) return true;
|
||||
if (isRarMainVolume(item)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
+131
-12
@@ -1,5 +1,6 @@
|
||||
#include "filemgr.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "path_util.h"
|
||||
#include "rar_extract.h"
|
||||
#include "zip_extract.h"
|
||||
#include "zipx_volume.h"
|
||||
|
||||
/* Cancellation callback: stop when the task is asked to cancel. */
|
||||
static int
|
||||
@@ -67,11 +69,135 @@ extract_progress(void *userdata, const zipx_progress_t *p) {
|
||||
delta, NULL);
|
||||
}
|
||||
|
||||
/* Case-insensitive substring search (strcasestr is not available on MinGW). */
|
||||
static const char *
|
||||
ci_strstr(const char *hay, const char *needle) {
|
||||
size_t nlen = strlen(needle);
|
||||
const char *p;
|
||||
|
||||
if(!nlen) {
|
||||
return hay;
|
||||
}
|
||||
for(p = hay; *p; p++) {
|
||||
size_t i;
|
||||
|
||||
for(i = 0; i < nlen; i++) {
|
||||
if(!p[i] ||
|
||||
tolower((unsigned char)p[i]) != tolower((unsigned char)needle[i])) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(i == nlen) {
|
||||
return p;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void
|
||||
extract_set_detail(zipx_result_t *result, const char *text) {
|
||||
size_t len = strlen(text);
|
||||
|
||||
if(len > sizeof(result->detail) - 1) {
|
||||
len = sizeof(result->detail) - 1;
|
||||
}
|
||||
memcpy(result->detail, text, len);
|
||||
result->detail[len] = 0;
|
||||
}
|
||||
|
||||
/* Which engine a volume set belongs to, decided from the member names:
|
||||
0 zip, 1 rar, 2 7z, -1 unknown. */
|
||||
static int
|
||||
volume_format(const zipx_volume_t *vol) {
|
||||
static const char *const exts[] = { ".zip", ".rar", ".7z", NULL };
|
||||
const char *best = NULL;
|
||||
int best_kind = -1;
|
||||
int i;
|
||||
int j;
|
||||
|
||||
for(i = 0; i < vol->count; i++) {
|
||||
for(j = 0; exts[j]; j++) {
|
||||
const char *hit = ci_strstr(vol->paths[i], exts[j]);
|
||||
|
||||
if(hit && (!best || hit > best)) {
|
||||
best = hit;
|
||||
best_kind = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
return best_kind;
|
||||
}
|
||||
|
||||
/* Removes the source archive once a task is done with it. For a split set
|
||||
every volume has to go: leaving the other parts behind would leave the user
|
||||
with something that still looks like a usable archive. */
|
||||
static void
|
||||
remove_source_archives(const char *path) {
|
||||
zipx_volume_t vol;
|
||||
char *err = NULL;
|
||||
int rc = zipx_volume_detect(path, &vol, &err);
|
||||
int i;
|
||||
|
||||
free(err);
|
||||
if(rc > 0) {
|
||||
for(i = 0; i < vol.count; i++) {
|
||||
unlink(vol.paths[i]);
|
||||
}
|
||||
zipx_volume_free(&vol);
|
||||
return;
|
||||
}
|
||||
unlink(path);
|
||||
}
|
||||
|
||||
/* Pick the right engine by the archive file name. Returns ZIPX_ERR_FORMAT
|
||||
for anything that does not look like a supported archive. */
|
||||
static zipx_status_t
|
||||
extract_dispatch(file_task_t *task, zipx_conflict_t conflict,
|
||||
const zipx_limits_t *limits, zipx_result_t *result) {
|
||||
zipx_volume_t vol;
|
||||
char *vol_err = NULL;
|
||||
int vrc = zipx_volume_detect(task->src, &vol, &vol_err);
|
||||
int kind = vrc > 0 ? volume_format(&vol) : -1;
|
||||
|
||||
if(vrc < 0) {
|
||||
/* A broken set gets the precise reason (which volume is missing, ...)
|
||||
instead of a generic "unsupported format". */
|
||||
extract_set_detail(result, task->src);
|
||||
snprintf(result->message, sizeof(result->message), "%s",
|
||||
vol_err ? vol_err : "the archive volumes are incomplete");
|
||||
free(vol_err);
|
||||
return ZIPX_ERR_OPEN;
|
||||
}
|
||||
free(vol_err);
|
||||
if(vrc > 0) {
|
||||
zipx_status_t status;
|
||||
|
||||
if(kind == 0) {
|
||||
status = zipx_extract(task->src, task->dst, conflict, limits,
|
||||
extract_cancel, extract_progress, task, result);
|
||||
} else if(kind == 1) {
|
||||
/* unrar chains its own volume naming (x.part1.rar); a byte contiguous
|
||||
set named x.rar.001 cannot be handed to it as-is. */
|
||||
extract_set_detail(result, task->src);
|
||||
snprintf(result->message, sizeof(result->message),
|
||||
"RAR volume sets named 'x.rar.001' are not supported yet "
|
||||
"(rename the parts to 'x.part1.rar', 'x.part2.rar', ...)");
|
||||
status = ZIPX_ERR_UNSUPPORTED;
|
||||
} else if(kind == 2) {
|
||||
extract_set_detail(result, task->src);
|
||||
snprintf(result->message, sizeof(result->message),
|
||||
"7z archives are not supported yet (repack as .zip or .rar)");
|
||||
status = ZIPX_ERR_UNSUPPORTED;
|
||||
} else {
|
||||
extract_set_detail(result, task->src);
|
||||
snprintf(result->message, sizeof(result->message),
|
||||
"unsupported split archive (only .zip, .rar and .7z volumes "
|
||||
"are recognised)");
|
||||
status = ZIPX_ERR_UNSUPPORTED;
|
||||
}
|
||||
zipx_volume_free(&vol);
|
||||
return status;
|
||||
}
|
||||
if(ends_with_ci(task->src, ".zip")) {
|
||||
return zipx_extract(task->src, task->dst, conflict, limits,
|
||||
extract_cancel, extract_progress, task, result);
|
||||
@@ -80,17 +206,10 @@ extract_dispatch(file_task_t *task, zipx_conflict_t conflict,
|
||||
return rar_extract(task->src, task->dst, conflict, limits,
|
||||
extract_cancel, extract_progress, task, result);
|
||||
}
|
||||
{
|
||||
size_t len = strlen(task->src);
|
||||
if(len > sizeof(result->detail) - 1) {
|
||||
len = sizeof(result->detail) - 1;
|
||||
}
|
||||
memcpy(result->detail, task->src, len);
|
||||
result->detail[len] = 0;
|
||||
snprintf(result->message, sizeof(result->message),
|
||||
"unsupported archive format (only .zip and .rar are accepted)");
|
||||
return ZIPX_ERR_UNSUPPORTED;
|
||||
}
|
||||
extract_set_detail(result, task->src);
|
||||
snprintf(result->message, sizeof(result->message),
|
||||
"unsupported archive format (only .zip and .rar are accepted)");
|
||||
return ZIPX_ERR_UNSUPPORTED;
|
||||
}
|
||||
|
||||
static const char *
|
||||
@@ -170,7 +289,7 @@ extract_worker(void *arg) {
|
||||
|
||||
/* Only delete the source archive when this task owns it (upload flow). */
|
||||
if(task->extract_remove_source && task->src[0]) {
|
||||
unlink(task->src);
|
||||
remove_source_archives(task->src);
|
||||
}
|
||||
pthread_mutex_lock(&g_tasks_lock);
|
||||
task->state = TASK_DONE;
|
||||
|
||||
+96
-9
@@ -22,6 +22,9 @@
|
||||
#include "mz_strm.h"
|
||||
#include "mz_strm_os.h"
|
||||
|
||||
#include "zipx_volume.h"
|
||||
#include "zipx_volstream.h"
|
||||
|
||||
#ifndef O_CLOEXEC
|
||||
#define O_CLOEXEC 0
|
||||
#endif
|
||||
@@ -1248,6 +1251,76 @@ publish_staging(zipx_ctx_t *c, const char *dst_dir, int dst_existed) {
|
||||
* public entry point
|
||||
**************************************************************************/
|
||||
|
||||
/* Opens the archive for scanning. A split set is served by the volume stream;
|
||||
because tools disagree on whether a split keeps absolute offsets or offsets
|
||||
relative to each volume, both layouts are attempted before giving up. */
|
||||
static int
|
||||
open_archive(const char *zip_path, zipx_volume_t *vol, int vol_set,
|
||||
zipx_ctx_t *c, void **zip_out, void **stream_out) {
|
||||
int attempts = vol_set ? 2 : 1;
|
||||
int attempt;
|
||||
|
||||
for(attempt = 0; attempt < attempts; attempt++) {
|
||||
void *stream = NULL;
|
||||
void *zip = NULL;
|
||||
int rc;
|
||||
|
||||
if(vol_set) {
|
||||
int mode = vol->mode;
|
||||
|
||||
if(attempt == 1) {
|
||||
mode = (vol->mode == ZIPX_VOL_MODE_DISK) ? ZIPX_VOL_MODE_CONCAT
|
||||
: ZIPX_VOL_MODE_DISK;
|
||||
}
|
||||
stream = zipx_volstream_create(mode);
|
||||
if(stream && zipx_volstream_set_parts(
|
||||
stream, (const char *const *)vol->paths,
|
||||
vol->count) != MZ_OK) {
|
||||
zipx_volstream_delete(&stream);
|
||||
stream = NULL;
|
||||
}
|
||||
} else {
|
||||
stream = mz_stream_os_create();
|
||||
}
|
||||
zip = mz_zip_create();
|
||||
if(!stream || !zip) {
|
||||
if(stream) {
|
||||
mz_stream_delete(&stream);
|
||||
}
|
||||
if(zip) {
|
||||
mz_zip_delete(&zip);
|
||||
}
|
||||
ctx_fail(c, ZIPX_ERR_INTERNAL, zip_path, "out of memory");
|
||||
return (int)c->result->status;
|
||||
}
|
||||
rc = mz_stream_open(stream, vol_set ? vol->paths[0] : zip_path,
|
||||
MZ_OPEN_MODE_READ);
|
||||
if(rc == MZ_OK) {
|
||||
rc = mz_zip_open(zip, stream, MZ_OPEN_MODE_READ);
|
||||
}
|
||||
if(rc == MZ_OK) {
|
||||
*zip_out = zip;
|
||||
*stream_out = stream;
|
||||
return ZIPX_OK;
|
||||
}
|
||||
/* Wrong layout for this set (or a corrupt archive): drop it and retry. */
|
||||
mz_zip_close(zip);
|
||||
mz_zip_delete(&zip);
|
||||
mz_stream_close(stream);
|
||||
mz_stream_delete(&stream);
|
||||
}
|
||||
|
||||
if(vol_set) {
|
||||
ctx_fail(c, ZIPX_ERR_OPEN, zip_path,
|
||||
"cannot read the split archive starting at '%s' (%d volumes): %s",
|
||||
vol->paths[0], vol->count,
|
||||
errno ? strerror(errno) : "no known volume layout matched");
|
||||
} else {
|
||||
ctx_fail(c, ZIPX_ERR_OPEN, zip_path, "%s", strerror(errno ? errno : EIO));
|
||||
}
|
||||
return (int)c->result->status;
|
||||
}
|
||||
|
||||
zipx_status_t
|
||||
zipx_extract(const char *zip_path, const char *dst_dir,
|
||||
zipx_conflict_t conflict, const zipx_limits_t *limits,
|
||||
@@ -1263,6 +1336,8 @@ zipx_extract(const char *zip_path, const char *dst_dir,
|
||||
int root_fd = -1;
|
||||
int dst_existed = 0;
|
||||
int status = ZIPX_OK;
|
||||
zipx_volume_t vol;
|
||||
int vol_set = 0;
|
||||
|
||||
if(!result || !zip_path || !dst_dir || !dst_dir[0]) {
|
||||
if(result) {
|
||||
@@ -1274,6 +1349,8 @@ zipx_extract(const char *zip_path, const char *dst_dir,
|
||||
}
|
||||
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
memset(&vol, 0, sizeof(vol));
|
||||
vol.index = -1;
|
||||
memset(result, 0, sizeof(*result));
|
||||
c->result = result;
|
||||
c->conflict = conflict;
|
||||
@@ -1305,16 +1382,25 @@ zipx_extract(const char *zip_path, const char *dst_dir,
|
||||
goto done;
|
||||
}
|
||||
|
||||
stream = mz_stream_os_create();
|
||||
zip = mz_zip_create();
|
||||
if(!stream || !zip) {
|
||||
status = ctx_fail(c, ZIPX_ERR_INTERNAL, zip_path, "out of memory");
|
||||
goto done;
|
||||
{
|
||||
char *vol_err = NULL;
|
||||
int vrc = zipx_volume_detect(zip_path, &vol, &vol_err);
|
||||
|
||||
if(vrc < 0) {
|
||||
/* A volume of a set that is incomplete gets a precise message here
|
||||
instead of a generic "cannot open" further down. */
|
||||
status = ctx_fail(c, ZIPX_ERR_OPEN, zip_path, "%s",
|
||||
vol_err ? vol_err : "cannot read the volume set");
|
||||
free(vol_err);
|
||||
goto done;
|
||||
}
|
||||
free(vol_err);
|
||||
if(vrc > 0) {
|
||||
vol_set = 1;
|
||||
}
|
||||
}
|
||||
if(mz_stream_open(stream, zip_path, MZ_OPEN_MODE_READ) ||
|
||||
mz_zip_open(zip, stream, MZ_OPEN_MODE_READ)) {
|
||||
status = ctx_fail(c, ZIPX_ERR_OPEN, zip_path, "%s",
|
||||
strerror(errno ? errno : EIO));
|
||||
status = open_archive(zip_path, &vol, vol_set, c, &zip, &stream);
|
||||
if(status != ZIPX_OK) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
@@ -1375,6 +1461,7 @@ done:
|
||||
mz_stream_close(stream);
|
||||
mz_stream_delete(&stream);
|
||||
}
|
||||
zipx_volume_free(&vol);
|
||||
result->entries_total = c->entries_total;
|
||||
result->entries_done = c->entries_done;
|
||||
result->bytes_total = c->bytes_total;
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
/* zipx_volstream -- present a multi-file archive volume set as one stream.
|
||||
part of ps5-web-file-manager
|
||||
|
||||
See zipx_volstream.h for the two split layouts this supports. */
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "mz.h"
|
||||
#include "mz_os.h"
|
||||
#include "mz_strm.h"
|
||||
#include "mz_strm_os.h"
|
||||
|
||||
#include "zipx_volstream.h"
|
||||
|
||||
#define VOL_INT32_MAX 0x7fffffffLL
|
||||
|
||||
typedef struct {
|
||||
mz_stream stream; /* first member: callbacks cast the handle to this */
|
||||
char **paths; /* ordered part paths */
|
||||
int32_t count;
|
||||
int64_t *prefix; /* count + 1 entries, prefix[count] == total */
|
||||
int64_t total;
|
||||
int32_t mode; /* ZIPX_VOL_MODE_* */
|
||||
int32_t disk; /* active part index */
|
||||
int64_t pos; /* absolute position inside the concatenated set */
|
||||
int32_t os_part; /* part currently held by os, -1 when nothing is open */
|
||||
void *os;
|
||||
int32_t opened;
|
||||
int32_t error;
|
||||
} zipx_volstream_t;
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static int32_t
|
||||
vol_use_part(zipx_volstream_t *v, int32_t part) {
|
||||
if(v->os_part == part) {
|
||||
return MZ_OK;
|
||||
}
|
||||
if(v->os_part >= 0) {
|
||||
mz_stream_close(v->os);
|
||||
v->os_part = -1;
|
||||
}
|
||||
if(mz_stream_open(v->os, v->paths[part], MZ_OPEN_MODE_READ) != MZ_OK) {
|
||||
v->error = MZ_OPEN_ERROR;
|
||||
return MZ_OPEN_ERROR;
|
||||
}
|
||||
v->os_part = part;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
/* Part holding an absolute offset, walking from `hint` (parts are laid out in
|
||||
order and reads are sequential, so this stays O(1) amortised). */
|
||||
static int32_t
|
||||
vol_part_of(zipx_volstream_t *v, int64_t pos, int32_t hint) {
|
||||
int32_t i;
|
||||
|
||||
if(pos < 0 || pos >= v->total) {
|
||||
return -1;
|
||||
}
|
||||
i = hint;
|
||||
if(i < 0) {
|
||||
i = 0;
|
||||
}
|
||||
if(i > v->count - 1) {
|
||||
i = v->count - 1;
|
||||
}
|
||||
while(i > 0 && pos < v->prefix[i]) {
|
||||
i--;
|
||||
}
|
||||
while(i < v->count - 1 && pos >= v->prefix[i + 1]) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
vol_open(void *stream, const char *path, int32_t mode) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
int64_t sum = 0;
|
||||
int32_t i;
|
||||
|
||||
(void)path;
|
||||
(void)mode;
|
||||
if(!v || v->count <= 0 || !v->paths) {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
v->prefix = (int64_t *)calloc((size_t)v->count + 1, sizeof(*v->prefix));
|
||||
if(!v->prefix) {
|
||||
v->error = MZ_MEM_ERROR;
|
||||
return MZ_MEM_ERROR;
|
||||
}
|
||||
for(i = 0; i < v->count; i++) {
|
||||
int64_t size = mz_os_get_file_size(v->paths[i]);
|
||||
|
||||
if(size < 0) {
|
||||
v->error = MZ_OPEN_ERROR;
|
||||
return MZ_OPEN_ERROR;
|
||||
}
|
||||
v->prefix[i] = sum;
|
||||
sum += size;
|
||||
}
|
||||
v->prefix[v->count] = sum;
|
||||
v->total = sum;
|
||||
|
||||
if(!v->os) {
|
||||
v->os = mz_stream_os_create();
|
||||
if(!v->os) {
|
||||
v->error = MZ_MEM_ERROR;
|
||||
return MZ_MEM_ERROR;
|
||||
}
|
||||
}
|
||||
/* The end-of-central-directory record sits on the last disk, so a split
|
||||
disk set starts there; a byte split is read from its first byte. */
|
||||
v->disk = (v->mode == ZIPX_VOL_MODE_DISK) ? v->count - 1 : 0;
|
||||
v->pos = v->prefix[v->disk];
|
||||
v->os_part = -1;
|
||||
v->opened = 1;
|
||||
v->error = MZ_OK;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
vol_is_open(void *stream) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
|
||||
return (v && v->opened) ? MZ_OK : MZ_OPEN_ERROR;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
vol_read(void *stream, void *buf, int32_t size) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
int32_t done = 0;
|
||||
int32_t hint;
|
||||
|
||||
if(!v || !v->opened || !buf) {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
if(size <= 0) {
|
||||
return 0;
|
||||
}
|
||||
hint = v->os_part >= 0 ? v->os_part : 0;
|
||||
while(done < size) {
|
||||
int32_t part = vol_part_of(v, v->pos, hint);
|
||||
int64_t in_part;
|
||||
int64_t avail;
|
||||
int64_t want;
|
||||
int32_t got;
|
||||
|
||||
if(part < 0) {
|
||||
break; /* end of the logical archive */
|
||||
}
|
||||
hint = part;
|
||||
if(vol_use_part(v, part) != MZ_OK) {
|
||||
break;
|
||||
}
|
||||
in_part = v->pos - v->prefix[part];
|
||||
avail = (v->prefix[part + 1] - v->prefix[part]) - in_part;
|
||||
if(avail <= 0) { /* empty part: step over it */
|
||||
v->pos = v->prefix[part + 1];
|
||||
continue;
|
||||
}
|
||||
want = (int64_t)(size - done);
|
||||
if(want > avail) {
|
||||
want = avail;
|
||||
}
|
||||
if(want > VOL_INT32_MAX) {
|
||||
want = VOL_INT32_MAX;
|
||||
}
|
||||
if(mz_stream_tell(v->os) != in_part) {
|
||||
if(mz_stream_seek(v->os, in_part, MZ_SEEK_SET) != MZ_OK) {
|
||||
v->error = MZ_SEEK_ERROR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
got = mz_stream_read(v->os, (uint8_t *)buf + done, (int32_t)want);
|
||||
if(got <= 0) {
|
||||
if(got < 0) {
|
||||
v->error = MZ_READ_ERROR;
|
||||
}
|
||||
break;
|
||||
}
|
||||
done += got;
|
||||
v->pos += got;
|
||||
if(got < (int32_t)want) {
|
||||
break; /* short read: let the caller come back */
|
||||
}
|
||||
}
|
||||
if(done == 0 && v->error != MZ_OK) {
|
||||
return v->error;
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
vol_write(void *stream, const void *buf, int32_t size) {
|
||||
(void)stream;
|
||||
(void)buf;
|
||||
(void)size;
|
||||
return MZ_SUPPORT_ERROR;
|
||||
}
|
||||
|
||||
static int64_t
|
||||
vol_tell(void *stream) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
|
||||
if(!v || !v->opened) {
|
||||
return -1;
|
||||
}
|
||||
if(v->mode == ZIPX_VOL_MODE_DISK) {
|
||||
return v->pos - v->prefix[v->disk];
|
||||
}
|
||||
return v->pos;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
vol_seek(void *stream, int64_t offset, int32_t origin) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
int64_t target;
|
||||
|
||||
if(!v || !v->opened) {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
if(origin == MZ_SEEK_SET) {
|
||||
target = offset;
|
||||
if(v->mode == ZIPX_VOL_MODE_DISK) {
|
||||
target += v->prefix[v->disk]; /* offsets are disk relative there */
|
||||
}
|
||||
} else if(origin == MZ_SEEK_CUR) {
|
||||
target = v->pos + offset;
|
||||
} else if(origin == MZ_SEEK_END) {
|
||||
target = v->total + offset;
|
||||
} else {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
if(target < 0) {
|
||||
target = 0;
|
||||
}
|
||||
if(target > v->total) {
|
||||
target = v->total;
|
||||
}
|
||||
v->pos = target;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
vol_close(void *stream) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
|
||||
if(!v) {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
if(v->os && v->os_part >= 0) {
|
||||
mz_stream_close(v->os);
|
||||
v->os_part = -1;
|
||||
}
|
||||
v->opened = 0;
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
vol_error(void *stream) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
|
||||
return v ? v->error : MZ_PARAM_ERROR;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
vol_get_prop(void *stream, int32_t prop, int64_t *value) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
|
||||
if(!v || !value) {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
if(v->mode != ZIPX_VOL_MODE_DISK) {
|
||||
/* A byte split keeps absolute offsets, so the disk properties must look
|
||||
unsupported: minizip-ng then leaves every offset alone. */
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
if(prop == MZ_STREAM_PROP_DISK_NUMBER) {
|
||||
*value = v->disk;
|
||||
return MZ_OK;
|
||||
}
|
||||
if(prop == MZ_STREAM_PROP_DISK_SIZE) {
|
||||
*value = v->prefix[v->disk + 1] - v->prefix[v->disk];
|
||||
return MZ_OK;
|
||||
}
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
|
||||
static int32_t
|
||||
vol_set_prop(void *stream, int32_t prop, int64_t value) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
|
||||
if(!v) {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
if(prop != MZ_STREAM_PROP_DISK_NUMBER || v->mode != ZIPX_VOL_MODE_DISK) {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
if(value < 0) {
|
||||
/* minizip-ng passes -1 for entries that live on the same disk as the
|
||||
central directory, which is the final volume of the set. */
|
||||
v->disk = v->count - 1;
|
||||
v->pos = v->prefix[v->disk];
|
||||
return MZ_OK;
|
||||
}
|
||||
if(value >= v->count) {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
v->disk = (int32_t)value;
|
||||
v->pos = v->prefix[v->disk];
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
static void vol_destroy(void **stream);
|
||||
|
||||
static mz_stream_vtbl vol_vtbl = {
|
||||
vol_open, vol_is_open, vol_read, vol_write, vol_tell, vol_seek, vol_close,
|
||||
vol_error, NULL, vol_destroy, vol_get_prop, vol_set_prop,
|
||||
};
|
||||
|
||||
static void *
|
||||
vol_create(void) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)calloc(1, sizeof(*v));
|
||||
|
||||
if(!v) {
|
||||
return NULL;
|
||||
}
|
||||
v->stream.vtbl = &vol_vtbl;
|
||||
v->os_part = -1;
|
||||
return (void *)&v->stream;
|
||||
}
|
||||
|
||||
static void
|
||||
vol_destroy(void **stream) {
|
||||
zipx_volstream_t *v;
|
||||
int32_t i;
|
||||
|
||||
if(!stream || !*stream) {
|
||||
return;
|
||||
}
|
||||
v = (zipx_volstream_t *)*stream;
|
||||
vol_close(&v->stream);
|
||||
if(v->os) {
|
||||
mz_stream_os_delete(&v->os);
|
||||
}
|
||||
if(v->paths) {
|
||||
for(i = 0; i < v->count; i++) {
|
||||
free(v->paths[i]);
|
||||
}
|
||||
free(v->paths);
|
||||
}
|
||||
free(v->prefix);
|
||||
free(v);
|
||||
*stream = NULL;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
void *
|
||||
zipx_volstream_create(int32_t mode) {
|
||||
void *stream = vol_create();
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
|
||||
if(!v) {
|
||||
return NULL;
|
||||
}
|
||||
v->mode = (mode == ZIPX_VOL_MODE_DISK) ? ZIPX_VOL_MODE_DISK :
|
||||
ZIPX_VOL_MODE_CONCAT;
|
||||
return stream;
|
||||
}
|
||||
|
||||
void
|
||||
zipx_volstream_delete(void **stream) {
|
||||
mz_stream_delete(stream); /* routes through vtbl->destroy */
|
||||
}
|
||||
|
||||
int32_t
|
||||
zipx_volstream_set_parts(void *stream, const char *const *paths, int32_t count) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
int32_t i;
|
||||
|
||||
if(!v || !paths || count <= 0) {
|
||||
return MZ_PARAM_ERROR;
|
||||
}
|
||||
v->paths = (char **)calloc((size_t)count, sizeof(*v->paths));
|
||||
if(!v->paths) {
|
||||
return MZ_MEM_ERROR;
|
||||
}
|
||||
v->count = count;
|
||||
for(i = 0; i < count; i++) {
|
||||
size_t len = strlen(paths[i]) + 1;
|
||||
|
||||
v->paths[i] = (char *)malloc(len);
|
||||
if(!v->paths[i]) {
|
||||
return MZ_MEM_ERROR;
|
||||
}
|
||||
memcpy(v->paths[i], paths[i], len);
|
||||
}
|
||||
return MZ_OK;
|
||||
}
|
||||
|
||||
int64_t
|
||||
zipx_volstream_total(void *stream) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
|
||||
return v ? v->total : -1;
|
||||
}
|
||||
|
||||
const char *
|
||||
zipx_volstream_describe(void *stream, char *buf, unsigned int size) {
|
||||
zipx_volstream_t *v = (zipx_volstream_t *)stream;
|
||||
const char *base;
|
||||
|
||||
if(!buf || size == 0) {
|
||||
return "";
|
||||
}
|
||||
if(!v || v->count <= 0) {
|
||||
snprintf(buf, size, "(no volumes)");
|
||||
return buf;
|
||||
}
|
||||
base = strrchr(v->paths[0], '/');
|
||||
#ifdef _WIN32
|
||||
{
|
||||
const char *alt = strrchr(v->paths[0], '\\');
|
||||
if(alt && (!base || alt > base)) {
|
||||
base = alt;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
base = base ? base + 1 : v->paths[0];
|
||||
snprintf(buf, size, "%s (%d volumes)", base, (int)v->count);
|
||||
return buf;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/* zipx_volstream -- present a multi-file archive volume set as one stream.
|
||||
part of ps5-web-file-manager
|
||||
|
||||
Two split layouts exist in the wild and they need different behaviour:
|
||||
|
||||
ZIPX_VOL_MODE_CONCAT (byte split)
|
||||
`name.zip.001`, `name.zip.002`, ... (7-Zip) and `name.part1.zip`,
|
||||
`name.part2.zip` (WinRAR). Each part is a byte slice of one archive, so
|
||||
byte N of the logical archive is byte N of the concatenation and every
|
||||
offset stored inside the archive is already absolute. Offsets are passed
|
||||
through untouched and the disk properties are reported as unsupported so
|
||||
minizip-ng keeps using absolute offsets.
|
||||
|
||||
ZIPX_VOL_MODE_DISK (zip split disks)
|
||||
`name.z01`, `name.z02`, ..., `name.zip` (Info-ZIP / PKZIP style). The
|
||||
central directory stores the offset of a local header relative to the
|
||||
disk it starts on, so minizip-ng switches the active disk through
|
||||
MZ_STREAM_PROP_DISK_NUMBER before seeking. Seek/tell are relative to the
|
||||
active disk here, which is exactly what mz_zip_entry_seek_local_header
|
||||
expects, and the stream starts on the last disk because that is where the
|
||||
end-of-central-directory record lives. */
|
||||
|
||||
#ifndef ZIPX_VOLSTREAM_H
|
||||
#define ZIPX_VOLSTREAM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define ZIPX_VOL_MODE_CONCAT 0
|
||||
#define ZIPX_VOL_MODE_DISK 1
|
||||
|
||||
/* Creates a stream handle; pass it to mz_stream_open() afterwards. */
|
||||
void *zipx_volstream_create(int32_t mode);
|
||||
|
||||
/* Deletes a handle created above (safe with *stream == NULL). */
|
||||
void zipx_volstream_delete(void **stream);
|
||||
|
||||
/* Copies the ordered part paths into the handle. Must be called before the
|
||||
stream is opened. Returns MZ_OK (0) or MZ_MEM_ERROR (-4). */
|
||||
int32_t zipx_volstream_set_parts(void *stream, const char *const *paths,
|
||||
int32_t count);
|
||||
|
||||
/* Total logical size (sum of the part sizes), or -1 when not resolved. */
|
||||
int64_t zipx_volstream_total(void *stream);
|
||||
|
||||
/* Human readable description of the set, e.g. "name.z01 (3 volumes)".
|
||||
Writes into buf and returns buf. */
|
||||
const char *zipx_volstream_describe(void *stream, char *buf, unsigned int size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,601 @@
|
||||
/* zipx_volume -- see zipx_volume.h for what this groups and why. */
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "zipx_volstream.h"
|
||||
#include "zipx_volume.h"
|
||||
|
||||
typedef struct {
|
||||
long num;
|
||||
char *path;
|
||||
} vol_part_t;
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* path helpers */
|
||||
|
||||
static const char *
|
||||
file_base(const char *path) {
|
||||
const char *slash = strrchr(path, '/');
|
||||
const char *back = strrchr(path, '\\');
|
||||
|
||||
if(back && (!slash || back > slash)) {
|
||||
slash = back;
|
||||
}
|
||||
return slash ? slash + 1 : path;
|
||||
}
|
||||
|
||||
/* Directory part without a trailing separator; "" for a bare filename. */
|
||||
static void
|
||||
file_dir(const char *path, char *buf, size_t size) {
|
||||
const char *base = file_base(path);
|
||||
size_t len = (size_t)(base - path);
|
||||
|
||||
while(len > 0 && (path[len - 1] == '/' || path[len - 1] == '\\')) {
|
||||
len--;
|
||||
}
|
||||
if(len >= size) {
|
||||
len = size - 1;
|
||||
}
|
||||
memcpy(buf, path, len);
|
||||
buf[len] = 0;
|
||||
}
|
||||
|
||||
static int
|
||||
ends_with_ci(const char *s, const char *suffix) {
|
||||
size_t ls = strlen(s);
|
||||
size_t lf = strlen(suffix);
|
||||
|
||||
if(lf > ls) {
|
||||
return 0;
|
||||
}
|
||||
return strcasecmp(s + ls - lf, suffix) == 0;
|
||||
}
|
||||
|
||||
static int
|
||||
file_exists(const char *path) {
|
||||
struct stat st;
|
||||
|
||||
return stat(path, &st) == 0;
|
||||
}
|
||||
|
||||
static char *
|
||||
vol_join(const char *dir, const char *name) {
|
||||
size_t need = strlen(dir) + strlen(name) + 2;
|
||||
char *out = (char *)malloc(need);
|
||||
|
||||
if(!out) {
|
||||
return NULL;
|
||||
}
|
||||
if(dir[0]) {
|
||||
snprintf(out, need, "%s/%s", dir, name);
|
||||
} else {
|
||||
snprintf(out, need, "%s", name);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static char *
|
||||
err_printf(const char *fmt, ...) {
|
||||
va_list ap;
|
||||
char *buf;
|
||||
int need;
|
||||
|
||||
va_start(ap, fmt);
|
||||
need = vsnprintf(NULL, 0, fmt, ap);
|
||||
va_end(ap);
|
||||
if(need < 0) {
|
||||
return NULL;
|
||||
}
|
||||
buf = (char *)malloc((size_t)need + 1);
|
||||
if(!buf) {
|
||||
return NULL;
|
||||
}
|
||||
va_start(ap, fmt);
|
||||
vsnprintf(buf, (size_t)need + 1, fmt, ap);
|
||||
va_end(ap);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* part collection */
|
||||
|
||||
/* Matches "PREFIX<digits>SUFFIX"; returns 1 and the value on a match. */
|
||||
static int
|
||||
match_numbered(const char *entry, const char *prefix, const char *suffix,
|
||||
long *num) {
|
||||
size_t plen = strlen(prefix);
|
||||
size_t slen = strlen(suffix);
|
||||
const char *p;
|
||||
char *end = NULL;
|
||||
long value;
|
||||
|
||||
if(strncmp(entry, prefix, plen) != 0) {
|
||||
return 0;
|
||||
}
|
||||
p = entry + plen;
|
||||
if(!isdigit((unsigned char)*p)) {
|
||||
return 0;
|
||||
}
|
||||
value = strtol(p, &end, 10);
|
||||
if(end == p) {
|
||||
return 0;
|
||||
}
|
||||
if(slen > 0) {
|
||||
if(strcmp(end, suffix) != 0) {
|
||||
return 0;
|
||||
}
|
||||
} else if(*end != 0) {
|
||||
return 0;
|
||||
}
|
||||
*num = value;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int
|
||||
part_compare(const void *a, const void *b) {
|
||||
const vol_part_t *pa = (const vol_part_t *)a;
|
||||
const vol_part_t *pb = (const vol_part_t *)b;
|
||||
|
||||
if(pa->num < pb->num) {
|
||||
return -1;
|
||||
}
|
||||
if(pa->num > pb->num) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
free_parts(vol_part_t *parts, int count) {
|
||||
int i;
|
||||
|
||||
for(i = 0; i < count; i++) {
|
||||
free(parts[i].path);
|
||||
parts[i].path = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Collects every entry in `dir` matching PREFIX<digits>SUFFIX, ordered by the
|
||||
number. Returns the count, or -1 when the directory cannot be listed (with
|
||||
*err set) or the set is larger than `max`. */
|
||||
static int
|
||||
scan_parts(const char *dir, const char *prefix, const char *suffix,
|
||||
vol_part_t *parts, int max, char **err) {
|
||||
const char *target = dir[0] ? dir : ".";
|
||||
DIR *d = opendir(target);
|
||||
struct dirent *ent;
|
||||
int count = 0;
|
||||
|
||||
if(!d) {
|
||||
if(err && !*err) {
|
||||
*err = err_printf("cannot list the directory '%s' that holds the other "
|
||||
"volumes", target);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
while((ent = readdir(d)) != NULL) {
|
||||
long num = 0;
|
||||
|
||||
if(!match_numbered(ent->d_name, prefix, suffix, &num)) {
|
||||
continue;
|
||||
}
|
||||
if(count >= max) {
|
||||
if(err && !*err) {
|
||||
*err = err_printf("volume set has more than %d parts, the supported "
|
||||
"maximum", max);
|
||||
}
|
||||
free_parts(parts, count);
|
||||
closedir(d);
|
||||
return -1;
|
||||
}
|
||||
parts[count].num = num;
|
||||
parts[count].path = vol_join(dir, ent->d_name);
|
||||
if(!parts[count].path) {
|
||||
free_parts(parts, count + 1);
|
||||
closedir(d);
|
||||
return -1;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
closedir(d);
|
||||
qsort(parts, (size_t)count, sizeof(*parts), part_compare);
|
||||
return count;
|
||||
}
|
||||
|
||||
/* Verifies the parts are numbered 1..count with no gap (and no duplicate),
|
||||
filling *err with the exact missing name when they are not. */
|
||||
static int
|
||||
check_contiguous(vol_part_t *parts, int count, const char *dir,
|
||||
const char *prefix, const char *suffix, int width,
|
||||
char **err) {
|
||||
int i;
|
||||
|
||||
for(i = 0; i < count; i++) {
|
||||
if(parts[i].num != (long)(i + 1)) {
|
||||
if(err && !*err) {
|
||||
char name[512];
|
||||
char *full;
|
||||
|
||||
snprintf(name, sizeof(name), "%s%0*ld%s", prefix, width,
|
||||
(long)(i + 1), suffix);
|
||||
full = vol_join(dir, name);
|
||||
*err = err_printf("volume set is incomplete: '%s' is missing",
|
||||
full ? full : name);
|
||||
free(full);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* volume set bookkeeping */
|
||||
|
||||
static void
|
||||
volume_reset(zipx_volume_t *vol) {
|
||||
memset(vol, 0, sizeof(*vol));
|
||||
vol->index = -1;
|
||||
}
|
||||
|
||||
static int
|
||||
volume_take(zipx_volume_t *vol, vol_part_t *parts, int count, int mode,
|
||||
const char *selected) {
|
||||
int i;
|
||||
|
||||
vol->paths = (char **)calloc((size_t)count, sizeof(*vol->paths));
|
||||
if(!vol->paths) {
|
||||
return -1;
|
||||
}
|
||||
for(i = 0; i < count; i++) {
|
||||
vol->paths[i] = parts[i].path;
|
||||
parts[i].path = NULL; /* ownership moves into vol */
|
||||
}
|
||||
vol->count = count;
|
||||
vol->mode = mode;
|
||||
vol->is_set = 1;
|
||||
vol->index = -1;
|
||||
for(i = 0; i < count; i++) {
|
||||
if(selected && strcmp(vol->paths[i], selected) == 0) {
|
||||
vol->index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* detection: name.zip.001 / name.7z.001 / name.rar.001 */
|
||||
|
||||
static int
|
||||
detect_digit_suffix(const char *dir, const char *base, const char *selected,
|
||||
zipx_volume_t *vol, char **err) {
|
||||
static const char *const known[] = { "zip", "7z", "rar", NULL };
|
||||
const char *dot = strrchr(base, '.');
|
||||
vol_part_t parts[ZIPX_VOL_MAX_PARTS];
|
||||
char stem[2048];
|
||||
char prefix[2100];
|
||||
size_t stem_len;
|
||||
int count;
|
||||
int i;
|
||||
int known_ext = 0;
|
||||
|
||||
if(!dot || dot == base || !dot[1]) {
|
||||
return 0;
|
||||
}
|
||||
for(i = 1; dot[i]; i++) {
|
||||
if(!isdigit((unsigned char)dot[i])) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
stem_len = (size_t)(dot - base);
|
||||
if(stem_len >= sizeof(stem)) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(stem, base, stem_len);
|
||||
stem[stem_len] = 0;
|
||||
for(i = 0; known[i]; i++) {
|
||||
char tail[8];
|
||||
|
||||
snprintf(tail, sizeof(tail), ".%s", known[i]);
|
||||
if(ends_with_ci(stem, tail)) {
|
||||
known_ext = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!known_ext) {
|
||||
return 0; /* "backup.001" style names are not archive volumes */
|
||||
}
|
||||
snprintf(prefix, sizeof(prefix), "%s.", stem);
|
||||
count = scan_parts(dir, prefix, "", parts, ZIPX_VOL_MAX_PARTS, err);
|
||||
if(count < 0) {
|
||||
return -1;
|
||||
}
|
||||
if(count <= 1) {
|
||||
free_parts(parts, count > 0 ? count : 0);
|
||||
if(count == 1 && err && !*err) {
|
||||
*err = err_printf("'%s' is the first volume of a split archive but no "
|
||||
"other volumes ('%s.002', ...) are present", base,
|
||||
stem);
|
||||
}
|
||||
return count == 1 ? -1 : 0;
|
||||
}
|
||||
if(check_contiguous(parts, count, dir, prefix, "", 3, err)) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
if(volume_take(vol, parts, count, ZIPX_VOL_MODE_CONCAT, selected)) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* detection: name.z01 ... name.zip */
|
||||
|
||||
static int
|
||||
detect_z_suffix(const char *dir, const char *base, const char *selected,
|
||||
zipx_volume_t *vol, char **err) {
|
||||
const char *dot = strrchr(base, '.');
|
||||
vol_part_t parts[ZIPX_VOL_MAX_PARTS];
|
||||
char stem[2048];
|
||||
char prefix[2100];
|
||||
size_t stem_len;
|
||||
int count;
|
||||
|
||||
if(!dot || dot == base) {
|
||||
return 0;
|
||||
}
|
||||
if((dot[1] != 'z' && dot[1] != 'Z') || !isdigit((unsigned char)dot[2])) {
|
||||
return 0;
|
||||
}
|
||||
{
|
||||
int i;
|
||||
|
||||
for(i = 2; dot[i]; i++) {
|
||||
if(!isdigit((unsigned char)dot[i])) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
stem_len = (size_t)(dot - base);
|
||||
if(stem_len >= sizeof(stem)) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(stem, base, stem_len);
|
||||
stem[stem_len] = 0;
|
||||
|
||||
snprintf(prefix, sizeof(prefix), "%s.z", stem);
|
||||
count = scan_parts(dir, prefix, "", parts, ZIPX_VOL_MAX_PARTS - 1, err);
|
||||
if(count < 0) {
|
||||
return -1;
|
||||
}
|
||||
if(count == 0) {
|
||||
return 0;
|
||||
}
|
||||
if(check_contiguous(parts, count, dir, prefix, "", 2, err)) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
/* The central directory always lives in "name.zip", the final volume. */
|
||||
{
|
||||
char name[2100];
|
||||
char *last;
|
||||
|
||||
snprintf(name, sizeof(name), "%s.zip", stem);
|
||||
last = vol_join(dir, name);
|
||||
if(!last) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
if(!file_exists(last)) {
|
||||
if(err && !*err) {
|
||||
*err = err_printf("volume set is incomplete: the last volume '%s' that "
|
||||
"holds the archive index is missing", name);
|
||||
}
|
||||
free(last);
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
parts[count].num = (long)count + 1;
|
||||
parts[count].path = last;
|
||||
count++;
|
||||
}
|
||||
if(volume_take(vol, parts, count, ZIPX_VOL_MODE_DISK, selected)) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* detection: the final "name.zip" of a name.z01 ... name.zip set */
|
||||
|
||||
static int
|
||||
detect_zip_tail(const char *dir, const char *base, const char *selected,
|
||||
zipx_volume_t *vol, char **err) {
|
||||
vol_part_t parts[ZIPX_VOL_MAX_PARTS];
|
||||
char stem[2048];
|
||||
char prefix[2100];
|
||||
size_t stem_len;
|
||||
int count;
|
||||
|
||||
if(!ends_with_ci(base, ".zip")) {
|
||||
return 0;
|
||||
}
|
||||
stem_len = strlen(base) - 4;
|
||||
if(stem_len == 0 || stem_len >= sizeof(stem)) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(stem, base, stem_len);
|
||||
stem[stem_len] = 0;
|
||||
|
||||
snprintf(prefix, sizeof(prefix), "%s.z", stem);
|
||||
count = scan_parts(dir, prefix, "", parts, ZIPX_VOL_MAX_PARTS - 1, err);
|
||||
if(count < 0) {
|
||||
return -1;
|
||||
}
|
||||
if(count == 0) {
|
||||
return 0; /* an ordinary single volume archive */
|
||||
}
|
||||
if(check_contiguous(parts, count, dir, prefix, "", 2, err)) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
char *last = vol_join(dir, base);
|
||||
|
||||
if(!last) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
parts[count].num = (long)count + 1;
|
||||
parts[count].path = last;
|
||||
count++;
|
||||
}
|
||||
if(volume_take(vol, parts, count, ZIPX_VOL_MODE_DISK, selected)) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
/* detection: name.part1.zip ... */
|
||||
|
||||
static int
|
||||
detect_part_suffix(const char *dir, const char *base, const char *selected,
|
||||
zipx_volume_t *vol, char **err) {
|
||||
vol_part_t parts[ZIPX_VOL_MAX_PARTS];
|
||||
char stem[2048];
|
||||
char prefix[2100];
|
||||
char trimmed[2048];
|
||||
const char *dot;
|
||||
size_t len;
|
||||
int count;
|
||||
|
||||
if(!ends_with_ci(base, ".zip")) {
|
||||
return 0;
|
||||
}
|
||||
len = strlen(base) - 4;
|
||||
if(len == 0 || len >= sizeof(trimmed)) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(trimmed, base, len);
|
||||
trimmed[len] = 0;
|
||||
dot = strrchr(trimmed, '.');
|
||||
if(!dot || dot == trimmed || strncasecmp(dot, ".part", 5) != 0 ||
|
||||
!isdigit((unsigned char)dot[5])) {
|
||||
return 0;
|
||||
}
|
||||
{
|
||||
size_t stem_len = (size_t)(dot - trimmed);
|
||||
|
||||
if(stem_len >= sizeof(stem)) {
|
||||
return 0;
|
||||
}
|
||||
memcpy(stem, trimmed, stem_len);
|
||||
stem[stem_len] = 0;
|
||||
}
|
||||
snprintf(prefix, sizeof(prefix), "%s.part", stem);
|
||||
count = scan_parts(dir, prefix, ".zip", parts, ZIPX_VOL_MAX_PARTS, err);
|
||||
if(count < 0) {
|
||||
return -1;
|
||||
}
|
||||
if(count <= 1) {
|
||||
free_parts(parts, count > 0 ? count : 0);
|
||||
if(count == 1 && err && !*err) {
|
||||
*err = err_printf("'%s' is a volume of a split archive but the other "
|
||||
"volumes ('%s.part1.zip', ...) are missing", base, stem);
|
||||
}
|
||||
return count == 1 ? -1 : 0;
|
||||
}
|
||||
if(check_contiguous(parts, count, dir, prefix, ".zip", 1, err)) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
if(volume_take(vol, parts, count, ZIPX_VOL_MODE_DISK, selected)) {
|
||||
free_parts(parts, count);
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------------- */
|
||||
|
||||
int
|
||||
zipx_volume_detect(const char *path, zipx_volume_t *out, char **err) {
|
||||
char dir[4096];
|
||||
const char *base;
|
||||
int rc;
|
||||
|
||||
if(err) {
|
||||
*err = NULL;
|
||||
}
|
||||
if(!path || !out) {
|
||||
return -1;
|
||||
}
|
||||
volume_reset(out);
|
||||
base = file_base(path);
|
||||
file_dir(path, dir, sizeof(dir));
|
||||
|
||||
rc = detect_digit_suffix(dir, base, path, out, err);
|
||||
if(rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
rc = detect_z_suffix(dir, base, path, out, err);
|
||||
if(rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
rc = detect_zip_tail(dir, base, path, out, err);
|
||||
if(rc != 0) {
|
||||
return rc;
|
||||
}
|
||||
return detect_part_suffix(dir, base, path, out, err);
|
||||
}
|
||||
|
||||
void
|
||||
zipx_volume_free(zipx_volume_t *vol) {
|
||||
int i;
|
||||
|
||||
if(!vol) {
|
||||
return;
|
||||
}
|
||||
if(vol->paths) {
|
||||
for(i = 0; i < vol->count; i++) {
|
||||
free(vol->paths[i]);
|
||||
}
|
||||
free(vol->paths);
|
||||
}
|
||||
memset(vol, 0, sizeof(*vol));
|
||||
vol->index = -1;
|
||||
}
|
||||
|
||||
int
|
||||
zipx_volume_is_first(const char *path) {
|
||||
const char *base = file_base(path);
|
||||
const char *dot = strrchr(base, '.');
|
||||
|
||||
if(!dot) {
|
||||
return 0;
|
||||
}
|
||||
if(strcmp(dot, ".001") == 0) {
|
||||
return 1;
|
||||
}
|
||||
if((dot[1] == 'z' || dot[1] == 'Z') && dot[2] == '0' && dot[3] == '1' &&
|
||||
dot[4] == 0) {
|
||||
return 1;
|
||||
}
|
||||
if(ends_with_ci(base, ".part1.zip")) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/* zipx_volume -- group a multi-file archive volume set into an ordered list.
|
||||
part of ps5-web-file-manager
|
||||
|
||||
Supports the naming conventions seen in the wild:
|
||||
|
||||
name.zip.001, name.zip.002, ... byte split (7-Zip "split to volumes")
|
||||
name.part1.zip, name.part2.zip byte split (WinRAR zip volumes)
|
||||
name.z01, name.z02, ..., name.zip zip split disks (Info-ZIP / PKZIP)
|
||||
|
||||
A caller can hand in any member of the set (the user usually clicks one file
|
||||
in the browser) and gets back the full ordered list plus the split layout so
|
||||
the engine can pick the matching stream behaviour. */
|
||||
|
||||
#ifndef ZIPX_VOLUME_H
|
||||
#define ZIPX_VOLUME_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define ZIPX_VOL_MAX_PARTS 512
|
||||
|
||||
typedef struct {
|
||||
char **paths; /* ordered part paths, owned by this struct */
|
||||
int count;
|
||||
int index; /* position of the path that was handed in (-1 unknown) */
|
||||
int mode; /* ZIPX_VOL_MODE_CONCAT or ZIPX_VOL_MODE_DISK */
|
||||
int is_set; /* 1 when the path is part of a multi-file set */
|
||||
} zipx_volume_t;
|
||||
|
||||
/* Inspects `path`: 1 when it belongs to a multi-file set (out is filled),
|
||||
0 when it is an ordinary single file (out is cleared), -1 on a hard error
|
||||
(*err, when non-NULL, receives a malloc'd message the caller must free;
|
||||
it is also set for the 0 case when a sibling set looks broken, so callers
|
||||
can surface "volumes are incomplete" instead of a generic open failure). */
|
||||
int zipx_volume_detect(const char *path, zipx_volume_t *out, char **err);
|
||||
|
||||
void zipx_volume_free(zipx_volume_t *vol);
|
||||
|
||||
/* True when `path` looks like the first volume of a set ("x.zip.001",
|
||||
"x.z01", "x.part1.zip"), used by the UI to label the entry. */
|
||||
int zipx_volume_is_first(const char *path);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Builds split-archive fixtures for the multi-volume extraction tests.
|
||||
|
||||
Output goes to tests/fixtures-split/ and covers the three naming conventions
|
||||
the engine recognises, plus two broken sets used to check the error messages:
|
||||
|
||||
plain.zip.001 .002 .003 byte split (7-Zip "split to volumes")
|
||||
parts.part1.zip ... .part3.zip byte split (WinRAR zip volumes)
|
||||
disks.z01 .z02 .zip zip split disks, offsets relative to each disk
|
||||
broken.zip.001 first volume only, the rest missing
|
||||
gap.zip.001 .gap.zip.003 volume 2 missing
|
||||
|
||||
The byte-split sets are produced by slicing one ordinary archive. The
|
||||
"disks" set is built by hand: the central directory is rewritten so that every
|
||||
entry records the disk it starts on and an offset relative to that disk, which
|
||||
is what a real zip split disk archive looks like (APPNOTE 4.4.11).
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import zipfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# The test binary receives tests/fixtures as its fixture directory, and
|
||||
# make_fixtures.py has already rebuilt that tree by the time this script runs,
|
||||
# so the split sets are added there (only our own files are replaced).
|
||||
OUT = os.path.join(HERE, "fixtures")
|
||||
|
||||
OWN_PREFIXES = ("plain.zip.", "parts.part", "disks.", "broken.zip.",
|
||||
"gap.zip.", "split_single.zip")
|
||||
|
||||
ENTRIES = [
|
||||
("readme.txt", b"split archive fixture\n" * 20),
|
||||
("sub/data.bin", bytes(range(256)) * 40),
|
||||
("sub/deep/more.bin", b"ABCD" * 3000),
|
||||
("tail.bin", bytes(reversed(range(256))) * 30),
|
||||
]
|
||||
|
||||
|
||||
def build_plain_zip():
|
||||
"""One ordinary archive; ZIP_STORED keeps the layout predictable."""
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w", zipfile.ZIP_STORED) as zf:
|
||||
for name, data in ENTRIES:
|
||||
info = zipfile.ZipInfo(name, date_time=(2026, 1, 1, 0, 0, 0))
|
||||
info.compress_type = zipfile.ZIP_STORED
|
||||
zf.writestr(info, data)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def read_central_directory(blob):
|
||||
"""Returns (cd_offset, cd_size, count, [(name, local_offset, cd_pos)])."""
|
||||
eocd = blob.rfind(b"PK\x05\x06")
|
||||
if eocd < 0:
|
||||
raise SystemExit("fixture build: no end-of-central-directory record")
|
||||
count, cd_size, cd_offset = struct.unpack_from("<HII", blob, eocd + 10)
|
||||
entries = []
|
||||
pos = cd_offset
|
||||
for _ in range(count):
|
||||
if blob[pos : pos + 4] != b"PK\x01\x02":
|
||||
raise SystemExit("fixture build: bad central directory signature")
|
||||
name_len, extra_len, comment_len = struct.unpack_from("<HHH", blob, pos + 28)
|
||||
local_offset = struct.unpack_from("<I", blob, pos + 42)[0]
|
||||
name = blob[pos + 46 : pos + 46 + name_len].decode("utf-8")
|
||||
entries.append((name, local_offset, pos))
|
||||
pos += 46 + name_len + extra_len + comment_len
|
||||
return cd_offset, cd_size, count, entries
|
||||
|
||||
|
||||
def split_into_disks(blob, parts=3):
|
||||
"""Rewrites offsets so the archive looks like a real zip split disk set.
|
||||
|
||||
Entries are packed into `parts - 1` disks; the last disk carries whatever
|
||||
is left plus the central directory and the end record.
|
||||
"""
|
||||
cd_offset, _cd_size, count, entries = read_central_directory(blob)
|
||||
entries.sort(key=lambda e: e[1])
|
||||
|
||||
# Local header size = fixed part + name + extra, so an entry ends where the
|
||||
# next one begins (or at the central directory for the last one).
|
||||
limits = []
|
||||
for index, (_name, local_offset, _cd_pos) in enumerate(entries):
|
||||
end = entries[index + 1][1] if index + 1 < len(entries) else cd_offset
|
||||
limits.append((local_offset, end))
|
||||
|
||||
target = max(1, cd_offset // (parts - 1))
|
||||
disk_of_entry = []
|
||||
disk_starts = [0]
|
||||
current = 0
|
||||
for local_offset, end in limits:
|
||||
if (end - disk_starts[current]) > target and current < parts - 2:
|
||||
current += 1
|
||||
disk_starts.append(local_offset)
|
||||
disk_of_entry.append(current)
|
||||
|
||||
out = bytearray(blob)
|
||||
# Central directory entries: disk number + offset relative to that disk.
|
||||
for (name, local_offset, cd_pos), disk in zip(entries, disk_of_entry):
|
||||
struct.pack_into("<H", out, cd_pos + 34, disk)
|
||||
struct.pack_into("<I", out, cd_pos + 42, local_offset - disk_starts[disk])
|
||||
|
||||
last_disk = current
|
||||
eocd = len(blob) - 22
|
||||
struct.pack_into("<H", out, eocd + 4, last_disk) # this disk
|
||||
struct.pack_into("<H", out, eocd + 6, last_disk) # disk with the cd
|
||||
struct.pack_into("<H", out, eocd + 8, count) # entries on this disk
|
||||
struct.pack_into("<H", out, eocd + 10, count) # entries in total
|
||||
struct.pack_into("<I", out, eocd + 16, cd_offset - disk_starts[last_disk])
|
||||
|
||||
bounds = disk_starts[1:] + [len(blob)]
|
||||
return [bytes(out[a:b]) for a, b in zip(disk_starts, bounds)]
|
||||
|
||||
|
||||
def write(path, data):
|
||||
with open(path, "wb") as handle:
|
||||
handle.write(data)
|
||||
print(" %s (%d bytes)" % (os.path.basename(path), len(data)))
|
||||
|
||||
|
||||
def write_set(base, names, chunks):
|
||||
for name, chunk in zip(names, chunks):
|
||||
write(os.path.join(OUT, name % base), chunk)
|
||||
|
||||
|
||||
def write_disk_set(base, chunks):
|
||||
"""Split disks are named name.z01, name.z02, ..., name.zip: the last volume
|
||||
(the one holding the central directory) drops the numeric suffix."""
|
||||
for index, chunk in enumerate(chunks):
|
||||
if index == len(chunks) - 1:
|
||||
name = "%s.zip" % base
|
||||
else:
|
||||
name = "%s.z%02d" % (base, index + 1)
|
||||
write(os.path.join(OUT, name), chunk)
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.isdir(OUT):
|
||||
os.makedirs(OUT)
|
||||
for name in os.listdir(OUT):
|
||||
if name.startswith(OWN_PREFIXES):
|
||||
os.remove(os.path.join(OUT, name))
|
||||
|
||||
plain = build_plain_zip()
|
||||
third = (len(plain) + 2) // 3
|
||||
chunks = [plain[0:third], plain[third : 2 * third], plain[2 * third :]]
|
||||
chunks = [c for c in chunks if c]
|
||||
|
||||
print("byte split (name.zip.NNN):")
|
||||
write_set("plain", ["%s.zip.001", "%s.zip.002", "%s.zip.003"], chunks)
|
||||
|
||||
print("byte split (name.partN.zip):")
|
||||
write_set("parts", ["%s.part1.zip", "%s.part2.zip", "%s.part3.zip"], chunks)
|
||||
|
||||
print("zip split disks (name.zNN + name.zip):")
|
||||
disks = split_into_disks(plain)
|
||||
write_disk_set("disks", disks)
|
||||
|
||||
print("broken sets:")
|
||||
write(os.path.join(OUT, "broken.zip.001"), chunks[0])
|
||||
write(os.path.join(OUT, "gap.zip.001"), chunks[0])
|
||||
write(os.path.join(OUT, "gap.zip.003"), chunks[2] if len(chunks) > 2 else b"")
|
||||
|
||||
# A plain archive next to the sets, so the tests can prove the normal
|
||||
# single file path still works now that volume detection runs first.
|
||||
write(os.path.join(OUT, "split_single.zip"), plain)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -23,6 +23,7 @@ find "$BUILD" -maxdepth 1 -type f -name '*.log' -delete 2>/dev/null || true
|
||||
mkdir -p "$BUILD"
|
||||
|
||||
"$PYTHON" "$ROOT/tests/make_fixtures.py"
|
||||
"$PYTHON" "$ROOT/tests/make_split_fixtures.py"
|
||||
|
||||
MZ_CFLAGS=(-I"$ROOT/third_party/minizip-ng/include" -DHAVE_ZLIB -DZLIB_COMPAT -D_FILE_OFFSET_BITS=64)
|
||||
RAR_CFLAGS=(-I"$ROOT/third_party/unrar" -DDMC_UNRAR_DISABLE_BE32TOH_BE64TOH=1)
|
||||
@@ -66,6 +67,16 @@ COMPAT_INC="$ROOT/tests/compat"
|
||||
-include "$ROOT/tests/posix_compat.h" \
|
||||
-o "$BUILD/zip_extract.o" "$ROOT/src/zip_extract.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" \
|
||||
-include "$ROOT/tests/posix_compat.h" \
|
||||
-o "$BUILD/zipx_volstream.o" "$ROOT/src/zipx_volstream.c"
|
||||
|
||||
"$CC" -c -O2 -Wall -Wextra -Wno-unused-parameter -I"$ROOT/src" \
|
||||
-I"$COMPAT_INC" -include "$ROOT/tests/posix_compat.h" \
|
||||
-o "$BUILD/zipx_volume.o" "$ROOT/src/zipx_volume.c"
|
||||
|
||||
"$CC" -c -O2 -Wall -Wextra -Wno-unused-parameter \
|
||||
-I"$ROOT/third_party/minizip-ng/include" -I"$ROOT/third_party/unrar7" -I"$ROOT/src" -I"$COMPAT_INC" \
|
||||
-include "$ROOT/tests/posix_compat.h" \
|
||||
|
||||
+122
-2
@@ -59,9 +59,23 @@ read_text(const char *path, char *buf, size_t size) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Reads binary content (entries may contain NUL bytes, so read_text cannot be
|
||||
used to verify them). Returns the number of bytes read, or -1. */
|
||||
static long
|
||||
read_bytes(const char *path, unsigned char *buf, size_t size) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
size_t n;
|
||||
|
||||
if(!f) {
|
||||
return -1;
|
||||
}
|
||||
n = fread(buf, 1, size, f);
|
||||
fclose(f);
|
||||
return (long)n;
|
||||
}
|
||||
|
||||
static int
|
||||
remove_dir(const char *path) {
|
||||
DIR *dir = opendir(path);
|
||||
remove_dir(const char *path) { DIR *dir = opendir(path);
|
||||
struct dirent *ent;
|
||||
|
||||
if(!dir) {
|
||||
@@ -199,6 +213,111 @@ expect_status(zipx_result_t *res, zipx_status_t status,
|
||||
|
||||
/**************************************************************************/
|
||||
|
||||
/* Verifies the entries all three split layouts share: the same archive was
|
||||
sliced three ways, so every extraction has to produce identical content. */
|
||||
static void
|
||||
check_split_content(const char *dst_name, const char *label) {
|
||||
static unsigned char data[16384];
|
||||
char dst[4096];
|
||||
char file[4224];
|
||||
char buf[512];
|
||||
long n;
|
||||
size_t i;
|
||||
int ok = 1;
|
||||
|
||||
work_path(dst, sizeof(dst), dst_name);
|
||||
check(exists(dst), label);
|
||||
|
||||
snprintf(file, sizeof(file), "%s/readme.txt", dst);
|
||||
n = read_bytes(file, (unsigned char *)buf, sizeof(buf));
|
||||
check(n == 440 && !memcmp(buf, "split archive fixture\n", 22),
|
||||
" readme.txt content");
|
||||
|
||||
snprintf(file, sizeof(file), "%s/sub/data.bin", dst);
|
||||
n = read_bytes(file, data, 10240);
|
||||
if(n != 10240) {
|
||||
ok = 0;
|
||||
} else {
|
||||
for(i = 0; i < 10240; i++) {
|
||||
if(data[i] != (unsigned char)(i % 256)) {
|
||||
ok = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
check(ok, " sub/data.bin binary content (10 KiB across volumes)");
|
||||
|
||||
snprintf(file, sizeof(file), "%s/sub/deep/more.bin", dst);
|
||||
n = read_bytes(file, data, 12000);
|
||||
check(n == 12000 && data[0] == 'A' && data[3] == 'D',
|
||||
" sub/deep/more.bin content");
|
||||
}
|
||||
|
||||
static void
|
||||
test_volumes(void) {
|
||||
zipx_result_t res;
|
||||
test_ctx_t t = {0};
|
||||
char dst[4096];
|
||||
|
||||
printf("split volumes\n");
|
||||
|
||||
/* name.zip.001 style (7-Zip byte split), picked from the first volume. */
|
||||
expect_ok(&res, run("plain.zip.001", "out_vol_plain_a", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
"byte split: extract from part 001");
|
||||
check_split_content("out_vol_plain_a", " output tree exists");
|
||||
|
||||
/* Same set, but the user clicked the last volume this time. */
|
||||
expect_ok(&res, run("plain.zip.003", "out_vol_plain_b", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
"byte split: extract from part 003");
|
||||
check_split_content("out_vol_plain_b", " output tree exists");
|
||||
|
||||
/* name.partN.zip style, picked from the middle volume. */
|
||||
expect_ok(&res, run("parts.part2.zip", "out_vol_parts", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
"partN split: extract from part2");
|
||||
check_split_content("out_vol_parts", " output tree exists");
|
||||
|
||||
/* name.z01 ... name.zip: offsets are relative to each disk, so this one
|
||||
exercises the disk-aware volume stream. */
|
||||
expect_ok(&res, run("disks.z01", "out_vol_disks_a", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
"split disks: extract from z01");
|
||||
check_split_content("out_vol_disks_a", " output tree exists");
|
||||
|
||||
expect_ok(&res, run("disks.zip", "out_vol_disks_b", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
"split disks: extract from the final volume");
|
||||
check_split_content("out_vol_disks_b", " output tree exists");
|
||||
|
||||
/* A plain archive must still take the ordinary path. */
|
||||
expect_ok(&res, run("split_single.zip", "out_vol_single", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
"plain archive still extracts (no volume set detected)");
|
||||
check_split_content("out_vol_single", " output tree exists");
|
||||
|
||||
/* Broken sets must fail with the exact missing volume, not a vague error. */
|
||||
expect_status(&res, run("gap.zip.001", "out_vol_gap", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
ZIPX_ERR_OPEN, "incomplete set: reported as an open failure");
|
||||
check(strstr(res.message, "gap.zip.002") != NULL,
|
||||
" error names the missing volume");
|
||||
printf(" message: %s\n", res.message);
|
||||
|
||||
expect_status(&res, run("broken.zip.001", "out_vol_broken", ZIPX_CONFLICT_FAIL,
|
||||
NULL, &t, &res),
|
||||
ZIPX_ERR_OPEN, "lone first volume: reported as an open failure");
|
||||
check(strstr(res.message, "no other volumes") != NULL,
|
||||
" error says the remaining volumes are missing");
|
||||
printf(" message: %s\n", res.message);
|
||||
|
||||
work_path(dst, sizeof(dst), "out_vol_gap");
|
||||
check(!exists(dst), "nothing was written for the incomplete set");
|
||||
}
|
||||
|
||||
/**************************************************************************/
|
||||
|
||||
static void
|
||||
test_basic(void) {
|
||||
zipx_result_t res;
|
||||
@@ -655,6 +774,7 @@ main(int argc, char **argv) {
|
||||
test_cancel();
|
||||
test_many_files();
|
||||
test_large_profile();
|
||||
test_volumes();
|
||||
|
||||
printf("\n%d checks, %d failures\n", g_checks, g_failures);
|
||||
return g_failures ? 1 : 0;
|
||||
|
||||
Reference in new issue
Block a user