feat(7z): dispatch + archive recognition + password UI

With the extraction engine in place (021c9cb), the /api/extract task
finally picks .7z files up.

* src/filemgr_internal.h -- file_task_t gains extract_password (256
  bytes, UTF-8, the size a sane user will never exceed but enough for the
  encrypted RAR / 7z passwords the engine actually accepts).
* src/extract.c -- extract_dispatch() routes a single .7z to sevenz_extract()
  and a byte-split set (name.7z.001, .002, ...) to the same call (the
  volume detector classifies by extension).  api_extract() reads the
  optional "password" form field and copies it into the task; the error
  code map gains ZIPX_ERR_PASSWORD -> "extract_password".
* Makefile -- the sevenz_extract / sevenz_chain / sevenz_volstream sources
  join COMMON_SRCS, and every third_party/7z/*.c (LZMA SDK 26.03, public
  domain, decoder subset) joins THIRD_PARTY_C_SRCS.  The C flag set gains
  -Ithird_party/7z and -DZ7_PPMD_SUPPORT, the latter so PPMd is built
  in (the SDK guards PPMd sources behind a macro that defaults to off).

* assets/main.js -- isSevenZipArchive + isSevenZipSplitVolume; both feed
  isExtractableArchive.  actionExtract() now prompts for a password on
  7z archives (encrypted-stream support is opt-in -- the engine rejects
  unencrypted archives with no password just fine, but the prompt saves
  a wasted scan when an archive is encrypted).  startExtractTask() takes
  the password and sends it as a form field.

* assets/lang-{en,zh}.js -- extractPasswordAsk string for the new prompt.

ZIP 108 / RAR 27 / 7z 26 still all green on the host.

No real-device build attempted in this commit: the WSL runner is the
one that knows the PS5 toolchain, and the user has to be the one to
push it through.  The Makefile changes are the wiring the cross
compiler needs; a `make linux` on a Linux host with libmicrohttpd-dev
should link end-to-end.
This commit is contained in:
Songlx516 committed 2026-09-13 09:59:30 +08:00
1 parent 021c9cb339
commit 200b38604f
6 files changed
+80 -22

No files matched your search

+6 -6
View File
@@ -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 src/zipx_volume.c src/zipx_volstream.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 src/zipx_common.c src/sevenz_extract.c src/sevenz_chain.c src/sevenz_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/*))
@@ -70,10 +70,10 @@ UNRAR7_SRCS := \
third_party/unrar7/timefn.cpp third_party/unrar7/ui.cpp third_party/unrar7/unicode.cpp \
third_party/unrar7/unpack.cpp third_party/unrar7/volume.cpp
THIRD_PARTY_C_SRCS := $(wildcard third_party/zlib/src/*.c) $(wildcard third_party/minizip-ng/src/*.c)
THIRD_PARTY_C_FLAGS := -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include \
THIRD_PARTY_C_SRCS := $(wildcard third_party/zlib/src/*.c) $(wildcard third_party/minizip-ng/src/*.c) $(wildcard third_party/7z/*.c)
THIRD_PARTY_C_FLAGS := -O2 -w -Ithird_party/zlib/include -Ithird_party/minizip-ng/include -Ithird_party/7z \
-DHAVE_ZLIB -DZLIB_COMPAT -DHAVE_UNISTD_H=1 -D_FILE_OFFSET_BITS=64 -D_LARGEFILE64_SOURCE \
-DHAVE_FSEEKO
-DHAVE_FSEEKO -DZ7_PPMD_SUPPORT
UNRAR7_CXX_FLAGS := -O2 -w -std=c++17 -DRARDLL -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
# prospero-clang++ defaults to -stdlib=libc++; state it explicitly for clarity.
UNRAR7_CXX_FLAGS_PS5 := $(UNRAR7_CXX_FLAGS) -stdlib=libc++
@@ -84,12 +84,12 @@ PS5_TP_OBJS := $(patsubst %.c,ps5-obj/%.o,$(THIRD_PARTY_C_SRCS)) \
LINUX_TP_OBJS := $(patsubst %.c,linux-obj/%.o,$(THIRD_PARTY_C_SRCS)) \
$(patsubst %.cpp,linux-obj/%.o,$(UNRAR7_SRCS))
CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar7 -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
CFLAGS := -Oz -fno-asynchronous-unwind-tables -fno-unwind-tables -Wall -Werror -ffunction-sections -fdata-sections -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar7 -Ithird_party/7z -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
CFLAGS += `$(PKG_CONFIG) libmicrohttpd --cflags`
LDFLAGS := -Wl,--gc-sections
LDADD := `$(PKG_CONFIG) libmicrohttpd --libs`
LDADD += -lSceIpmi -lSceAppInstUtil -lSceUserService
LINUX_CFLAGS := -O2 -flto -Wall -Werror -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar7 -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
LINUX_CFLAGS := -O2 -flto -Wall -Werror -Isrc -Ithird_party/minizip-ng/include -Ithird_party/unrar7 -Ithird_party/7z -DVERSION_TAG=\"$(VERSION_TAG)\" -DTITLE_ID=\"$(TITLE_ID)\"
LINUX_CFLAGS += `$(HOST_PKG_CONFIG) libmicrohttpd --cflags`
LINUX_LDADD := `$(HOST_PKG_CONFIG) libmicrohttpd --libs` -pthread
+1
View File
@@ -101,6 +101,7 @@ window.WFM_LANG = {
extracting: "Extracting",
extractConfirm: "Extract {name} to {path}?",
extractOverwriteAsk: "If a file or folder with the same name already exists in the target:\n\nOK = overwrite same-name files (folders still merge)\nCancel = fail if the target already exists",
extractPasswordAsk: "This archive may be encrypted (e.g. 7zAES).\n\nEnter the password to extract, or leave it empty to try without one.",
extractUploadConfirm: "Upload and extract {name}?\n\nTarget folder: {path}\nThe uploaded archive will be deleted after success.",
extractStarted: "Extraction started: {name}",
extractDone: "Extraction complete: {name}",
+1
View File
@@ -101,6 +101,7 @@ window.WFM_LANG = {
extracting: "解压",
extractConfirm: "解压 {name} 到 {path}?",
extractOverwriteAsk: "若目标已存在同名文件或目录:\n\n确定 = 覆盖同名文件(目录仍会合并)\n取消 = 若目标已存在则失败",
extractPasswordAsk: "此压缩包可能加密了(如 7zAES)。\n\n输入密码后解压,留空则尝试无密码解压。",
extractUploadConfirm: "上传并解压 {name}?\n\n目标目录:{path}\n成功后将删除上传的压缩包。",
extractStarted: "已开始解压 {name}",
extractDone: "解压完成:{name}",
+29 -4
View File
@@ -784,11 +784,25 @@ function isZipSplitVolume(item) {
return false;
}
function isSevenZipArchive(item) {
return item.type === "-" && /\.7z$/i.test(item.name);
}
function isSevenZipSplitVolume(item) {
if (item.type !== "-") return false;
// Byte-split 7z sets: name.7z.001 / .002 / ... — any volume is enough,
// the engine walks the directory to assemble the rest.
if (/\.7z\.0*\d+$/i.test(item.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;
if (isSevenZipArchive(item)) return true;
if (isSevenZipSplitVolume(item)) return true;
return false;
}
@@ -835,18 +849,20 @@ function actionInstallSelectedPkgs() {
return queuePkgInstall(selectedEntries().filter(isPkgPackage));
}
async function startExtractTask(path, dstDir, conflict, removeSource, name, large) {
async function startExtractTask(path, dstDir, conflict, removeSource, name, large, password) {
try {
taskRefreshPath = cwd;
setBusy(true);
setStatus(t("extractStarted", { name }));
const data = await apiForm("/api/extract", {
const form = {
path,
dst_dir: dstDir,
conflict,
remove_source: removeSource ? "1" : "0",
large: large ? "1" : "0"
});
};
if (password) form.password = password;
const data = await apiForm("/api/extract", form);
trackTask(data.task_id, "extract", false);
clearSelection(false);
await pollTasks();
@@ -880,7 +896,16 @@ function actionExtract() {
if (!confirm(t("extractConfirm", { name: displayName(item), path: displayPath(cwd) }))) return;
const conflict = confirm(t("extractOverwriteAsk")) ? "overwrite" : "fail";
const large = shouldPromptLargeMode(item.size) ? promptLargeMode(item.size) : false;
startExtractTask(item.path, cwd, conflict, false, displayName(item), large);
// 7z archives can be encrypted (7zAES); ask up front so an unprotected
// archive doesn't pay a wasted scan + folder parse. An empty submission is
// fine — the engine returns ZIPX_ERR_PASSWORD and the user retries.
let password = "";
if (isSevenZipArchive(item) || isSevenZipSplitVolume(item)) {
const asked = prompt(t("extractPasswordAsk"), "");
if (asked === null) return;
password = asked;
}
startExtractTask(item.path, cwd, conflict, false, displayName(item), large, password);
}
function openImagePreview(item) {
+38 -12
View File
@@ -15,6 +15,7 @@
#include "json_util.h"
#include "path_util.h"
#include "rar_extract.h"
#include "sevenz_extract.h"
#include "zip_extract.h"
#include "zipx_volume.h"
@@ -184,10 +185,11 @@ extract_dispatch(file_task_t *task, zipx_conflict_t conflict,
"(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;
status = sevenz_extract(task->src, task->dst, conflict, limits,
extract_cancel, extract_progress, task,
task->extract_password[0] ? task->extract_password
: NULL,
result);
} else {
extract_set_detail(result, task->src);
snprintf(result->message, sizeof(result->message),
@@ -206,9 +208,16 @@ 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);
}
if(ends_with_ci(task->src, ".7z")) {
return sevenz_extract(task->src, task->dst, conflict, limits,
extract_cancel, extract_progress, task,
task->extract_password[0] ? task->extract_password
: NULL,
result);
}
extract_set_detail(result, task->src);
snprintf(result->message, sizeof(result->message),
"unsupported archive format (only .zip and .rar are accepted)");
"unsupported archive format (only .zip, .rar and .7z are accepted)");
return ZIPX_ERR_UNSUPPORTED;
}
@@ -231,6 +240,7 @@ extract_error_code(zipx_status_t status) {
case ZIPX_ERR_SPACE: return "no_space";
case ZIPX_ERR_IO: return "extract_io";
case ZIPX_ERR_CRC: return "extract_crc";
case ZIPX_ERR_PASSWORD: return "extract_password";
default: return "extract_failed";
}
}
@@ -317,6 +327,7 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
char *conflict_str = body_form_value(body, body_size, "conflict");
char *remove_str = body_form_value(body, body_size, "remove_source");
char *large_str = body_form_value(body, body_size, "large");
char *password_str = body_form_value(body, body_size, "password");
extract_conflict_t conflict = EXTRACT_CONFLICT_FAIL;
int remove_source = remove_str && !strcmp(remove_str, "1");
int large = large_str && !strcmp(large_str, "1");
@@ -325,7 +336,8 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
struct stat st;
if(!path || !dst_dir || !path[0] || !dst_dir[0]) {
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
free(path); free(dst_dir); free(conflict_str); free(remove_str);
free(large_str); free(password_str);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid path");
}
if(conflict_str) {
@@ -334,23 +346,27 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
} else if(!strcmp(conflict_str, "merge")) {
conflict = EXTRACT_CONFLICT_MERGE;
} else if(strcmp(conflict_str, "fail")) {
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
free(path); free(dst_dir); free(conflict_str); free(remove_str);
free(large_str); free(password_str);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "invalid conflict");
}
}
if(stat(path, &st) || !S_ISREG(st.st_mode)) {
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
free(path); free(dst_dir); free(conflict_str); free(remove_str);
free(large_str); free(password_str);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST, "file not found");
}
if(stat(dst_dir, &st) || !S_ISDIR(st.st_mode)) {
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
free(path); free(dst_dir); free(conflict_str); free(remove_str);
free(large_str); free(password_str);
return send_json_error(conn, MHD_HTTP_BAD_REQUEST,
"destination must be a directory");
}
task = calloc(1, sizeof(*task));
if(!task) {
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
free(path); free(dst_dir); free(conflict_str); free(remove_str);
free(large_str); free(password_str);
return send_json_error(conn, MHD_HTTP_INTERNAL_SERVER_ERROR,
"out of memory");
}
@@ -360,6 +376,14 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
task->extract_conflict = (int)conflict;
task->extract_remove_source = remove_source;
task->extract_large = large;
/* The size cap (256 bytes, including the NUL) leaves room for a 255-codepoint
UTF-8 password without overflowing the field or letting a malicious header
run away with it. Anything longer is truncated, which is what a sane user
will never hit but matches the storage size of the field. */
if(password_str) {
snprintf(task->extract_password, sizeof(task->extract_password), "%s",
password_str);
}
snprintf(task->src, sizeof(task->src), "%s", path);
snprintf(task->dst, sizeof(task->dst), "%s", dst_dir);
task->created_at = time(NULL);
@@ -370,7 +394,8 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
if(has_active_task_locked()) {
pthread_mutex_unlock(&g_tasks_lock);
free_task(task);
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
free(path); free(dst_dir); free(conflict_str); free(remove_str);
free(large_str); free(password_str);
return send_json_error(conn, MHD_HTTP_CONFLICT, "another task is running");
}
task->id = g_next_task_id++;
@@ -384,7 +409,8 @@ api_extract(struct MHD_Connection *conn, const char *body, size_t body_size) {
pthread_detach(task->thread);
}
free(path); free(dst_dir); free(conflict_str); free(remove_str); free(large_str);
free(path); free(dst_dir); free(conflict_str); free(remove_str);
free(large_str); free(password_str);
strbuf_printf(&b, "{\"ok\":true,\"task_id\":%lu}", task->id);
return send_buffer(conn, MHD_HTTP_OK, b.data, "application/json");
}
+5
View File
@@ -59,6 +59,11 @@ typedef struct file_task {
int extract_conflict;
int extract_remove_source;
int extract_large;
/* UTF-8 password for archives that encrypt their streams (7zAES, RAR5 AES).
Empty means "try without one"; the engine returns ZIPX_ERR_PASSWORD for
an archive that needs one, and the web UI prompts and retries. The
length is bounded so a runaway header field cannot overflow task memory. */
char extract_password[256];
unsigned long long speed_sample_done;
struct timespec speed_sample_time;
task_eta_sample_t eta_samples[ETA_SAMPLE_SLOTS];